diff --git a/pom.xml b/pom.xml index a37cccb42..8a59bd182 100644 --- a/pom.xml +++ b/pom.xml @@ -307,7 +307,7 @@ false - org.jenkins.tools.test.PluginCompatTesterCli + org.jenkins.tools.test.CLI ${buildNumber} ${buildIsTainted} diff --git a/src/main/java/org/jenkins/tools/test/CLI.java b/src/main/java/org/jenkins/tools/test/CLI.java new file mode 100644 index 000000000..005675c3c --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/CLI.java @@ -0,0 +1,16 @@ +package org.jenkins.tools.test; + +import picocli.CommandLine; + +@CommandLine.Command( + name = "pct", + mixinStandardHelpOptions = true, + subcommands = {PluginCompatTesterCli.class, PluginListerCli.class}, + versionProvider = VersionProvider.class) +public class CLI { + + public static void main(String... args) { + int exitCode = new CommandLine(new CLI()).execute(args); + System.exit(exitCode); + } +} diff --git a/src/main/java/org/jenkins/tools/test/PluginCompatTester.java b/src/main/java/org/jenkins/tools/test/PluginCompatTester.java index f46423b6b..58c36950f 100644 --- a/src/main/java/org/jenkins/tools/test/PluginCompatTester.java +++ b/src/main/java/org/jenkins/tools/test/PluginCompatTester.java @@ -31,41 +31,34 @@ import hudson.util.VersionNumber; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; -import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarInputStream; -import java.util.jar.Manifest; +import java.util.NavigableMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; import org.jenkins.tools.test.exception.PluginCompatibilityTesterException; import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; -import org.jenkins.tools.test.exception.PomExecutionException; import org.jenkins.tools.test.maven.ExpressionEvaluator; import org.jenkins.tools.test.maven.ExternalMavenRunner; -import org.jenkins.tools.test.maven.MavenRunner; -import org.jenkins.tools.test.model.MavenPom; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.PluginRemoting; -import org.jenkins.tools.test.model.UpdateSite; import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; import org.jenkins.tools.test.model.hook.BeforeCompilationContext; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; import org.jenkins.tools.test.model.hook.PluginCompatTesterHooks; +import org.jenkins.tools.test.model.plugin_metadata.LocalCheckoutPluginMetadataExtractor; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; +import org.jenkins.tools.test.util.ServiceHelper; import org.jenkins.tools.test.util.StreamGobbler; +import org.jenkins.tools.test.util.WarExtractor; /** * Frontend for plugin compatibility tests @@ -77,9 +70,10 @@ public class PluginCompatTester { private static final Logger LOGGER = Logger.getLogger(PluginCompatTester.class.getName()); - /** First version with new parent POM. */ - public static final String JENKINS_CORE_FILE_REGEX = - "WEB-INF/lib/jenkins-core-([0-9.]+(?:-[0-9a-f.]+)*(?:-(?i)([a-z]+)(-)?([0-9a-f.]+)?)?(?:-(?i)([a-z]+)(-)?([0-9a-f_.]+)?)?(?:-SNAPSHOT)?)[.]jar"; + /** + * A sentinel value for the Git URL to be used for local checkouts. + */ + private static final String LOCAL_CHECKOUT = ""; private final PluginCompatTesterConfig config; private final ExternalMavenRunner runner; @@ -90,122 +84,102 @@ public PluginCompatTester(PluginCompatTesterConfig config) { } public void testPlugins() throws PluginCompatibilityTesterException { - PluginCompatTesterHooks pcth = - new PluginCompatTesterHooks(config.getExternalHooksJars(), config.getExcludeHooks()); - // Determine the plugin data - - // Scan bundled plugins. If there is any bundled plugin, only these plugins will be taken - // under the consideration for the PCT run. - UpdateSite.Data data = scanWAR(config.getWar(), "WEB-INF/(?:optional-)?plugins/([^/.]+)[.][hj]pi"); - if (!data.plugins.isEmpty()) { - // Scan detached plugins to recover proper Group IDs for them. At the moment, we are - // considering that bomfile contains the info about the detached ones. - UpdateSite.Data detachedData = scanWAR(config.getWar(), "WEB-INF/(?:detached-)?plugins/([^/.]+)[.][hj]pi"); - - // Add detached if and only if no added as normal one - if (detachedData != null) { - detachedData.plugins.forEach((key, value) -> { - if (!data.plugins.containsKey(key)) { - data.plugins.put(key, value); - } - }); - } - } + ServiceHelper serviceHelper = new ServiceHelper(config.getExternalHooksJars()); + PluginCompatTesterHooks pcth = new PluginCompatTesterHooks(serviceHelper, config.getExcludeHooks()); + + // Extract the metadata + WarExtractor warExtractor = new WarExtractor( + config.getWar(), serviceHelper, config.getIncludePlugins(), config.getExcludePlugins()); + String coreVersion = warExtractor.extractCoreVersion(); + List plugins = warExtractor.extractPlugins(); + NavigableMap> pluginsByRepository = WarExtractor.byRepository(plugins); - if (data.plugins.isEmpty()) { - throw new PluginCompatibilityTesterException( - "List of plugins to check is empty, it is not possible to run PCT"); + /* + * Run the before checkout hooks on everything that we are about to check out (as opposed to an existing local + * checkout). + */ + for (Plugin plugin : plugins) { + BeforeCheckoutContext c = new BeforeCheckoutContext(coreVersion, plugin, config); + pcth.runBeforeCheckout(c); } - // if there is only one plugin and it's not already resolved (not in the war) and there is a - // local checkout available then it needs to be added to the plugins to check - if (onlyOnePluginIncluded() - && localCheckoutProvided() - && !data.plugins.containsKey( - config.getIncludePlugins().iterator().next())) { - String artifactId = config.getIncludePlugins().iterator().next(); - UpdateSite.Plugin extracted = extractFromLocalCheckout(); - data.plugins.put(artifactId, extracted); + if (localCheckoutProvided()) { + LocalCheckoutPluginMetadataExtractor localCheckoutPluginMetadataExtractor = + new LocalCheckoutPluginMetadataExtractor(config, runner); + // Do not perform the before checkout hooks on a local checkout + List localCheckout = localCheckoutPluginMetadataExtractor.extractMetadata(); + pluginsByRepository.put(LOCAL_CHECKOUT, localCheckout); } - String coreVersion = data.core.version; + if (pluginsByRepository.keySet().isEmpty()) { + throw new MetadataExtractionException("List of plugins to check is empty"); + } PluginCompatibilityTesterException lastException = null; LOGGER.log(Level.INFO, "Starting plugin tests on core version {0}", coreVersion); - for (UpdateSite.Plugin plugin : data.plugins.values()) { - if (!config.getIncludePlugins().isEmpty() - && !config.getIncludePlugins().contains(plugin.name.toLowerCase())) { - LOGGER.log(Level.FINE, "Plugin {0} not in included plugins; skipping", plugin.name); - continue; - } - if (!config.getExcludePlugins().isEmpty() - && config.getExcludePlugins().contains(plugin.name.toLowerCase())) { - LOGGER.log(Level.INFO, "Plugin {0} in excluded plugins; skipping", plugin.name); - continue; - } + for (Map.Entry> entry : pluginsByRepository.entrySet()) { + // Construct a single working directory for the clone + String gitUrl = entry.getKey(); - PluginRemoting remote; - if (localCheckoutProvided() && onlyOnePluginIncluded()) { - // Only one plugin and checkout directory provided - remote = new PluginRemoting(new File(config.getLocalCheckoutDir(), "pom.xml")); - } else if (localCheckoutProvided()) { - // Local directory provided for more than one plugin, so each plugin is allocated in - // localCheckoutDir/plugin-name. If there is no subdirectory for the plugin, it will - // be cloned from SCM. - File pomFile = new File(new File(config.getLocalCheckoutDir(), plugin.name), "pom.xml"); - if (pomFile.exists()) { - remote = new PluginRemoting(pomFile); - } else { - remote = new PluginRemoting(plugin.url); - } + File cloneDir; + if (gitUrl.equals(LOCAL_CHECKOUT)) { + cloneDir = config.getLocalCheckoutDir(); } else { - // Only one plugin but checkout directory not provided or more than a plugin and no - // local checkout directory provided - remote = new PluginRemoting(plugin.url); + cloneDir = new File(config.getWorkingDir(), getRepoNameFromGitUrl(gitUrl)); + // All plugins from the same reactor are assumed to be of the same version + String tag = entry.getValue().get(0).getTag(); + + try { + cloneFromScm(gitUrl, config.getFallbackGitHubOrganization(), tag, cloneDir); + } catch (PluginSourcesUnavailableException e) { + lastException = throwOrAddSuppressed(lastException, e, config.isFailFast()); + LOGGER.log( + Level.SEVERE, + String.format("Internal error while cloning repository %s at commit %s.", gitUrl, tag), + e); + continue; + } } - - try { - Model model = remote.retrieveModel(); - testPluginAgainst(coreVersion, plugin, model, pcth); - } catch (PluginCompatibilityTesterException e) { - lastException = throwOrAddSuppressed(lastException, e, config.isFailFast()); - LOGGER.log( - Level.SEVERE, - String.format( - "Internal error while executing a test for core %s and plugin %s at version %s.", - coreVersion, plugin.getDisplayName(), plugin.version), - e); + // For each of the plugin metadata entries, go test the plugin + for (Plugin plugin : entry.getValue()) { + try { + testPluginAgainst(coreVersion, plugin, cloneDir, pcth); + } catch (PluginCompatibilityTesterException e) { + lastException = throwOrAddSuppressed(lastException, e, config.isFailFast()); + LOGGER.log( + Level.SEVERE, + String.format( + "Internal error while executing a test for core %s and plugin %s at version %s.", + coreVersion, plugin.getName(), plugin.getVersion()), + e); + } } } - if (lastException != null) { throw lastException; } } - private UpdateSite.Plugin extractFromLocalCheckout() throws PluginSourcesUnavailableException { - Model model = new PluginRemoting(new File(config.getLocalCheckoutDir(), "pom.xml")).retrieveModel(); - return new UpdateSite.Plugin( - model.getArtifactId(), - "" /* version is not required */, - model.getScm().getConnection(), - null); - } - - private static File createBuildLogFile( - File workDirectory, String pluginName, String pluginVersion, String coreVersion) { - return new File(workDirectory.getAbsolutePath() + private static File createBuildLogFile(File workDirectory, Plugin plugin, String coreVersion) { + File f = new File(workDirectory.getAbsolutePath() + File.separator - + createBuildLogFilePathFor(pluginName, pluginVersion, coreVersion)); + + createBuildLogFilePathFor(plugin.getPluginId(), plugin.getVersion(), coreVersion)); + try { + Files.createDirectories(f.getParentFile().toPath()); + Files.deleteIfExists(f.toPath()); + Files.createFile(f.toPath()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create build log file", e); + } + return f; } - private static String createBuildLogFilePathFor(String pluginName, String pluginVersion, String coreVersion) { - return String.format("logs/%s/v%s_against_core_version_%s.log", pluginName, pluginVersion, coreVersion); + private static String createBuildLogFilePathFor(String pluginId, String pluginVersion, String coreVersion) { + return String.format("logs/%s/v%s_against_core_version_%s.log", pluginId, pluginVersion, coreVersion); } - private void testPluginAgainst( - String coreVersion, UpdateSite.Plugin plugin, Model model, PluginCompatTesterHooks pcth) + private void testPluginAgainst(String coreVersion, Plugin plugin, File cloneLocation, PluginCompatTesterHooks pcth) throws PluginCompatibilityTesterException { LOGGER.log( Level.INFO, @@ -217,119 +191,40 @@ private void testPluginAgainst( + "##\n" + "#############################################\n" + "#############################################\n\n\n\n\n", - new Object[] {plugin.name, plugin.version, coreVersion}); - - File pluginCheckoutDir = - new File(config.getWorkingDir().getAbsolutePath() + File.separator + plugin.name + File.separator); - String parentFolder = null; - - // Run any precheckout hooks - BeforeCheckoutContext beforeCheckout = new BeforeCheckoutContext(plugin, model, coreVersion, config); - pcth.runBeforeCheckout(beforeCheckout); - - if (!beforeCheckout.ranCheckout()) { - File checkoutDir = beforeCheckout.getCheckoutDir(); - if (checkoutDir != null) { - pluginCheckoutDir = checkoutDir; - } - try { - if (Files.isDirectory(pluginCheckoutDir.toPath())) { - LOGGER.log(Level.INFO, "Deleting working directory {0}", pluginCheckoutDir.getAbsolutePath()); - FileUtils.deleteDirectory(pluginCheckoutDir); - } - - Files.createDirectory(pluginCheckoutDir.toPath()); - LOGGER.log(Level.INFO, "Created plugin checkout directory {0}", pluginCheckoutDir.getAbsolutePath()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - if (localCheckoutProvided()) { - if (!onlyOnePluginIncluded()) { - File localCheckoutPluginDir = new File(config.getLocalCheckoutDir(), plugin.name); - File pomLocalCheckoutPluginDir = new File(localCheckoutPluginDir, "pom.xml"); - if (pomLocalCheckoutPluginDir.exists()) { - LOGGER.log( - Level.INFO, - "Copying plugin directory from {0}", - localCheckoutPluginDir.getAbsolutePath()); - try { - org.codehaus.plexus.util.FileUtils.copyDirectoryStructure( - localCheckoutPluginDir, pluginCheckoutDir); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } else { - cloneFromScm( - model.getScm().getConnection(), - config.getFallbackGitHubOrganization(), - model.getScm().getTag(), - pluginCheckoutDir); - } - } else { - // TODO this fails when it encounters symlinks (e.g. - // work/jobs/…/builds/lastUnstableBuild), and even up-to-date versions of - // org.apache.commons.io.FileUtils seem to not handle links, so may need to - // use something like - // http://docs.oracle.com/javase/tutorial/displayCode.html?code=http://docs.oracle.com/javase/tutorial/essential/io/examples/Copy.java - File localCheckoutDir = config.getLocalCheckoutDir(); - if (localCheckoutDir == null) { - throw new AssertionError("Could never happen, but needed to silence SpotBugs"); - } - LOGGER.log(Level.INFO, "Copy plugin directory from {0}", localCheckoutDir.getAbsolutePath()); - try { - org.codehaus.plexus.util.FileUtils.copyDirectoryStructure(localCheckoutDir, pluginCheckoutDir); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - } else { - // These hooks could redirect the SCM, skip checkout (if multiple plugins use - // the same preloaded repo) - cloneFromScm( - model.getScm().getConnection(), - config.getFallbackGitHubOrganization(), - model.getScm().getTag(), - pluginCheckoutDir); - } - } else { - // If the plugin exists in a different directory (multi-module plugins) - if (beforeCheckout.getPluginDir() != null) { - pluginCheckoutDir = beforeCheckout.getCheckoutDir(); - } - if (beforeCheckout.getParentFolder() != null) { - parentFolder = beforeCheckout.getParentFolder(); - } - LOGGER.log( - Level.INFO, - "The plugin has already been checked out, likely due to a multi-module" + " situation; continuing"); - } + new Object[] {plugin.getName(), plugin.getVersion(), coreVersion}); - File buildLogFile = createBuildLogFile(config.getWorkingDir(), plugin.name, plugin.version, coreVersion); - try { - FileUtils.forceMkdir(buildLogFile.getParentFile()); // Creating log directory - FileUtils.touch(buildLogFile); // Creating log file - } catch (IOException e) { - throw new UncheckedIOException(e); - } + File buildLogFile = createBuildLogFile(config.getWorkingDir(), plugin, coreVersion); - // Ran the BeforeCompileHooks + // Run the before compile hooks BeforeCompilationContext beforeCompile = - new BeforeCompilationContext(plugin, model, coreVersion, config, pluginCheckoutDir, parentFolder); + new BeforeCompilationContext(coreVersion, plugin, config, cloneLocation); pcth.runBeforeCompilation(beforeCompile); // First build against the original POM. This defends against source incompatibilities // (which we do not care about for this purpose); and ensures that we are testing a // plugin binary as close as possible to what was actually released. We also skip // potential javadoc execution to avoid general test failure. - if (!beforeCompile.ranCompile()) { - runner.run( - Map.of("maven.javadoc.skip", "true"), - pluginCheckoutDir, - buildLogFile, - "clean", - "process-test-classes"); + + Map properties = new LinkedHashMap<>(); + properties.put("maven.javadoc.skip", "true"); + + /* + * For multi-module projects where one plugin depends on another plugin in the same multi-module project, pass + * -Dset.changelist for incrementals releases so that Maven can find the first module when compiling the classes + * for the second module. + */ + boolean setChangelist = false; + ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(cloneLocation, null, runner); + if (!expressionEvaluator.evaluateList("project.modules").isEmpty()) { + String version = expressionEvaluator.evaluateString("project.version"); + if (version.contains("999999-SNAPSHOT") && !plugin.getVersion().equals(version)) { + setChangelist = true; + } + } + if (setChangelist) { + properties.put("set.changelist", "true"); } + runner.run(properties, cloneLocation, plugin.getModule(), buildLogFile, "clean", "process-test-classes"); List args = new ArrayList<>(); args.add("hpi:resolve-test-dependencies"); @@ -337,21 +232,19 @@ private void testPluginAgainst( args.add("surefire:test"); // Run preexecution hooks - BeforeExecutionContext forExecutionHooks = new BeforeExecutionContext( - plugin, - model, - coreVersion, - config, - pluginCheckoutDir, - parentFolder, - args, - new MavenPom(pluginCheckoutDir)); + BeforeExecutionContext forExecutionHooks = + new BeforeExecutionContext(coreVersion, plugin, config, cloneLocation, args); pcth.runBeforeExecution(forExecutionHooks); - Map properties = new LinkedHashMap<>(config.getMavenProperties()); + properties = new LinkedHashMap<>(config.getMavenProperties()); properties.put("overrideWar", config.getWar().toString()); properties.put("jenkins.version", coreVersion); properties.put("useUpperBounds", "true"); + if (setChangelist) { + properties.put("set.changelist", "true"); + // As hooks may be adjusting the POMs, tell git-changelist-extension to ignore dirty commits. + properties.put("ignore.dirt", "true"); + } if (new VersionNumber(coreVersion).isOlderThan(new VersionNumber("2.382"))) { /* * Versions of Jenkins prior to 2.382 are susceptible to JENKINS-68696, in which @@ -365,16 +258,16 @@ private void testPluginAgainst( // Execute with tests runner.run( - Collections.unmodifiableMap(properties), pluginCheckoutDir, buildLogFile, args.toArray(new String[0])); + Collections.unmodifiableMap(properties), + cloneLocation, + plugin.getModule(), + buildLogFile, + args.toArray(new String[0])); } - public static void cloneFromScm( + private static void cloneFromScm( String url, String fallbackGitHubOrganization, String scmTag, File checkoutDirectory) throws PluginSourcesUnavailableException { - if (StringUtils.startsWith(url, "scm:git:")) { - url = StringUtils.substringAfter(url, "scm:git:"); - } - List gitUrls = new ArrayList<>(); gitUrls.add(url); if (fallbackGitHubOrganization != null) { @@ -643,7 +536,7 @@ private static void cloneImpl(String gitUrl, String scmTag, File checkoutDirecto } } - public static List getFallbackGitUrl( + private static List getFallbackGitUrl( List gitUrls, String gitUrlFromMetadata, String fallbackGitHubOrganization) { Pattern pattern = Pattern.compile("(.*github.com[:|/])([^/]*)(.*)"); Matcher matcher = pattern.matcher(gitUrlFromMetadata); @@ -661,97 +554,17 @@ private boolean localCheckoutProvided() { return localCheckoutDir != null && localCheckoutDir.exists(); } - private boolean onlyOnePluginIncluded() { - return config.getIncludePlugins().size() == 1; - } - - /** - * Scans through a WAR file, accumulating plugin information - * - * @param war WAR to scan - * @param pluginRegExp The plugin regexp to use, can be used to differentiate between detached - * or "normal" plugins in the war file - * @return Update center data - */ - @SuppressFBWarnings(value = "REDOS", justification = "intended behavior") - static UpdateSite.Data scanWAR(File war, String pluginRegExp) { - UpdateSite.Entry core = null; - List plugins = new ArrayList<>(); - try (JarFile jf = new JarFile(war)) { - Enumeration entries = jf.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - String name = entry.getName(); - Matcher m = Pattern.compile(JENKINS_CORE_FILE_REGEX).matcher(name); - if (m.matches()) { - if (core != null) { - throw new IllegalStateException(">1 jenkins-core.jar in " + war); - } - // http://foobar is used to workaround the check in - // https://github.com/jenkinsci/jenkins/commit/f8daafd0327081186c06555f225e84c420261b4c - // We do not really care about the value - core = new UpdateSite.Entry("core", m.group(1), "https://foobar"); - } - - m = Pattern.compile(pluginRegExp).matcher(name); - if (m.matches()) { - try (InputStream is = jf.getInputStream(entry); - JarInputStream jis = new JarInputStream(is)) { - Manifest manifest = jis.getManifest(); - String shortName = manifest.getMainAttributes().getValue("Short-Name"); - if (shortName == null) { - shortName = manifest.getMainAttributes().getValue("Extension-Name"); - if (shortName == null) { - shortName = m.group(1); - } - } - String longName = manifest.getMainAttributes().getValue("Long-Name"); - String version = manifest.getMainAttributes().getValue("Plugin-Version"); - // Remove extra build information from the version number - final Matcher matcher = - Pattern.compile("^(.+-SNAPSHOT)(.+)$").matcher(version); - if (matcher.matches()) { - version = matcher.group(1); - } - String url = "jar:" + war.toURI() + "!/" + name; - UpdateSite.Plugin plugin = new UpdateSite.Plugin(shortName, version, url, longName); - plugins.add(plugin); - } - } - } - } catch (IOException e) { - throw new UncheckedIOException(e); + public static String getRepoNameFromGitUrl(String gitUrl) throws PluginSourcesUnavailableException { + // obtain the last path component (and strip any trailing .git) + int index = gitUrl.lastIndexOf("/"); + if (index < 0) { + throw new PluginSourcesUnavailableException("Failed to obtain local directory for " + gitUrl); } - if (core == null) { - throw new IllegalStateException("no jenkins-core.jar in " + war); - } - LOGGER.log(Level.INFO, "Scanned contents of {0} with {1} plugins", new Object[] {war, plugins.size()}); - return new UpdateSite.Data(core, plugins); - } - - /** - * Provides the Maven module used for a plugin on a {@code mvn [...] -pl} operation in the - * parent path - */ - public static String getMavenModule(String plugin, File pluginPath, MavenRunner runner) - throws PomExecutionException { - String absolutePath = pluginPath.getAbsolutePath(); - if (absolutePath.endsWith(plugin)) { - return plugin; - } - String target = absolutePath.substring(absolutePath.lastIndexOf(File.separatorChar) + 1); - File parentFile = pluginPath.getParentFile(); - if (parentFile == null) { - return null; - } - ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(parentFile, runner); - List modules = expressionEvaluator.evaluateList("project.modules"); - for (String module : modules) { - if (module.contains(target)) { - return module; - } + String name = gitUrl.substring(++index); + if (name.endsWith(".git")) { + return name.substring(0, name.length() - 4); } - return null; + return name; } /** diff --git a/src/main/java/org/jenkins/tools/test/PluginCompatTesterCli.java b/src/main/java/org/jenkins/tools/test/PluginCompatTesterCli.java index 77509efc7..a7af01a22 100644 --- a/src/main/java/org/jenkins/tools/test/PluginCompatTesterCli.java +++ b/src/main/java/org/jenkins/tools/test/PluginCompatTesterCli.java @@ -42,7 +42,7 @@ import picocli.CommandLine; @CommandLine.Command( - name = "pct", + name = "test-plugins", mixinStandardHelpOptions = true, description = "Perform a compatibility test for plugins against Jenkins core and other plugins.", versionProvider = VersionProvider.class) @@ -196,11 +196,6 @@ public Integer call() throws PluginCompatibilityTesterException { PluginCompatTester tester = new PluginCompatTester(config); tester.testPlugins(); - return 0; - } - - public static void main(String... args) { - int exitCode = new CommandLine(new PluginCompatTesterCli()).execute(args); - System.exit(exitCode); + return Integer.valueOf(0); } } diff --git a/src/main/java/org/jenkins/tools/test/PluginListerCli.java b/src/main/java/org/jenkins/tools/test/PluginListerCli.java new file mode 100644 index 000000000..86b26e98b --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/PluginListerCli.java @@ -0,0 +1,116 @@ +package org.jenkins.tools.test; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; +import org.jenkins.tools.test.picocli.ExistingFileTypeConverter; +import org.jenkins.tools.test.util.ServiceHelper; +import org.jenkins.tools.test.util.WarExtractor; +import picocli.CommandLine; + +@CommandLine.Command( + name = "list-plugins", + mixinStandardHelpOptions = true, + description = "List (non-detached) plugins and their associated repositories that the bundled in the WAR.", + versionProvider = VersionProvider.class) +public class PluginListerCli implements Callable { + + @CommandLine.Option( + names = {"-w", "--war"}, + required = true, + description = "Path to the WAR file to be examined.", + converter = ExistingFileTypeConverter.class) + private File warFile; + + @CommandLine.Option( + names = "--external-hooks-jars", + split = ",", + arity = "1", + paramLabel = "jar", + description = "Comma-separated list of paths to external hooks JARs.", + converter = ExistingFileTypeConverter.class) + private Set externalHooksJars = Set.of(); + + @CheckForNull + @CommandLine.Option( + names = {"-o", "--output"}, + required = false, + description = "Location of the file to write containing the plugins grouped by repository." + + " The format of the file is a line per repository; each line consists of" + + " a comma-separated list of plugins in that repository.") + private File output; + + @CheckForNull + @CommandLine.Option( + names = "--include-plugins", + split = ",", + arity = "1", + paramLabel = "plugin", + description = + "Comma-separated list of plugin artifact IDs to test. If not set, every plugin in the WAR will be listed.") + private Set includePlugins; + + @CheckForNull + @CommandLine.Option( + names = "--exclude-plugins", + split = ",", + arity = "1", + paramLabel = "plugin", + description = + "Comma-separated list of plugin artifact IDs to skip. If not set, only the plugins specified by --plugins will be listed (or all plugins otherwise).") + private Set excludePlugins; + + @Override + public Integer call() throws MetadataExtractionException { + ServiceHelper serviceHelper = new ServiceHelper(externalHooksJars); + WarExtractor warExtractor = new WarExtractor(warFile, serviceHelper, includePlugins, excludePlugins); + List plugins = warExtractor.extractPlugins(); + if (plugins.isEmpty()) { + throw new MetadataExtractionException("Found no plugins in " + warFile); + } + + if (output != null) { + NavigableMap> pluginsByRepository = WarExtractor.byRepository(plugins); + + try (BufferedWriter writer = Files.newBufferedWriter(output.toPath())) { + for (Map.Entry> entry : pluginsByRepository.entrySet()) { + writer.write( + entry.getValue().stream().map(Plugin::getPluginId).collect(Collectors.joining(","))); + writer.newLine(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + // First find the longest String so we can pad correctly + int maxLength = plugins.stream() + .map(Plugin::getPluginId) + .map(String::length) + .max(Integer::compareTo) + .get(); + + // Add some padding for the longest entry + maxLength += 4; + + System.out.println(String.format(Locale.ROOT, "%-" + maxLength + "s%s", "PLUGIN", "REPOSITORY")); + for (Plugin plugin : plugins) { + System.out.println( + String.format(Locale.ROOT, "%-" + maxLength + "s%s", plugin.getPluginId(), plugin.getGitUrl())); + } + } + + return Integer.valueOf(0); + } +} diff --git a/src/main/java/org/jenkins/tools/test/exception/MetadataExtractionException.java b/src/main/java/org/jenkins/tools/test/exception/MetadataExtractionException.java new file mode 100644 index 000000000..8519e76d1 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/exception/MetadataExtractionException.java @@ -0,0 +1,18 @@ +package org.jenkins.tools.test.exception; + +/** + * Exception used when extracting metadata from the WAR fails; e.g. the list of plugins or Jenkins + * version could not be obtained. + */ +public class MetadataExtractionException extends PluginCompatibilityTesterException { + + private static final long serialVersionUID = 1L; + + public MetadataExtractionException(String message, Throwable cause) { + super(message, cause); + } + + public MetadataExtractionException(String message) { + super(message); + } +} diff --git a/src/main/java/org/jenkins/tools/test/hook/AbstractMultiParentHook.java b/src/main/java/org/jenkins/tools/test/hook/AbstractMultiParentHook.java deleted file mode 100644 index 5311f410b..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/AbstractMultiParentHook.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.File; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.PluginCompatTester; -import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; - -/** Utility class to ease create simple hooks for multi-module projects */ -public abstract class AbstractMultiParentHook extends PluginCompatTesterHookBeforeCheckout { - - private static final Logger LOGGER = Logger.getLogger(AbstractMultiParentHook.class.getName()); - - protected boolean firstRun = true; - - @Override - @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "intended behavior") - public void action(@NonNull BeforeCheckoutContext context) throws PluginSourcesUnavailableException { - - // We should not execute the hook if using localCheckoutDir - File localCheckoutDir = context.getConfig().getLocalCheckoutDir(); - boolean shouldExecuteHook = localCheckoutDir == null || !localCheckoutDir.exists(); - - if (shouldExecuteHook) { - LOGGER.log(Level.INFO, "Executing hook for {0}", context.getPlugin().getDisplayName()); - // Determine if we need to run the download; only run for first identified plugin in the - // series - if (firstRun) { - LOGGER.log(Level.INFO, "Preparing for multi-module checkout"); - - // Checkout to the parent directory. All other processes will be on the child - // directory - File parentPath = - new File(context.getConfig().getWorkingDir().getAbsolutePath() + "/" + getParentFolder()); - - Model model = context.getModel(); - // Like the call in PluginCompatTester#runHooks but with subdirectories trimmed: - PluginCompatTester.cloneFromScm( - model.getScm().getConnection(), - context.getConfig().getFallbackGitHubOrganization(), - model.getScm().getTag(), - parentPath); - } - - // Checkout already happened, don't run through again - context.setRanCheckout(true); - firstRun = false; - - // Change the "download"" directory; after download, it's simply used for reference - File childPath = new File(context.getConfig().getWorkingDir().getAbsolutePath() - + "/" - + getParentFolder() - + "/" - + getPluginFolderName(context)); - - LOGGER.log(Level.INFO, "Child path for {0}: {1}", new Object[] { - context.getPlugin().getDisplayName(), childPath.getPath() - }); - context.setCheckoutDir(childPath); - context.setPluginDir(childPath); - context.setParentFolder(getParentFolder()); - } else { - configureLocalCheckOut(context.getConfig().getLocalCheckoutDir(), context); - } - } - - protected void configureLocalCheckOut(File localCheckoutDir, @NonNull BeforeCheckoutContext context) { - // Do nothing to keep compatibility with pre-existing Hooks - LOGGER.log( - Level.INFO, - "Ignoring local checkout directory for {0}", - context.getPlugin().getDisplayName()); - } - - /** - * Return the folder where the multi-module project will be checked out. This should be the name - * of the plugin's Git repository. - */ - protected abstract String getParentFolder(); - - /** - * Returns the plugin folder name. By default it will be the plugin name, but it can be - * overridden to support plugins (like {@code workflow-cps}) that are not located in a folder - * with the same name as the plugin itself. - */ - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return context.getPlugin().name; - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/AnalysisPomExecutionHook.java b/src/main/java/org/jenkins/tools/test/hook/AnalysisPomExecutionHook.java index 086a62073..b7886d70a 100644 --- a/src/main/java/org/jenkins/tools/test/hook/AnalysisPomExecutionHook.java +++ b/src/main/java/org/jenkins/tools/test/hook/AnalysisPomExecutionHook.java @@ -2,7 +2,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Set; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeExecution; import org.kohsuke.MetaInfServices; @@ -27,9 +26,6 @@ public class AnalysisPomExecutionHook extends PluginWithFailsafeIntegrationTests @Override public boolean check(@NonNull BeforeExecutionContext context) { - Model model = context.getModel(); - return "io.jenkins.plugins".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); + return ARTIFACT_IDS.contains(context.getPlugin().getPluginId()); } } diff --git a/src/main/java/org/jenkins/tools/test/hook/AwsJavaSdkHook.java b/src/main/java/org/jenkins/tools/test/hook/AwsJavaSdkHook.java deleted file mode 100644 index 71bd71df6..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/AwsJavaSdkHook.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class AwsJavaSdkHook extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "aws-java-sdk-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return ("org.jenkins-ci.plugins".equals(model.getGroupId()) - && "aws-java-sdk".equals(model.getArtifactId()) - && "hpi".equals(model.getPackaging())) - || ("org.jenkins-ci.plugins.aws-java-sdk".equals(model.getGroupId()) - && model.getArtifactId().startsWith("aws-java-sdk") - && "hpi".equals(model.getPackaging())); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/BlueOceanHook.java b/src/main/java/org/jenkins/tools/test/hook/BlueOceanHook.java deleted file mode 100644 index 7eacc8cab..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/BlueOceanHook.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -/** Workaround for the Blue Ocean plugins since they are stored in a central repository. */ -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class BlueOceanHook extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "blueocean-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "io.jenkins.blueocean".equals(model.getGroupId()) - && (model.getArtifactId().startsWith("blueocean") - || "jenkins-design-language".equals(model.getArtifactId())) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/ConfigurationAsCodeHook.java b/src/main/java/org/jenkins/tools/test/hook/ConfigurationAsCodeHook.java deleted file mode 100644 index 40564bd78..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/ConfigurationAsCodeHook.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class ConfigurationAsCodeHook extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "configuration-as-code-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "io.jenkins".equals(model.getGroupId()) - && "configuration-as-code".equals(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } - - @Override - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return "plugin"; - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineHook.java b/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineHook.java deleted file mode 100644 index d032dcd90..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineHook.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -/** - * Workaround for the Pipeline: Declarative plugins since they are stored in a central repository. - */ -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class DeclarativePipelineHook extends AbstractMultiParentHook { - - private static final Set ARTIFACT_IDS = Set.of( - "pipeline-model-api", - "pipeline-model-definition", - "pipeline-model-extensions", - "pipeline-stage-tags-metadata"); - - @Override - protected String getParentFolder() { - return "pipeline-model-definition-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "org.jenkinsci.plugins".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineMigrationHook.java b/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineMigrationHook.java deleted file mode 100644 index ec9cdd5e6..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/DeclarativePipelineMigrationHook.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -/** - * Workaround for the Declarative Pipeline Migration Assistant plugins since they are stored in a - * central repository. - */ -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class DeclarativePipelineMigrationHook extends AbstractMultiParentHook { - - private static final Set ARTIFACT_IDS = - Set.of("declarative-pipeline-migration-assistant", "declarative-pipeline-migration-assistant-api"); - - @Override - protected String getParentFolder() { - return "declarative-pipeline-migration-assistant-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "org.jenkins-ci.plugins.to-declarative".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/JacocoHook.java b/src/main/java/org/jenkins/tools/test/hook/JacocoHook.java index eb3d2a793..abb7ccedf 100644 --- a/src/main/java/org/jenkins/tools/test/hook/JacocoHook.java +++ b/src/main/java/org/jenkins/tools/test/hook/JacocoHook.java @@ -3,7 +3,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.stream.IntStream; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeExecution; import org.kohsuke.MetaInfServices; @@ -17,10 +16,7 @@ public class JacocoHook extends PluginCompatTesterHookBeforeExecution { @Override public boolean check(@NonNull BeforeExecutionContext context) { - Model model = context.getModel(); - // pending https://github.com/jenkinsci/jacoco-plugin/pull/206 - // "org.jenkins-ci.plugins".equals(model.getGroupId()) - return "jacoco".equals(model.getArtifactId()) && "hpi".equals(model.getPackaging()); + return context.getPlugin().getPluginId().equals("jacoco"); } @Override diff --git a/src/main/java/org/jenkins/tools/test/hook/MinaSshdApi.java b/src/main/java/org/jenkins/tools/test/hook/MinaSshdApi.java deleted file mode 100644 index 744e2c441..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/MinaSshdApi.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class MinaSshdApi extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "mina-sshd-api-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "io.jenkins.plugins.mina-sshd-api".equals(model.getGroupId()) - && model.getArtifactId().startsWith("mina-sshd-api") - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/MultiParentCompileHook.java b/src/main/java/org/jenkins/tools/test/hook/MultiParentCompileHook.java deleted file mode 100644 index 1bf09a74f..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/MultiParentCompileHook.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.File; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.commons.lang.StringUtils; -import org.jenkins.tools.test.PluginCompatTester; -import org.jenkins.tools.test.exception.PomExecutionException; -import org.jenkins.tools.test.maven.ExpressionEvaluator; -import org.jenkins.tools.test.maven.ExternalMavenRunner; -import org.jenkins.tools.test.maven.MavenRunner; -import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.BeforeCompilationContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHook; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCompile; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHooks; -import org.jenkins.tools.test.model.hook.Stage; -import org.jenkins.tools.test.model.hook.StageContext; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCompile.class) -@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "intended behavior") -public class MultiParentCompileHook extends PluginCompatTesterHookBeforeCompile { - - private static final Logger LOGGER = Logger.getLogger(MultiParentCompileHook.class.getName()); - - protected MavenRunner runner; - - public MultiParentCompileHook() { - LOGGER.log(Level.INFO, "Loaded multi-parent compile hook"); - } - - @Override - public void action(@NonNull BeforeCompilationContext context) throws PomExecutionException { - LOGGER.log(Level.INFO, "Executing multi-parent compile hook"); - PluginCompatTesterConfig config = context.getConfig(); - - runner = new ExternalMavenRunner(config.getExternalMaven(), config.getMavenSettings(), config.getMavenArgs()); - - File pluginDir = context.getPluginDir(); - LOGGER.log(Level.INFO, "Plugin dir is {0}", pluginDir); - - // We need to compile before generating effective pom overriding jenkins.version - // only if the plugin is not already compiled - if (!context.ranCompile()) { - compile(pluginDir, config.getLocalCheckoutDir(), context.getParentFolder(), context.getPlugin().name); - context.setRanCompile(true); - } - - LOGGER.log(Level.INFO, "Executed multi-parent compile hook"); - } - - @Override - public boolean check(@NonNull BeforeCompilationContext context) { - for (PluginCompatTesterHook hook : - PluginCompatTesterHooks.hooksByStage.get(Stage.CHECKOUT)) { - PluginCompatTesterHook checkoutHook = - (PluginCompatTesterHook) hook; - if (checkoutHook instanceof AbstractMultiParentHook - && checkoutHook.check(new BeforeCheckoutContext( - context.getPlugin(), context.getModel(), context.getCoreVersion(), context.getConfig()))) { - return true; - } - } - return false; - } - - private void compile(File path, File localCheckoutDir, String parentFolder, String pluginName) - throws PomExecutionException { - if (isSnapshotMultiParentPlugin(parentFolder, path, localCheckoutDir)) { - // "process-test-classes" not working properly on multi-module plugin. - // See https://issues.jenkins.io/browse/JENKINS-62658 - // installs dependencies into local repository - String mavenModule = PluginCompatTester.getMavenModule(pluginName, path, runner); - if (mavenModule == null || mavenModule.isBlank()) { - throw new IllegalStateException( - String.format("Unable to retrieve the Maven module for plugin %s on %s", pluginName, path)); - } - runner.run( - Map.of( - "skipTests", - "true", - "invoker.skip", - "true", - "enforcer.skip", - "true", - "maven.javadoc.skip", - "true"), - path.getParentFile(), - setupCompileResources(path.getParentFile()), - "clean", - "install", - "-am", - "-pl", - mavenModule); - } else { - runner.run( - Map.of("maven.javadoc.skip", "true"), - path, - setupCompileResources(path), - "clean", - "process-test-classes"); - } - } - - /** - * Checks if a plugin is a multiparent plugin with a SNAPSHOT project.version and without local - * checkout directory overriden. - */ - private boolean isSnapshotMultiParentPlugin(String parentFolder, File path, File localCheckoutDir) - throws PomExecutionException { - if (localCheckoutDir != null) { - return false; - } - if (parentFolder == null || parentFolder.isBlank()) { - return false; - } - if (!path.getAbsolutePath().contains(parentFolder)) { - LOGGER.log(Level.WARNING, "Parent folder {0} not present in path {1}", new Object[] { - parentFolder, path.getAbsolutePath() - }); - return false; - } - File parentFile = path.getParentFile(); - if (!StringUtils.equals(parentFolder, parentFile.getName())) { - LOGGER.log(Level.WARNING, "{0} is not the parent folder of {1}", new Object[] { - parentFolder, path.getAbsolutePath() - }); - return false; - } - - ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(parentFile, runner); - return expressionEvaluator.evaluateString("project.version").endsWith("-SNAPSHOT"); - } - - private File setupCompileResources(File path) { - LOGGER.log(Level.INFO, "Plugin compilation log directory: {0}", path); - return new File(path + "/compilePluginLog.log"); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/PipelineStageViewHook.java b/src/main/java/org/jenkins/tools/test/hook/PipelineStageViewHook.java deleted file mode 100644 index 658d23ff3..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/PipelineStageViewHook.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class PipelineStageViewHook extends AbstractMultiParentHook { - - private static final Set ARTIFACT_IDS = Set.of("pipeline-rest-api", "pipeline-stage-view"); - - @Override - protected String getParentFolder() { - return "pipeline-stage-view-plugin"; - } - - @Override - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return context.getPlugin().name.equals("pipeline-rest-api") ? "rest-api" : "ui"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "org.jenkins-ci.plugins.pipeline-stage-view".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/PropertyVersionHook.java b/src/main/java/org/jenkins/tools/test/hook/PropertyVersionHook.java index f7bf161c4..7e18c2514 100644 --- a/src/main/java/org/jenkins/tools/test/hook/PropertyVersionHook.java +++ b/src/main/java/org/jenkins/tools/test/hook/PropertyVersionHook.java @@ -2,7 +2,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.util.VersionNumber; -import java.io.File; import org.jenkins.tools.test.exception.PomExecutionException; import org.jenkins.tools.test.maven.ExpressionEvaluator; import org.jenkins.tools.test.maven.ExternalMavenRunner; @@ -31,17 +30,14 @@ public boolean check(@NonNull BeforeExecutionContext context) { PluginCompatTesterConfig config = context.getConfig(); MavenRunner runner = new ExternalMavenRunner(config.getExternalMaven(), config.getMavenSettings(), config.getMavenArgs()); - File pluginDir = context.getPluginDir(); - if (pluginDir != null) { - ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(pluginDir, runner); - try { - String version = expressionEvaluator.evaluateString(getProperty()); - return new VersionNumber(version).isOlderThan(new VersionNumber(getMinimumVersion())); - } catch (PomExecutionException e) { - return false; - } + ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator( + context.getCloneDirectory(), context.getPlugin().getModule(), runner); + try { + String version = expressionEvaluator.evaluateString(getProperty()); + return new VersionNumber(version).isOlderThan(new VersionNumber(getMinimumVersion())); + } catch (PomExecutionException e) { + return false; } - return false; } @Override diff --git a/src/main/java/org/jenkins/tools/test/hook/SwarmHook.java b/src/main/java/org/jenkins/tools/test/hook/SwarmHook.java deleted file mode 100644 index e51d06c7e..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/SwarmHook.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class SwarmHook extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "swarm-plugin"; - } - - @Override - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return "plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "org.jenkins-ci.plugins".equals(model.getGroupId()) - && "swarm".equals(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/TagValidationHook.java b/src/main/java/org/jenkins/tools/test/hook/TagValidationHook.java index f47b9f0ea..471f48aa4 100644 --- a/src/main/java/org/jenkins/tools/test/hook/TagValidationHook.java +++ b/src/main/java/org/jenkins/tools/test/hook/TagValidationHook.java @@ -1,7 +1,6 @@ package org.jenkins.tools.test.hook; import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; @@ -17,9 +16,10 @@ public boolean check(@NonNull BeforeCheckoutContext context) { @Override public void action(@NonNull BeforeCheckoutContext context) throws PluginSourcesUnavailableException { - Model model = context.getModel(); - if (model.getScm().getTag() == null || model.getScm().getTag().equals("HEAD")) { - throw new PluginSourcesUnavailableException("Failed to check out plugin sources for " + model.getVersion()); + String tag = context.getPlugin().getTag(); + if (tag == null || tag.equals("HEAD")) { + throw new PluginSourcesUnavailableException("Failed to check out plugin sources for " + + context.getPlugin().getPluginId()); } } } diff --git a/src/main/java/org/jenkins/tools/test/hook/WarningsNGCheckoutHook.java b/src/main/java/org/jenkins/tools/test/hook/WarningsNGCheckoutHook.java deleted file mode 100644 index c660069d3..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/WarningsNGCheckoutHook.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.File; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class WarningsNGCheckoutHook extends AbstractMultiParentHook { - - private static final Logger LOGGER = Logger.getLogger(WarningsNGCheckoutHook.class.getName()); - - private static final Set ARTIFACT_IDS = - Set.of(/* localCheckoutDir */ "warnings-ng-parent", /* checkout */ "warnings-ng"); - - @Override - protected String getParentFolder() { - return "warnings-ng-plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - return "io.jenkins.plugins".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); - } - - @Override - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return "plugin"; - } - - @Override - protected void configureLocalCheckOut(File localCheckoutDir, @NonNull BeforeCheckoutContext context) { - File pluginDir = new File(localCheckoutDir, getPluginFolderName(context)); - if (!pluginDir.exists() && !pluginDir.isDirectory()) { - throw new RuntimeException( - "Invalid localCheckoutDir for " + context.getPlugin().getDisplayName()); - } - - // Checkout already happened, don't run through again - context.setRanCheckout(true); - firstRun = false; - - // Change the "download"" directory; after download, it's simply used for reference - LOGGER.log(Level.INFO, "Child path for {0}: {1}", new Object[] { - context.getPlugin().getDisplayName(), pluginDir.getPath() - }); - context.setCheckoutDir(pluginDir); - context.setPluginDir(pluginDir); - } -} diff --git a/src/main/java/org/jenkins/tools/test/hook/WarningsNGExecutionHook.java b/src/main/java/org/jenkins/tools/test/hook/WarningsNGExecutionHook.java index 06c9d1322..47a9e1e94 100644 --- a/src/main/java/org/jenkins/tools/test/hook/WarningsNGExecutionHook.java +++ b/src/main/java/org/jenkins/tools/test/hook/WarningsNGExecutionHook.java @@ -2,7 +2,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Set; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeExecution; import org.kohsuke.MetaInfServices; @@ -16,9 +15,6 @@ public class WarningsNGExecutionHook extends PluginWithFailsafeIntegrationTestsH @Override public boolean check(@NonNull BeforeExecutionContext context) { - Model model = context.getModel(); - return "io.jenkins.plugins".equals(model.getGroupId()) - && ARTIFACT_IDS.contains(model.getArtifactId()) - && "hpi".equals(model.getPackaging()); + return ARTIFACT_IDS.contains(context.getPlugin().getPluginId()); } } diff --git a/src/main/java/org/jenkins/tools/test/hook/WorkflowCpsHook.java b/src/main/java/org/jenkins/tools/test/hook/WorkflowCpsHook.java deleted file mode 100644 index fa6f98b7b..000000000 --- a/src/main/java/org/jenkins/tools/test/hook/WorkflowCpsHook.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jenkins.tools.test.hook; - -import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.util.VersionNumber; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.hook.BeforeCheckoutContext; -import org.jenkins.tools.test.model.hook.PluginCompatTesterHookBeforeCheckout; -import org.kohsuke.MetaInfServices; - -@MetaInfServices(PluginCompatTesterHookBeforeCheckout.class) -public class WorkflowCpsHook extends AbstractMultiParentHook { - - @Override - protected String getParentFolder() { - return "workflow-cps-plugin"; - } - - @Override - protected String getPluginFolderName(@NonNull BeforeCheckoutContext context) { - return "plugin"; - } - - @Override - public boolean check(@NonNull BeforeCheckoutContext context) { - Model model = context.getModel(); - VersionNumber pluginVersion = new VersionNumber(context.getPlugin().version); - // 2803 was the final release before it became a multi-module project. - // The history of groovy-cps history was merged into the repo, so the first multi-module - // release will be a little over 3500. - VersionNumber multiModuleSince = new VersionNumber("3500"); - return "org.jenkins-ci.plugins.workflow".equals(model.getGroupId()) - && "workflow-cps".equals(model.getArtifactId()) - && pluginVersion.isNewerThan(multiModuleSince) - && "hpi".equals(model.getPackaging()); - } -} diff --git a/src/main/java/org/jenkins/tools/test/maven/ExpressionEvaluator.java b/src/main/java/org/jenkins/tools/test/maven/ExpressionEvaluator.java index ffaa83525..891d17979 100644 --- a/src/main/java/org/jenkins/tools/test/maven/ExpressionEvaluator.java +++ b/src/main/java/org/jenkins/tools/test/maven/ExpressionEvaluator.java @@ -1,5 +1,6 @@ package org.jenkins.tools.test.maven; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; import java.io.IOException; @@ -21,11 +22,15 @@ public class ExpressionEvaluator { @NonNull private final File pluginPath; + @CheckForNull + private final String module; + @NonNull private final MavenRunner runner; - public ExpressionEvaluator(File pluginPath, MavenRunner runner) { + public ExpressionEvaluator(File pluginPath, String module, MavenRunner runner) { this.pluginPath = pluginPath; + this.module = module; this.runner = runner; } @@ -34,6 +39,7 @@ public String evaluateString(String expression) throws PomExecutionException { runner.run( Map.of("expression", expression, "output", log.toAbsolutePath().toString()), pluginPath, + module, null, "-q", "help:evaluate"); @@ -52,6 +58,7 @@ public List evaluateList(String expression) throws PomExecutionException runner.run( Map.of("expression", expression, "output", log.toAbsolutePath().toString()), pluginPath, + module, null, "-q", "help:evaluate"); diff --git a/src/main/java/org/jenkins/tools/test/maven/ExternalMavenRunner.java b/src/main/java/org/jenkins/tools/test/maven/ExternalMavenRunner.java index 5102cf968..a551d4fd3 100644 --- a/src/main/java/org/jenkins/tools/test/maven/ExternalMavenRunner.java +++ b/src/main/java/org/jenkins/tools/test/maven/ExternalMavenRunner.java @@ -54,7 +54,8 @@ public ExternalMavenRunner( @Override @SuppressFBWarnings(value = "COMMAND_INJECTION", justification = "intended behavior") - public void run(Map properties, File baseDirectory, File buildLogFile, String... args) + public void run( + Map properties, File baseDirectory, String moduleName, File buildLogFile, String... args) throws PomExecutionException { List cmd = new ArrayList<>(); if (externalMaven != null) { @@ -70,6 +71,10 @@ public void run(Map properties, File baseDirectory, File buildLo cmd.add("-s"); cmd.add(mavenSettings.toString()); } + if (moduleName != null && !moduleName.isBlank()) { + cmd.add("-pl"); + cmd.add(moduleName); + } for (Map.Entry entry : properties.entrySet()) { cmd.add("-D" + entry); } diff --git a/src/main/java/org/jenkins/tools/test/maven/MavenRunner.java b/src/main/java/org/jenkins/tools/test/maven/MavenRunner.java index 24e0a36ae..8396c812a 100644 --- a/src/main/java/org/jenkins/tools/test/maven/MavenRunner.java +++ b/src/main/java/org/jenkins/tools/test/maven/MavenRunner.java @@ -1,11 +1,17 @@ package org.jenkins.tools.test.maven; +import edu.umd.cs.findbugs.annotations.CheckForNull; import java.io.File; import java.util.Map; import org.jenkins.tools.test.exception.PomExecutionException; public interface MavenRunner { - void run(Map properties, File baseDirectory, File buildLogFile, String... args) + void run( + Map properties, + File baseDirectory, + @CheckForNull String moduleName, + @CheckForNull File buildLogFile, + String... args) throws PomExecutionException; } diff --git a/src/main/java/org/jenkins/tools/test/model/PluginRemoting.java b/src/main/java/org/jenkins/tools/test/model/PluginRemoting.java deleted file mode 100644 index 6f7698539..000000000 --- a/src/main/java/org/jenkins/tools/test/model/PluginRemoting.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, - * Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, - * Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.jenkins.tools.test.model; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.UncheckedIOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import org.apache.maven.model.Model; -import org.apache.maven.model.Scm; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; -import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; - -/** - * Utility class providing business for retrieving plugin POM data - * - * @author Frederic Camblor - */ -public class PluginRemoting { - - private String hpiRemoteUrl; - private File pomFile; - - public PluginRemoting(String hpiRemoteUrl) { - this.hpiRemoteUrl = hpiRemoteUrl; - } - - public PluginRemoting(File pomFile) { - this.pomFile = pomFile; - } - - private String retrievePomContent() { - try { - if (hpiRemoteUrl != null) { - return retrievePomContentFromHpi(); - } else { - return retrievePomContentFromXmlFile(); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD", justification = "Only file: URLs are supported") - private String retrievePomContentFromHpi() throws IOException { - URL url = new URL(hpiRemoteUrl); - if (!url.getProtocol().equals("jar") || !url.getFile().startsWith("file:")) { - throw new MalformedURLException("Invalid URL: " + url); - } - try (InputStream is = url.openStream(); - ZipInputStream zis = new ZipInputStream(is)) { - ZipEntry ze; - while ((ze = zis.getNextEntry()) != null) { - if (ze.getName().startsWith("META-INF/maven/") && ze.getName().endsWith("/pom.xml")) { - return new String(zis.readAllBytes(), StandardCharsets.UTF_8); - } - } - } - throw new FileNotFoundException("Failed to retrieve POM content from HPI: " + hpiRemoteUrl); - } - - private String retrievePomContentFromXmlFile() throws IOException { - return Files.readString(pomFile.toPath(), StandardCharsets.UTF_8); - } - - public Model retrieveModel() throws PluginSourcesUnavailableException { - String pomContent = this.retrievePomContent(); - - Model model; - try (Reader r = new StringReader(pomContent)) { - MavenXpp3Reader mavenXpp3Reader = new MavenXpp3Reader(); - model = mavenXpp3Reader.read(r); - } catch (XmlPullParserException e) { - throw new PluginSourcesUnavailableException("Failed to parse pom.xml", e); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - Scm scm = model.getScm(); - if (scm != null) { - // scm may contain properties so it needs to be resolved. - scm.setConnection(interpolateString(scm.getConnection(), model.getArtifactId())); - } - - return model; - } - - /** - * Replaces any occurence of {@code "${project.artifactId}"} or {@code "${artifactId}"} with the - * supplied value of the artifactId/ - * - * @param original the original string - * @param artifactId the interpolated String - * @return the original string with any interpolation for the artifactId resolved. - */ - static String interpolateString(String original, String artifactId) { - return original.replace("${project.artifactId}", artifactId); - } -} diff --git a/src/main/java/org/jenkins/tools/test/model/UpdateSite.java b/src/main/java/org/jenkins/tools/test/model/UpdateSite.java deleted file mode 100644 index 8007a6be8..000000000 --- a/src/main/java/org/jenkins/tools/test/model/UpdateSite.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.jenkins.tools.test.model; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public final class UpdateSite { - - /** In-memory representation of the update center data. */ - public static final class Data { - /** The latest jenkins.war. */ - @NonNull - public final Entry core; - - /** Plugins in the repository, keyed by their artifact IDs. */ - @NonNull - public final Map plugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - - public Data(@NonNull Entry core, @NonNull List plugins) { - this.core = core; - for (Plugin plugin : plugins) { - this.plugins.put(plugin.name, plugin); - } - } - } - - public static class Entry { - /** Artifact ID. */ - @NonNull - public final String name; - - /** The version. */ - @NonNull - public final String version; - - /** Download URL. */ - @NonNull - public final String url; - - public Entry(@NonNull String name, @NonNull String version, @NonNull String url) { - this.name = name; - this.version = version; - this.url = url; - } - } - - public static class Plugin extends Entry { - /** Human readable title of the plugin. */ - @CheckForNull - public final String title; - - public Plugin(@NonNull String name, @NonNull String version, @NonNull String url, @CheckForNull String title) { - super(name, version, url); - this.title = title; - } - - public final String getDisplayName() { - return title != null ? title : name; - } - } -} diff --git a/src/main/java/org/jenkins/tools/test/model/hook/BeforeCheckoutContext.java b/src/main/java/org/jenkins/tools/test/model/hook/BeforeCheckoutContext.java index 2d7a661bf..d58085e32 100644 --- a/src/main/java/org/jenkins/tools/test/model/hook/BeforeCheckoutContext.java +++ b/src/main/java/org/jenkins/tools/test/model/hook/BeforeCheckoutContext.java @@ -1,65 +1,13 @@ package org.jenkins.tools.test.model.hook; -import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.File; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.UpdateSite; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; public final class BeforeCheckoutContext extends StageContext { - private boolean ranCheckout; - - @CheckForNull - private File checkoutDir; - - @CheckForNull - private File pluginDir; - - @CheckForNull - private String parentFolder; - public BeforeCheckoutContext( - @NonNull UpdateSite.Plugin plugin, - @NonNull Model model, - @NonNull String coreVersion, - @NonNull PluginCompatTesterConfig config) { - super(Stage.CHECKOUT, plugin, model, coreVersion, config); - } - - public boolean ranCheckout() { - return ranCheckout; - } - - public void setRanCheckout(boolean ranCheckout) { - this.ranCheckout = ranCheckout; - } - - @CheckForNull - public File getCheckoutDir() { - return checkoutDir; - } - - public void setCheckoutDir(@CheckForNull File checkoutDir) { - this.checkoutDir = checkoutDir; - } - - @CheckForNull - public File getPluginDir() { - return pluginDir; - } - - public void setPluginDir(@CheckForNull File pluginDir) { - this.pluginDir = pluginDir; - } - - @CheckForNull - public String getParentFolder() { - return parentFolder; - } - - public void setParentFolder(@CheckForNull String parentFolder) { - this.parentFolder = parentFolder; + @NonNull String coreVersion, @NonNull Plugin plugin, @NonNull PluginCompatTesterConfig config) { + super(Stage.CHECKOUT, coreVersion, plugin, config); } } diff --git a/src/main/java/org/jenkins/tools/test/model/hook/BeforeCompilationContext.java b/src/main/java/org/jenkins/tools/test/model/hook/BeforeCompilationContext.java index 36880c2c3..a1d58f8eb 100644 --- a/src/main/java/org/jenkins/tools/test/model/hook/BeforeCompilationContext.java +++ b/src/main/java/org/jenkins/tools/test/model/hook/BeforeCompilationContext.java @@ -3,47 +3,25 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.UpdateSite; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; public final class BeforeCompilationContext extends StageContext { @CheckForNull - private final File pluginDir; - - @CheckForNull - private final String parentFolder; - - private boolean ranCompile; + private final File cloneDirectory; public BeforeCompilationContext( - @NonNull UpdateSite.Plugin plugin, - @NonNull Model model, @NonNull String coreVersion, + @NonNull Plugin plugin, @NonNull PluginCompatTesterConfig config, - @CheckForNull File pluginDir, - @CheckForNull String parentFolder) { - super(Stage.COMPILATION, plugin, model, coreVersion, config); - this.pluginDir = pluginDir; - this.parentFolder = parentFolder; - } - - @CheckForNull - public File getPluginDir() { - return pluginDir; + @NonNull File cloneDirectory) { + super(Stage.COMPILATION, coreVersion, plugin, config); + this.cloneDirectory = cloneDirectory; } @CheckForNull - public String getParentFolder() { - return parentFolder; - } - - public boolean ranCompile() { - return ranCompile; - } - - public void setRanCompile(boolean ranCompile) { - this.ranCompile = ranCompile; + public File getCloneDirectory() { + return cloneDirectory; } } diff --git a/src/main/java/org/jenkins/tools/test/model/hook/BeforeExecutionContext.java b/src/main/java/org/jenkins/tools/test/model/hook/BeforeExecutionContext.java index 2f33c2b7b..625ff7a2d 100644 --- a/src/main/java/org/jenkins/tools/test/model/hook/BeforeExecutionContext.java +++ b/src/main/java/org/jenkins/tools/test/model/hook/BeforeExecutionContext.java @@ -1,61 +1,37 @@ package org.jenkins.tools.test.model.hook; -import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; import java.util.List; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.model.MavenPom; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.UpdateSite; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; public final class BeforeExecutionContext extends StageContext { - @CheckForNull - private final File pluginDir; - - @CheckForNull - private final String parentFolder; - @NonNull - private final List args; + private final File cloneDirectory; @NonNull - private final MavenPom pom; + private final List args; public BeforeExecutionContext( - @NonNull UpdateSite.Plugin plugin, - @NonNull Model model, @NonNull String coreVersion, + @NonNull Plugin plugin, @NonNull PluginCompatTesterConfig config, - @CheckForNull File pluginDir, - @CheckForNull String parentFolder, - @NonNull List args, - @NonNull MavenPom pom) { - super(Stage.EXECUTION, plugin, model, coreVersion, config); - this.pluginDir = pluginDir; - this.parentFolder = parentFolder; + @NonNull File cloneDirectory, + @NonNull List args) { + super(Stage.EXECUTION, coreVersion, plugin, config); + this.cloneDirectory = cloneDirectory; this.args = args; - this.pom = pom; - } - - @CheckForNull - public File getPluginDir() { - return pluginDir; } - @CheckForNull - public String getParentFolder() { - return parentFolder; + @NonNull + public File getCloneDirectory() { + return cloneDirectory; } @NonNull public List getArgs() { return args; } - - @NonNull - public MavenPom getPom() { - return pom; - } } diff --git a/src/main/java/org/jenkins/tools/test/model/hook/PluginCompatTesterHooks.java b/src/main/java/org/jenkins/tools/test/model/hook/PluginCompatTesterHooks.java index da849f3b4..c44496b48 100644 --- a/src/main/java/org/jenkins/tools/test/model/hook/PluginCompatTesterHooks.java +++ b/src/main/java/org/jenkins/tools/test/model/hook/PluginCompatTesterHooks.java @@ -1,20 +1,14 @@ package org.jenkins.tools.test.model.hook; import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.File; -import java.io.UncheckedIOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.ServiceLoader; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.jenkins.tools.test.exception.PluginCompatibilityTesterException; +import org.jenkins.tools.test.util.ServiceHelper; /** * Loads and executes hooks for modifying the state of Plugin Compatibility Tester at different @@ -27,39 +21,21 @@ public class PluginCompatTesterHooks { private static final Logger LOGGER = Logger.getLogger(PluginCompatTesterHooks.class.getName()); - private ClassLoader classLoader = PluginCompatTesterHooks.class.getClassLoader(); - - public static final Map>> hooksByStage = + private static final Map>> hooksByStage = new EnumMap<>(Stage.class); @NonNull private final Set excludeHooks; - public PluginCompatTesterHooks(@NonNull Set externalJars, @NonNull Set excludeHooks) { + public PluginCompatTesterHooks(@NonNull ServiceHelper serviceHelper, @NonNull Set excludeHooks) { this.excludeHooks = excludeHooks; - setupExternalClassLoaders(externalJars); - setupHooksByStage(); + setupHooksByStage(serviceHelper); } - private void setupHooksByStage() { - hooksByStage.put(Stage.CHECKOUT, findHooks(PluginCompatTesterHookBeforeCheckout.class)); - hooksByStage.put(Stage.COMPILATION, findHooks(PluginCompatTesterHookBeforeCompile.class)); - hooksByStage.put(Stage.EXECUTION, findHooks(PluginCompatTesterHookBeforeExecution.class)); - } - - private void setupExternalClassLoaders(Set externalJars) { - if (externalJars.isEmpty()) { - return; - } - List urls = new ArrayList<>(); - for (File jar : externalJars) { - try { - urls.add(jar.toURI().toURL()); - } catch (MalformedURLException e) { - throw new UncheckedIOException(e); - } - } - classLoader = new URLClassLoader(urls.toArray(new URL[0]), classLoader); + private void setupHooksByStage(@NonNull ServiceHelper serviceHelper) { + hooksByStage.put(Stage.CHECKOUT, serviceHelper.loadServices(PluginCompatTesterHookBeforeCheckout.class)); + hooksByStage.put(Stage.COMPILATION, serviceHelper.loadServices(PluginCompatTesterHookBeforeCompile.class)); + hooksByStage.put(Stage.EXECUTION, serviceHelper.loadServices(PluginCompatTesterHookBeforeExecution.class)); } public void runBeforeCheckout(@NonNull BeforeCheckoutContext context) throws PluginCompatibilityTesterException { @@ -81,8 +57,9 @@ public void runBeforeExecution(@NonNull BeforeExecutionContext context) throws P * * @param context relevant information to hooks at various stages. */ - private void runHooks(@NonNull StageContext context) throws PluginCompatibilityTesterException { - for (PluginCompatTesterHook hook : hooksByStage.get(context.getStage())) { + private void runHooks(@NonNull C context) throws PluginCompatibilityTesterException { + for (PluginCompatTesterHook hook : + (List>) hooksByStage.get(context.getStage())) { if (!excludeHooks.contains(hook.getClass().getName()) && hook.check(context)) { LOGGER.log(Level.INFO, "Running hook: {0}", hook.getClass().getName()); hook.action(context); @@ -91,14 +68,4 @@ private void runHooks(@NonNull StageContext context) throws PluginCompatibilityT } } } - - private List> findHooks( - Class> clazz) { - List> sortedHooks = new ArrayList<>(); - for (PluginCompatTesterHook hook : ServiceLoader.load(clazz, classLoader)) { - sortedHooks.add((PluginCompatTesterHook) hook); - } - sortedHooks.sort(new HookOrderComparator()); - return sortedHooks; - } } diff --git a/src/main/java/org/jenkins/tools/test/model/hook/StageContext.java b/src/main/java/org/jenkins/tools/test/model/hook/StageContext.java index 8356d849d..9163ed03c 100644 --- a/src/main/java/org/jenkins/tools/test/model/hook/StageContext.java +++ b/src/main/java/org/jenkins/tools/test/model/hook/StageContext.java @@ -1,9 +1,8 @@ package org.jenkins.tools.test.model.hook; import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.UpdateSite; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; public abstract class StageContext { @@ -11,26 +10,21 @@ public abstract class StageContext { private final Stage stage; @NonNull - private final UpdateSite.Plugin plugin; - - @NonNull - private final Model model; + private final String coreVersion; @NonNull - private final String coreVersion; + private final Plugin plugin; @NonNull private final PluginCompatTesterConfig config; public StageContext( @NonNull Stage stage, - @NonNull UpdateSite.Plugin plugin, - @NonNull Model model, @NonNull String coreVersion, + @NonNull Plugin plugin, @NonNull PluginCompatTesterConfig config) { this.stage = stage; this.plugin = plugin; - this.model = model; this.coreVersion = coreVersion; this.config = config; } @@ -41,18 +35,13 @@ public Stage getStage() { } @NonNull - public UpdateSite.Plugin getPlugin() { - return plugin; - } - - @NonNull - public Model getModel() { - return model; + public String getCoreVersion() { + return coreVersion; } @NonNull - public String getCoreVersion() { - return coreVersion; + public Plugin getPlugin() { + return plugin; } @NonNull diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyMultiModulePluginMetadataExtractor.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyMultiModulePluginMetadataExtractor.java new file mode 100644 index 000000000..b5537c5eb --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyMultiModulePluginMetadataExtractor.java @@ -0,0 +1,68 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import java.util.Map; +import java.util.Set; +import java.util.jar.Manifest; +import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.model.hook.HookOrder; +import org.kohsuke.MetaInfServices; + +// TODO delete once all non standard multi module plugins are using +// https://github.com/jenkinsci/maven-hpi-plugin/pull/436 +@MetaInfServices(PluginMetadataExtractor.class) +@HookOrder(order = -500) +public class LegacyMultiModulePluginMetadataExtractor implements PluginMetadataExtractor { + + private final Set GROUP_IDS = Set.of("io.jenkins.blueocean", "io.jenkins.plugins.mina-sshd-api"); + + private final Set STANDARD_PLUGIN_IDS = Set.of( + "declarative-pipeline-migration-assistant", + "declarative-pipeline-migration-assistant-api", + "pipeline-model-api", + "pipeline-model-definition", + "pipeline-model-extensions", + "pipeline-stage-tags-metadata"); + + private final Map NONSTANDARD_PLUGIN_IDS = Map.of( + "configuration-as-code", "plugin", + "pipeline-rest-api", "rest-api", + "pipeline-stage-view", "ui", + "swarm", "plugin", + "warnings-ng", "plugin", + "workflow-cps", "plugin"); + + @Override + public boolean isApplicable(String pluginId, Manifest manifest, Model model) { + if (model.getScm() == null) { + return false; + } + String groupId = manifest.getMainAttributes().getValue("Group-Id"); + if (GROUP_IDS.contains(groupId) || STANDARD_PLUGIN_IDS.contains(pluginId)) { + return true; + } else { + return NONSTANDARD_PLUGIN_IDS.containsKey(pluginId); + } + } + + @Override + public Plugin extractMetadata(String pluginId, Manifest manifest, Model model) throws MetadataExtractionException { + assert pluginId.equals(model.getArtifactId()); + String groupId = manifest.getMainAttributes().getValue("Group-Id"); + Plugin.Builder builder = new Plugin.Builder() + .withPluginId(model.getArtifactId()) + .withName(model.getName()) + .withScmConnection(model.getScm().getConnection()) + .withTag(model.getScm().getTag()) + // Not guaranteed to be a hash, but close enough for this legacy code path + .withGitHash(model.getScm().getTag()) + .withVersion(model.getVersion() == null ? model.getParent().getVersion() : model.getVersion()); + if (GROUP_IDS.contains(groupId) || STANDARD_PLUGIN_IDS.contains(pluginId)) { + return builder.withModule(pluginId).build(); + } else if (NONSTANDARD_PLUGIN_IDS.containsKey(pluginId)) { + return builder.withModule(NONSTANDARD_PLUGIN_IDS.get(pluginId)).build(); + } else { + throw new MetadataExtractionException("No metadata could be extracted for " + pluginId); + } + } +} diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyPluginMetadataExtractor.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyPluginMetadataExtractor.java new file mode 100644 index 000000000..a4d95e53f --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LegacyPluginMetadataExtractor.java @@ -0,0 +1,37 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import java.util.jar.Manifest; +import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.model.hook.HookOrder; +import org.kohsuke.MetaInfServices; + +@MetaInfServices(PluginMetadataExtractor.class) +@HookOrder(order = -1000) +public class LegacyPluginMetadataExtractor implements PluginMetadataExtractor { + + @Override + public boolean isApplicable(String pluginId, Manifest manifest, Model model) { + /* + * Any multi-module project must have been handled before now (either the modern hook or a specific hook for a + * legacy multi-module project). + */ + return model.getScm() != null; + } + + @Override + public Plugin extractMetadata(String pluginId, Manifest manifest, Model model) throws MetadataExtractionException { + assert pluginId.equals(model.getArtifactId()); + return new Plugin.Builder() + .withPluginId(model.getArtifactId()) + .withName(model.getName()) + .withScmConnection(model.getScm().getConnection()) + // Not guaranteed to be a hash, but close enough for this legacy code path + .withGitHash(model.getScm().getTag()) + .withTag(model.getScm().getTag()) + // Any multi-module projects have already been handled by now or require new hooks + .withModule(null) + .withVersion(model.getVersion()) + .build(); + } +} diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LocalCheckoutPluginMetadataExtractor.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LocalCheckoutPluginMetadataExtractor.java new file mode 100644 index 000000000..14c36a7f5 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/LocalCheckoutPluginMetadataExtractor.java @@ -0,0 +1,97 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.exception.PomExecutionException; +import org.jenkins.tools.test.maven.ExpressionEvaluator; +import org.jenkins.tools.test.maven.MavenRunner; +import org.jenkins.tools.test.model.PluginCompatTesterConfig; + +public class LocalCheckoutPluginMetadataExtractor { + + private static final Logger LOGGER = Logger.getLogger(LocalCheckoutPluginMetadataExtractor.class.getName()); + + @NonNull + private final File localCheckoutDir; + + @NonNull + private final PluginCompatTesterConfig config; + + @NonNull + private final MavenRunner runner; + + public LocalCheckoutPluginMetadataExtractor(@NonNull PluginCompatTesterConfig config, @NonNull MavenRunner runner) { + this.localCheckoutDir = getLocalCheckoutDir(config); + this.config = config; + this.runner = runner; + } + + @NonNull + private static File getLocalCheckoutDir(@NonNull PluginCompatTesterConfig config) { + File result = config.getLocalCheckoutDir(); + if (result == null) { + throw new AssertionError("Could never happen, but needed to silence SpotBugs"); + } + return result; + } + + public List extractMetadata() throws MetadataExtractionException, PomExecutionException { + List plugins = new ArrayList<>(); + List modules = new ArrayList<>(); + modules.add(null); // Root module + modules.addAll(getModules()); + for (String module : modules) { + Plugin plugin = getPlugin(module); + if (plugin == null) { + continue; + } + if (config.getExcludePlugins() != null && config.getExcludePlugins().contains(plugin.getPluginId())) { + LOGGER.log(Level.INFO, "Plugin {0} in excluded plugins; skipping", plugin.getPluginId()); + } else if (config.getIncludePlugins() != null + && !config.getIncludePlugins().isEmpty() + && !config.getIncludePlugins().contains(plugin.getPluginId())) { + LOGGER.log(Level.INFO, "Plugin {0} not in included plugins; skipping", plugin.getPluginId()); + } else { + plugins.add(plugin); + } + } + if (plugins.isEmpty()) { + throw new MetadataExtractionException("Found no plugins in " + localCheckoutDir); + } + plugins.sort(Comparator.comparing(Plugin::getPluginId)); + return List.copyOf(plugins); + } + + private List getModules() throws PomExecutionException { + ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(localCheckoutDir, null, runner); + return expressionEvaluator.evaluateList("project.modules"); + } + + @CheckForNull + private Plugin getPlugin(String module) throws PomExecutionException { + ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(localCheckoutDir, module, runner); + String packaging = expressionEvaluator.evaluateString("project.packaging"); + if ("hpi".equals(packaging)) { + String pluginId = expressionEvaluator.evaluateString("project.artifactId"); + String version = expressionEvaluator.evaluateString("project.version"); + return toPlugin(pluginId, version, localCheckoutDir, module); + } + return null; + } + + private static Plugin toPlugin(String pluginId, String version, File cloneDirectory, String module) { + Plugin.Builder builder = new Plugin.Builder(); + builder.withPluginId(pluginId); + builder.withVersion(version); + builder.withGitUrl(cloneDirectory.toURI().toString()); + builder.withModule(module); + return builder.build(); + } +} diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractor.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractor.java new file mode 100644 index 000000000..9c716b939 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractor.java @@ -0,0 +1,51 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.model.hook.HookOrder; +import org.kohsuke.MetaInfServices; + +/** + * Extractor that obtains all the information from the plugin's manifiest file. This requires the + * plugin to have been built with a version of {@code maven-hpi-plugin} with + * https://github.com/jenkinsci/maven-hpi-plugin/pull/436 + */ +@MetaInfServices(PluginMetadataExtractor.class) +@HookOrder(order = 1000) // just incase it ever needs to be overridden +public class ModernPluginMetadataExtractor implements PluginMetadataExtractor { + + // https://github.com/jenkinsci/maven-hpi-plugin/pull/436 + private static final Attributes.Name PLUGIN_GIT_HASH = new Attributes.Name("Plugin-GitHash"); + private static final Attributes.Name PLUGIN_SCM_CONNECTION = new Attributes.Name("Plugin-ScmConnection"); + private static final Attributes.Name PLUGIN_SCM_TAG = new Attributes.Name("Plugin-ScmTag"); + private static final Attributes.Name PLUGIN_MODULE = new Attributes.Name("Plugin-Module"); + private static final Attributes.Name PLUGIN_ID = new Attributes.Name("Short-Name"); + private static final Attributes.Name PLUGIN_NAME = new Attributes.Name("Long-Name"); + private static final Attributes.Name PLUGIN_VERSION = new Attributes.Name("Plugin-Version"); + + @Override + public boolean isApplicable(String pluginId, Manifest manifest, Model model) { + // We are new enough to be a modern plugin + return manifest.getMainAttributes().containsKey(PLUGIN_SCM_CONNECTION); + } + + @Override + public Plugin extractMetadata(String pluginId, Manifest manifest, Model model) throws MetadataExtractionException { + // All the information is stored in the plugin's manifest + Attributes mainAttributes = manifest.getMainAttributes(); + + assert pluginId.equals(mainAttributes.getValue(PLUGIN_ID)); + + return new Plugin.Builder() + .withPluginId(mainAttributes.getValue(PLUGIN_ID)) + .withName(mainAttributes.getValue(PLUGIN_NAME)) + .withScmConnection(mainAttributes.getValue(PLUGIN_SCM_CONNECTION)) + .withTag(mainAttributes.getValue(PLUGIN_SCM_TAG)) + .withGitHash(mainAttributes.getValue(PLUGIN_GIT_HASH)) + .withModule(mainAttributes.getValue(PLUGIN_MODULE)) + .withVersion(mainAttributes.getValue(PLUGIN_VERSION)) + .build(); + } +} diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/Plugin.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/Plugin.java new file mode 100644 index 000000000..3538d5b86 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/Plugin.java @@ -0,0 +1,196 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import org.jenkins.tools.test.exception.MetadataExtractionException; + +/** + * Metadata representing a specific plugin for testing. + */ +public class Plugin { + @NonNull + private final String pluginId; + + @NonNull + private final String version; + + @NonNull + private final String gitUrl; + + @CheckForNull + private final String tag; + + @CheckForNull + private final String module; + + @CheckForNull + private final String gitHash; + + @CheckForNull + private final String name; + + private Plugin(Builder builder) { + this.pluginId = Objects.requireNonNull(builder.pluginId, "pluginId may not be null"); + this.version = Objects.requireNonNull(builder.version, "version may not be null"); + this.gitUrl = Objects.requireNonNull(builder.gitUrl, "gitUrl may not be null"); + this.tag = builder.tag; + this.module = builder.module; + this.gitHash = builder.gitHash; + this.name = builder.name; + } + + /** + * The unique plugin ID for this plugin. + */ + @NonNull + public String getPluginId() { + return pluginId; + } + + /** + * The Git URL for the source repository that contains this plugin; may be file based for a local checkout. + */ + @NonNull + public String getGitUrl() { + return gitUrl; + } + + /** + * The version of the plugin. + */ + @NonNull + public String getVersion() { + return version; + } + + /** + * The Git tag for this plugin as reported by Maven; may be {@code null} if Maven is not aware of a tag or for a local checkout. + */ + @CheckForNull + public String getTag() { + return tag; + } + + /** + * The Git hash for this plugin; will be {@code null} for a local checkout. + */ + @CheckForNull + public String getGitHash() { + return gitHash; + } + + /** + * The module name for this plugin; will be {@code null} if the plugin is not part of a multi-module build. + */ + @CheckForNull + public String getModule() { + return module; + } + + /** + * The plugin name if known, otherwise the plugin ID. + */ + @NonNull + public String getName() { + return name == null ? pluginId : name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Plugin that = (Plugin) o; + return getPluginId().equals(that.getPluginId()) + && getVersion().equals(that.getVersion()) + && getGitUrl().equals(that.getGitUrl()) + && Objects.equals(getTag(), that.getTag()) + && Objects.equals(getModule(), that.getModule()) + && Objects.equals(getGitHash(), that.getGitHash()) + && Objects.equals(getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getPluginId(), getVersion(), getGitUrl(), getTag(), getModule(), getGitHash(), getName()); + } + + public static final class Builder { + private String pluginId; + private String version; + private String gitUrl; + private String tag; + private String module; + private String gitHash; + private String name; + + public Builder() {} + + public Builder(Plugin from) { + this.pluginId = from.pluginId; + this.version = from.version; + this.gitUrl = from.gitUrl; + this.tag = from.tag; + this.module = from.module; + this.gitHash = from.gitHash; + this.name = from.name; + } + + public Builder withPluginId(String pluginId) { + this.pluginId = pluginId; + return this; + } + + public Builder withVersion(String version) { + this.version = version; + return this; + } + + /** + * Convenience method that strips {@code scm:git:} from the URL and sets the Git URL. + * @param scmUrl the Maven model SCM URL + * @throws MetadataExtractionException If the underlying SCM is not a Git URL + * @see #withGitUrl(String) + */ + public Builder withScmConnection(String scmUrl) throws MetadataExtractionException { + if (scmUrl.startsWith("scm:git:")) { + return withGitUrl(scmUrl.substring(8)); + } + throw new MetadataExtractionException( + "SCM URL" + scmUrl + " is not supported, only Git SCM URLs are supported"); + } + + public Builder withGitUrl(String gitUrl) { + this.gitUrl = gitUrl; + return this; + } + + public Builder withTag(String tag) { + this.tag = tag; + return this; + } + + public Builder withModule(String module) { + this.module = module; + return this; + } + + public Builder withGitHash(String gitHash) { + this.gitHash = gitHash; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Plugin build() { + return new Plugin(this); + } + } +} diff --git a/src/main/java/org/jenkins/tools/test/model/plugin_metadata/PluginMetadataExtractor.java b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/PluginMetadataExtractor.java new file mode 100644 index 000000000..b16c609a5 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/model/plugin_metadata/PluginMetadataExtractor.java @@ -0,0 +1,25 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import java.util.jar.Manifest; +import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; + +/** + * @author jnord + */ +public interface PluginMetadataExtractor { + + /** + * Determine whether the extractor is applicable to the given plugin. + */ + boolean isApplicable(String pluginId, Manifest manifest, Model model); + + /** + * Obtain the metadata for a give plugin from a jar file. + * + * @param manifest the plugins' manifest. + * @param model the plugins' model (from the HPI). + * @return a fully populated {@link Plugin} for the given plugin. + */ + Plugin extractMetadata(String pluginId, Manifest manifest, Model model) throws MetadataExtractionException; +} diff --git a/src/main/java/org/jenkins/tools/test/util/ModelReader.java b/src/main/java/org/jenkins/tools/test/util/ModelReader.java new file mode 100644 index 000000000..1e6a957ee --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/util/ModelReader.java @@ -0,0 +1,67 @@ +package org.jenkins.tools.test.util; + +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import org.apache.maven.model.Model; +import org.apache.maven.model.Scm; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.jenkins.tools.test.exception.MetadataExtractionException; + +/** + * Utility methods to load a {@link Model} + */ +public class ModelReader { + + /** + * Load the model that is embedded inside the plugin in {@code + * META-INF/maven/${groupId}/${artifactId}/pom.xml} + * + * @param groupId the groupId of the plugin + * @param artifactId the artifactId of the plugin + * @param jarInputStream the input stream created from the plugin's JAR file. + * @return the Maven model for the plugin as read from the {@code META-INF} directory. + * @throws MetadataExtractionException if the entry could not be loaded or found. + * @throws IOException if there was an I/O related issue obtaining the model. + */ + public static Model getPluginModelFromHpi(String groupId, String artifactId, JarInputStream jarInputStream) + throws MetadataExtractionException, IOException { + Model model = null; + final String entryName = "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml"; + JarEntry jarEntry; + while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { + if (entryName.equals(jarEntry.getName())) { + try { + MavenXpp3Reader mavenXpp3Reader = new MavenXpp3Reader(); + model = mavenXpp3Reader.read(jarInputStream); + break; + } catch (XmlPullParserException e) { + throw new MetadataExtractionException("Failed to parse pom.xml", e); + } + } + } + if (model == null) { + throw new MetadataExtractionException(entryName + " was not found in the plugin HPI"); + } + + Scm scm = model.getScm(); + if (scm != null) { + // scm may contain properties, so it needs to be resolved. + scm.setConnection(interpolateString(scm.getConnection(), model.getArtifactId())); + } + return model; + } + + /** + * Replace any occurrence of {@code "${project.artifactId}"} or {@code "${artifactId}"} with the + * supplied value of the artifactId/ + * + * @param original the original string + * @param artifactId the interpolated String + * @return the original string with any interpolation for the artifactId resolved. + */ + static String interpolateString(String original, String artifactId) { + return original.replace("${project.artifactId}", artifactId); + } +} diff --git a/src/main/java/org/jenkins/tools/test/util/ServiceHelper.java b/src/main/java/org/jenkins/tools/test/util/ServiceHelper.java new file mode 100644 index 000000000..02e34134b --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/util/ServiceHelper.java @@ -0,0 +1,63 @@ +package org.jenkins.tools.test.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.File; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import org.jenkins.tools.test.model.hook.HookOrder; +import org.jenkins.tools.test.model.hook.HookOrderComparator; + +/** + * Helper utilities to aid working with {@link ServiceLoader services}. + */ +public class ServiceHelper { + + private final ClassLoader classLoader; + + /** + * Creates a new {@code ServiceHelper} instance that will load services from a {@link ClassLoader} constructed from + * the {@link ServiceHelper}'s classloader with the addition of classes in {@code externalJars}. + * + * @param externalJars a possibly empty set of extra files to add to the {@link ClassLoader} + */ + public ServiceHelper(@NonNull Set externalJars) { + classLoader = createExternalClassLoader(externalJars); + } + + /** + * Locate and loads any services of the specified type, ordering the returned list according to the services {@link HookOrder} annotation. + * @param the class of the service type + * @param cls the interface or abstract class representing the service + * @return an List of discovered services of type {@code T} sorted by {@link HookOrderComparator} + */ + public List loadServices(Class cls) { + ServiceLoader sl = ServiceLoader.load(cls, classLoader); + ArrayList serviceList = new ArrayList<>(); + for (T service : sl) { + serviceList.add(service); + } + serviceList.sort(new HookOrderComparator()); + return serviceList; + } + + private ClassLoader createExternalClassLoader(Set externalJars) { + if (externalJars.isEmpty()) { + ServiceHelper.class.getClassLoader(); + } + List urls = new ArrayList<>(); + for (File jar : externalJars) { + try { + urls.add(jar.toURI().toURL()); + } catch (MalformedURLException e) { + throw new UncheckedIOException("Failed to setup ClassLoader with external JAR", e); + } + } + return new URLClassLoader(urls.toArray(new URL[0]), ServiceHelper.class.getClassLoader()); + } +} diff --git a/src/main/java/org/jenkins/tools/test/util/WarExtractor.java b/src/main/java/org/jenkins/tools/test/util/WarExtractor.java new file mode 100644 index 000000000..e49847793 --- /dev/null +++ b/src/main/java/org/jenkins/tools/test/util/WarExtractor.java @@ -0,0 +1,157 @@ +package org.jenkins.tools.test.util; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.apache.maven.model.Model; +import org.jenkins.tools.test.exception.MetadataExtractionException; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; +import org.jenkins.tools.test.model.plugin_metadata.PluginMetadataExtractor; + +public class WarExtractor { + + private static final Logger LOGGER = Logger.getLogger(WarExtractor.class.getName()); + + private static final String PREFIX = "WEB-INF/plugins/"; + + private static final String SUFFIX = ".hpi"; + + @NonNull + private final File warFile; + + @NonNull + private final List extractors; + + @CheckForNull + private final Set includedPlugins; + + @CheckForNull + private final Set excludedPlugins; + + public WarExtractor( + File warFile, ServiceHelper serviceHelper, Set includedPlugins, Set excludedPlugins) { + this.warFile = warFile; + this.extractors = serviceHelper.loadServices(PluginMetadataExtractor.class); + this.includedPlugins = includedPlugins; + this.excludedPlugins = excludedPlugins; + } + + /** + * Extract the Jenkins core version from the given WAR. + * + * @return The Jenkins core version.. + */ + public String extractCoreVersion() throws MetadataExtractionException { + try (JarFile jf = new JarFile(warFile)) { + Manifest manifest = jf.getManifest(); + String value = manifest.getMainAttributes().getValue("Jenkins-Version"); + if (value == null) { + throw new MetadataExtractionException("Jenkins WAR is missing required Manifest entry"); + } + return value; + } catch (IOException e) { + throw new UncheckedIOException("Failed to extract Jenkins core version from " + warFile.toString(), e); + } + } + + /** + * Extract the list of plugins to be tested from the given WAR. + * + * @return An unmodifiable list of plugins to be tested, sorted by plugin ID. + */ + public List extractPlugins() throws MetadataExtractionException { + List plugins = new ArrayList<>(); + try (JarFile jf = new JarFile(warFile)) { + Enumeration entries = jf.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (isInteresting(entry)) { + plugins.add(getPlugin(jf, entry)); + } + } + } catch (IOException e) { + throw new UncheckedIOException("I/O error occurred whilst extracting plugin metadata from WAR", e); + } + plugins.sort(Comparator.comparing(Plugin::getPluginId)); + return List.copyOf(plugins); + } + + /** + * Predicate that will check if the given {@link JarEntry} is an interesting plugin. Detached + * plugins are ignored. If the plugin is excluded, it will be ignored. If the set of included + * plugins is not empty, the plugin will be ignored if it is not in the set of included plugins. + * + * @return {@code true} iff {@code entry} represents a plugin in {@code WEB-INF/plugins/} + */ + private boolean isInteresting(JarEntry entry) { + if (entry.getName().startsWith(PREFIX) && entry.getName().endsWith(SUFFIX)) { + String pluginId = + entry.getName().substring(PREFIX.length(), entry.getName().length() - SUFFIX.length()); + if (excludedPlugins != null && excludedPlugins.contains(pluginId)) { + LOGGER.log(Level.INFO, "Plugin {0} in excluded plugins; skipping", pluginId); + return false; + } + if (includedPlugins != null && !includedPlugins.isEmpty() && !includedPlugins.contains(pluginId)) { + LOGGER.log(Level.INFO, "Plugin {0} not in included plugins; skipping", pluginId); + return false; + } + return true; + } + return false; + } + + /** + * Obtain the plugin metadata from the given JAR entry. + * The given JAR entry must be a plugin; otherwise, the behaviour is undefined. + * + * @param entry The {@link JarEntry} representing the plugin. + * @return The plugin metadata. + */ + private Plugin getPlugin(JarFile jf, JarEntry entry) throws MetadataExtractionException { + // The entry is the HPI file + Manifest manifest; + Model model; + String pluginId; + try (JarInputStream jis = new JarInputStream(jf.getInputStream(entry))) { + manifest = jis.getManifest(); + String groupId = manifest.getMainAttributes().getValue("Group-Id"); + String artifactId = pluginId = manifest.getMainAttributes().getValue("Short-Name"); + model = ModelReader.getPluginModelFromHpi(groupId, artifactId, jis); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + // Once all plugins have adopted https://github.com/jenkinsci/maven-hpi-plugin/pull/436 this can be simplified + LOGGER.log(Level.INFO, "Extracting metadata for {0}", pluginId); + for (PluginMetadataExtractor extractor : extractors) { + if (extractor.isApplicable(pluginId, manifest, model)) { + return extractor.extractMetadata(pluginId, manifest, model); + } + } + throw new MetadataExtractionException("No metadata could be extracted for entry " + entry.getName()); + } + + /** + * Group the plugins by repository. + * + * @return A map of repositories to plugins, sorted by the plugin Git URL. + */ + public static NavigableMap> byRepository(List plugins) { + return plugins.stream().collect(Collectors.groupingBy(Plugin::getGitUrl, TreeMap::new, Collectors.toList())); + } +} diff --git a/src/test/java/org/jenkins/tools/test/PluginCompatTesterTest.java b/src/test/java/org/jenkins/tools/test/PluginCompatTesterTest.java index 214eb2b33..7177df7d5 100644 --- a/src/test/java/org/jenkins/tools/test/PluginCompatTesterTest.java +++ b/src/test/java/org/jenkins/tools/test/PluginCompatTesterTest.java @@ -26,27 +26,20 @@ package org.jenkins.tools.test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.jenkins.tools.test.model.PluginCompatTesterConfig; -import org.jenkins.tools.test.model.UpdateSite; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.jvnet.hudson.test.Issue; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -65,7 +58,7 @@ void smokes(@TempDir File tempDir) throws Exception { PluginCompatTester tester = new PluginCompatTester(config); tester.testPlugins(); Path report = tempDir.toPath() - .resolve("text-finder") + .resolve("text-finder-plugin") .resolve("target") .resolve("surefire-reports") .resolve("TEST-InjectedTest.xml"); @@ -87,141 +80,24 @@ void smokes(@TempDir File tempDir) throws Exception { } @Test - void updateSite() { - UpdateSite.Data data = PluginCompatTester.scanWAR( - new File("target", "megawar.war").getAbsoluteFile(), "WEB-INF/(?:optional-)?plugins/([^/.]+)[.][hj]pi"); - assertEquals("core", data.core.name); - assertNotNull(data.core.version); - assertEquals("https://foobar", data.core.url); - UpdateSite.Plugin plugin = data.plugins.get("text-finder"); - assertNotNull(plugin); - assertEquals("Text Finder", plugin.getDisplayName()); - assertEquals("Text Finder", plugin.title); - assertEquals("text-finder", plugin.name); - assertNotNull(plugin.version); - assertNotNull(plugin.url); - } - - @Test - void testMatcher() { - - String fileName = "WEB-INF/lib/jenkins-core-2.7.3-alpha-33.jar"; - Matcher m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-alpha-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-ALPHA-33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-ALPHA-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-ALPHA-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-ALPHA-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-alpha-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-alpha-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-beta-33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-beta-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-BETA-33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-BETA-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-BETA-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-BETA-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-BETA-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-BETA-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-rc-33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-rc-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-RC-33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-RC-33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-RC-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-RC-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-rc-33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-rc-33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-RC33.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-RC33", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-alpha33-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-alpha33-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-rc-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-rc-SNAPSHOT", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-milestone.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-milestone", m.group(1), "Invalid group"); - - fileName = "WEB-INF/lib/jenkins-core-2.7.3-rc-milestone.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - assertEquals("2.7.3-rc-milestone", m.group(1), "Invalid group"); - } - - @Test - @Issue("JENKINS-50454") - void testCustomWarPackagerVersions() { - // TODO: needs more filtering - String fileName = - "WEB-INF/lib/jenkins-core-256.0-my-branch-2090468d82e49345519a2457f1d1e7426f01540b-SNAPSHOT.jar"; - Matcher m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - - fileName = - "WEB-INF/lib/jenkins-core-256.0-2090468d82e49345519a2457f1d1e7426f01540b-2090468d82e49345519a2457f1d1e7426f01540b-SNAPSHOT.jar"; - m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertTrue(m.matches(), "No matches"); - } - - @Test - @Issue("340") - void testJEP229WithUnderscore() { - String fileName = "WEB-INF/lib/jenkins-core-2.329-rc31964.3b_29e9d46_038_.jar"; - Matcher m = Pattern.compile(PluginCompatTester.JENKINS_CORE_FILE_REGEX).matcher(fileName); - assertThat("No matches", m.matches(), is(true)); - assertThat("Invalid group", m.group(1), is("2.329-rc31964.3b_29e9d46_038_")); + void testDirectoryFromGitUrl() throws Exception { + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("ssh://git@github.com/jenkinsci/plugin-compat-tester.git")); + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("https://github.com/jenkinsci/plugin-compat-tester.git")); + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("git@host.xz:jenkinsci/plugin-compat-tester.git")); + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("ssh://git@github.com/jenkinsci/plugin-compat-tester")); + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("https://github.com/jenkinsci/plugin-compat-tester")); + assertEquals( + "plugin-compat-tester", + PluginCompatTester.getRepoNameFromGitUrl("git@host.xz:jenkinsci/plugin-compat-tester")); } } diff --git a/src/test/java/org/jenkins/tools/test/PluginListerCliTest.java b/src/test/java/org/jenkins/tools/test/PluginListerCliTest.java new file mode 100644 index 000000000..5b1eb7391 --- /dev/null +++ b/src/test/java/org/jenkins/tools/test/PluginListerCliTest.java @@ -0,0 +1,31 @@ +package org.jenkins.tools.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.io.FileMatchers.aReadableFile; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +class PluginListerCliTest { + + @Test + void testFileOutput(@TempDir File tempDir) throws IOException { + PluginListerCli app = new PluginListerCli(); + CommandLine cmd = new CommandLine(app); + File outputFile = new File(tempDir, "output.txt"); + int retVal = cmd.execute( + "--war", new File("target", "megawar.war").getAbsolutePath(), "--output", outputFile.getAbsolutePath()); + assertEquals(retVal, 0); + assertThat(outputFile, aReadableFile()); + List plugins = Files.readAllLines(outputFile.toPath(), StandardCharsets.UTF_8); + assertThat(plugins, is(List.of("text-finder"))); + } +} diff --git a/src/test/java/org/jenkins/tools/test/hook/JacocoHookTest.java b/src/test/java/org/jenkins/tools/test/hook/JacocoHookTest.java index 67dc2d928..e371c0e78 100644 --- a/src/test/java/org/jenkins/tools/test/hook/JacocoHookTest.java +++ b/src/test/java/org/jenkins/tools/test/hook/JacocoHookTest.java @@ -1,14 +1,14 @@ package org.jenkins.tools.test.hook; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; import org.junit.jupiter.api.Test; class JacocoHookTest { @@ -17,15 +17,18 @@ class JacocoHookTest { void testCheckMethod() { final JacocoHook hook = new JacocoHook(); - Model model = new Model(); - model.setGroupId("org.jenkins-ci.plugins"); - model.setArtifactId("jacoco"); - model.setPackaging("hpi"); - BeforeExecutionContext context = new BeforeExecutionContext(null, model, null, null, null, null, null, null); + Plugin plugin = new Plugin.Builder() + .withGitHash("ignored") + .withGitUrl("ignored") + .withVersion("ignored") + .withPluginId("jacoco") + .build(); + + BeforeExecutionContext context = new BeforeExecutionContext(null, plugin, null, null, null); assertTrue(hook.check(context)); - model.setArtifactId("other-plugin"); - context = new BeforeExecutionContext(null, model, null, null, null, null, null, null); + plugin = new Plugin.Builder(plugin).withPluginId("other-plugin").build(); + context = new BeforeExecutionContext(null, plugin, null, null, null); assertFalse(hook.check(context)); } @@ -34,21 +37,22 @@ void testAction() { final JacocoHook hook = new JacocoHook(); List args = new ArrayList<>(List.of("hpi:resolve-test-dependencies", "hpi:test-hpl", "surefire:test")); - BeforeExecutionContext context = new BeforeExecutionContext(null, null, null, null, null, null, args, null); + BeforeExecutionContext context = new BeforeExecutionContext(null, null, null, null, args); + hook.action(context); - assertThat(args.size(), is(4)); - assertThat(args.get(0), is("jacoco:prepare-agent")); + // order is importat + assertThat( + args, + contains("jacoco:prepare-agent", "hpi:resolve-test-dependencies", "hpi:test-hpl", "surefire:test")); args = new ArrayList<>(List.of("other-plugin:other-goal", "surefire:test")); - context = new BeforeExecutionContext(null, null, null, null, null, null, args, null); + context = new BeforeExecutionContext(null, null, null, null, args); hook.action(context); - assertThat(args.size(), is(3)); - assertThat(args.get(1), is("jacoco:prepare-agent")); + assertThat(args, contains("other-plugin:other-goal", "jacoco:prepare-agent", "surefire:test")); args = new ArrayList<>(List.of("element1", "element2", "element3", "element4")); - context = new BeforeExecutionContext(null, null, null, null, null, null, args, null); + context = new BeforeExecutionContext(null, null, null, null, args); hook.action(context); - assertThat(args.size(), is(4)); - assertFalse(args.contains("jacoco:prepare-agent")); + assertThat(args, contains("element1", "element2", "element3", "element4")); } } diff --git a/src/test/java/org/jenkins/tools/test/hook/WarningsNGExecutionHookTest.java b/src/test/java/org/jenkins/tools/test/hook/WarningsNGExecutionHookTest.java index 885c685ab..97330315d 100644 --- a/src/test/java/org/jenkins/tools/test/hook/WarningsNGExecutionHookTest.java +++ b/src/test/java/org/jenkins/tools/test/hook/WarningsNGExecutionHookTest.java @@ -1,14 +1,14 @@ package org.jenkins.tools.test.hook; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; -import org.apache.maven.model.Model; import org.jenkins.tools.test.model.hook.BeforeExecutionContext; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; import org.junit.jupiter.api.Test; class WarningsNGExecutionHookTest { @@ -17,17 +17,18 @@ class WarningsNGExecutionHookTest { void testCheckMethod() { final WarningsNGExecutionHook hook = new WarningsNGExecutionHook(); - Model model = new Model(); - model.setGroupId("io.jenkins.plugins"); - model.setArtifactId("warnings-ng"); - model.setPackaging("hpi"); + Plugin plugin = new Plugin.Builder() + .withGitHash("ignored") + .withGitUrl("ignored") + .withVersion("ignored") + .withPluginId("warnings-ng") + .build(); - BeforeExecutionContext context = - new BeforeExecutionContext(null, model, null, null, null, null, List.of(), null); + BeforeExecutionContext context = new BeforeExecutionContext(null, plugin, null, null, null); assertTrue(hook.check(context)); - model.setArtifactId("other-plugin"); - context = new BeforeExecutionContext(null, model, null, null, null, null, List.of(), null); + plugin = new Plugin.Builder(plugin).withPluginId("other-plugin").build(); + context = new BeforeExecutionContext(null, plugin, null, null, null); assertFalse(hook.check(context)); } @@ -36,9 +37,12 @@ void testAction() { final WarningsNGExecutionHook hook = new WarningsNGExecutionHook(); List args = new ArrayList<>(List.of("hpi:resolve-test-dependencies", "hpi:test-hpl", "surefire:test")); - BeforeExecutionContext context = new BeforeExecutionContext(null, null, null, null, null, null, args, null); + BeforeExecutionContext context = new BeforeExecutionContext(null, null, null, null, args); hook.action(context); - assertThat(args.size(), is(4)); - assertTrue(args.contains("failsafe:integration-test")); + // failsafe tests after surefire to match a general build. + assertThat( + args, + contains( + "hpi:resolve-test-dependencies", "hpi:test-hpl", "surefire:test", "failsafe:integration-test")); } } diff --git a/src/test/java/org/jenkins/tools/test/model/PluginRemotingTest.java b/src/test/java/org/jenkins/tools/test/model/PluginRemotingTest.java deleted file mode 100644 index 7068586eb..000000000 --- a/src/test/java/org/jenkins/tools/test/model/PluginRemotingTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.jenkins.tools.test.model; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.File; -import org.apache.maven.model.Model; -import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; -import org.junit.jupiter.api.Test; - -class PluginRemotingTest { - - @Test - void testStringInterpolation() { - assertThat(PluginRemoting.interpolateString("${project.artifactId}", "wibble"), is("wibble")); - assertThat(PluginRemoting.interpolateString("prefix-${project.artifactId}", "blah"), is("prefix-blah")); - assertThat(PluginRemoting.interpolateString("${project.artifactId}suffix", "something"), is("somethingsuffix")); - - // no interpolation - contains neither ${artifactId} not ${project.artifactId} - assertThat(PluginRemoting.interpolateString("${aartifactId}suffix", "something"), is("${aartifactId}suffix")); - assertThat( - PluginRemoting.interpolateString("${projectXartifactId}suffix", "something"), - is("${projectXartifactId}suffix")); - } - - @Test - void smokes() throws Exception { - File pomFile = new File(getClass().getResource("smokes/pom.xml").toURI()); - PluginRemoting pluginRemoting = new PluginRemoting(pomFile); - Model model = pluginRemoting.retrieveModel(); - assertThat(model.getParent(), nullValue()); - assertThat(model.getGroupId(), is("com.example.jenkins")); - assertThat(model.getArtifactId(), is("example")); - assertThat(model.getPackaging(), is("hpi")); - assertThat(model.getScm().getConnection(), is("scm:git:https://jenkins.example.com/example-plugin.git")); - assertThat(model.getScm().getTag(), is("example-4.1")); - } - - @Test - void parent() throws Exception { - File pomFile = new File(getClass().getResource("parent/pom.xml").toURI()); - PluginRemoting pluginRemoting = new PluginRemoting(pomFile); - Model model = pluginRemoting.retrieveModel(); - assertThat(model.getParent().getGroupId(), is("com.example.jenkins")); - assertThat(model.getParent().getArtifactId(), is("example-parent")); - assertThat(model.getParent().getVersion(), is("4.1")); - assertThat(model.getGroupId(), nullValue()); - assertThat(model.getArtifactId(), is("example")); - assertThat(model.getPackaging(), is("hpi")); - assertThat(model.getScm().getConnection(), is("scm:git:https://jenkins.example.com/example-plugin.git")); - assertThat(model.getScm().getTag(), is("example-4.1")); - } - - @Test - void negative() throws Exception { - File pomFile = new File(getClass().getResource("negative/pom.xml").toURI()); - PluginRemoting pluginRemoting = new PluginRemoting(pomFile); - PluginSourcesUnavailableException e = - assertThrows(PluginSourcesUnavailableException.class, pluginRemoting::retrieveModel); - assertThat(e.getMessage(), is("Failed to parse pom.xml")); - } -} diff --git a/src/test/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest.java b/src/test/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest.java new file mode 100644 index 000000000..63b13812f --- /dev/null +++ b/src/test/java/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest.java @@ -0,0 +1,52 @@ +package org.jenkins.tools.test.model.plugin_metadata; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStream; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; + +class ModernPluginMetadataExtractorTest { + + @Test + void extractModernMetadata() throws Exception { + // from https://github.com/jenkinsci/aws-java-sdk-plugin/pull/956/checks?check_run_id=12250637623 + try (InputStream resourceAsStream = ModernPluginMetadataExtractorTest.class.getResourceAsStream( + "ModernPluginMetadataExtractorTest/modern/MANIFEST.MF")) { + assertNotNull(resourceAsStream); + Manifest manifest = new Manifest(resourceAsStream); + ModernPluginMetadataExtractor modernPluginMetadataExtractor = new ModernPluginMetadataExtractor(); + assertTrue(modernPluginMetadataExtractor.isApplicable("aws-java-sdk-ec2", manifest, null)); + Plugin plugin = modernPluginMetadataExtractor.extractMetadata("aws-java-sdk-ec2", manifest, null); + assertNotNull(plugin, "metadata should be extracted from a modern manifest"); + assertThat( + plugin, + allOf( + hasProperty("pluginId", is("aws-java-sdk-ec2")), + hasProperty("gitUrl", is("https://github.com/jenkinsci/aws-java-sdk-plugin.git")), + hasProperty("module", is("aws-java-sdk-ec2")), + hasProperty("gitHash", is("938ad577f750694635f3c0160ac2110db5d6eb98")), + hasProperty("name", is("Amazon Web Services SDK :: EC2")), + hasProperty("version", startsWith("1.12.406-373.v59d2b_d41281b_")))); + } + } + + @Test + void extractLegacyMetadata() throws Exception { + // from https://updates.jenkins.io/download/plugins/text-finder/1.23/text-finder.hpi + try (InputStream resourceAsStream = ModernPluginMetadataExtractorTest.class.getResourceAsStream( + "ModernPluginMetadataExtractorTest/legacy/MANIFEST.MF")) { + assertNotNull(resourceAsStream); + Manifest manifest = new Manifest(resourceAsStream); + ModernPluginMetadataExtractor modernPluginMetadataExtractor = new ModernPluginMetadataExtractor(); + assertFalse(modernPluginMetadataExtractor.isApplicable("aws-java-sdk-ec2", manifest, null)); + } + } +} diff --git a/src/test/java/org/jenkins/tools/test/util/ServiceHelperTest.java b/src/test/java/org/jenkins/tools/test/util/ServiceHelperTest.java new file mode 100644 index 000000000..da36b671b --- /dev/null +++ b/src/test/java/org/jenkins/tools/test/util/ServiceHelperTest.java @@ -0,0 +1,54 @@ +package org.jenkins.tools.test.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import javax.annotation.processing.Processor; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestEngine; + +class ServiceHelperTest { + + @Test + void testServiceLocationWithoutExteneralJar() { + // Use a service that is not from our code as our services will not be found until we are packaged + ServiceHelper sh = new ServiceHelper(Collections.emptySet()); + List loadServices = sh.loadServices(TestEngine.class); + assertThat(loadServices, not(empty())); + } + + @Test + void testServiceLocationWithExternalJar() throws IOException { + // use javax.annotation.processing.Processor as the serive as the class needs to be in our classpath, but there + // needs to be some service implementation inside the megwar that is not on our classpath. + // anotation-indexer has an implemtation of this + File annotationIndexer = locateAnnotationIndexerJar(); + + List baseServices = new ServiceHelper(Collections.emptySet()).loadServices(Processor.class); + List extraServices = new ServiceHelper(Set.of(annotationIndexer)).loadServices(Processor.class); + + assertThat(extraServices.size(), greaterThan(baseServices.size())); + } + + private static File locateAnnotationIndexerJar() throws IOException { + // unpacked megawar directory + Path libDir = Path.of("target", "megawar", "WEB-INF", "lib"); + try (Stream pathStream = Files.list(libDir)) { + return pathStream + .filter(p -> p.getFileName().toString().startsWith("annotation-indexer-")) + .findFirst() + .orElseThrow() + .toFile(); + } + } +} diff --git a/src/test/java/org/jenkins/tools/test/util/WarExtractorTest.java b/src/test/java/org/jenkins/tools/test/util/WarExtractorTest.java new file mode 100644 index 000000000..6b2195e59 --- /dev/null +++ b/src/test/java/org/jenkins/tools/test/util/WarExtractorTest.java @@ -0,0 +1,44 @@ +package org.jenkins.tools.test.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +import java.io.File; +import java.util.List; +import java.util.Set; +import org.jenkins.tools.test.model.plugin_metadata.Plugin; +import org.junit.jupiter.api.Test; + +class WarExtractorTest { + + @Test + void testExtractCoreVersion() throws Exception { + WarExtractor warExtractor = + new WarExtractor(new File("target", "megawar.war"), new ServiceHelper(Set.of()), Set.of(), Set.of()); + String coreVersion = warExtractor.extractCoreVersion(); + assertThat(coreVersion, startsWith("2.")); + } + + @Test + void testExtractPlugins() throws Exception { + WarExtractor warExtractor = + new WarExtractor(new File("target", "megawar.war"), new ServiceHelper(Set.of()), Set.of(), Set.of()); + List plugins = warExtractor.extractPlugins(); + assertThat(plugins, hasSize(1)); + Plugin plugin = plugins.get(0); + assertThat( + plugin, + allOf( + hasProperty("pluginId", is("text-finder")), + hasProperty("gitUrl", is("https://github.com/jenkinsci/text-finder-plugin.git")), + hasProperty("module", nullValue()), // not a multi-module project + hasProperty("tag", startsWith("text-finder-1.")), + hasProperty("name", is("Text Finder")), + hasProperty("version", startsWith("1.")))); + } +} diff --git a/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/.gitattributes b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/.gitattributes new file mode 100644 index 000000000..5d88f9d02 --- /dev/null +++ b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/.gitattributes @@ -0,0 +1,2 @@ +# Manifest files are canonically CRLF +*.mf text eol=crlf \ No newline at end of file diff --git a/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/legacy/MANIFEST.MF b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/legacy/MANIFEST.MF new file mode 100644 index 000000000..079672657 --- /dev/null +++ b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/legacy/MANIFEST.MF @@ -0,0 +1,20 @@ +Manifest-Version: 1.0 +Created-By: Maven Archiver 3.6.0 +Build-Jdk-Spec: 11 +Specification-Title: Text Finder +Specification-Version: 1.23 +Implementation-Title: Text Finder +Implementation-Version: 1.23 +Group-Id: org.jenkins-ci.plugins +Short-Name: text-finder +Long-Name: Text Finder +Url: https://github.com/jenkinsci/text-finder-plugin +Plugin-Version: 1.23 +Hudson-Version: 2.361.4 +Jenkins-Version: 2.361.4 +Plugin-Developers: Kohsuke Kawaguchi:kohsuke:,Santiago Pericas-Geertsen: + :,Basil Crow:basil: +Plugin-License-Name: The MIT License (MIT) +Plugin-License-Url: http://opensource.org/licenses/MIT +Plugin-ScmUrl: https://github.com/jenkinsci/text-finder-plugin + diff --git a/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/modern/MANIFEST.MF b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/modern/MANIFEST.MF new file mode 100644 index 000000000..d4d82308c --- /dev/null +++ b/src/test/resources/org/jenkins/tools/test/model/plugin_metadata/ModernPluginMetadataExtractorTest/modern/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Created-By: Maven Archiver 3.6.0 +Build-Jdk-Spec: 11 +Specification-Title: Amazon Web Services SDK :: EC2 +Specification-Version: 1.12 +Implementation-Title: Amazon Web Services SDK :: EC2 +Implementation-Version: 1.12.406-373.v59d2b_d41281b_ +Group-Id: org.jenkins-ci.plugins.aws-java-sdk +Artifact-Id: aws-java-sdk-ec2 +Short-Name: aws-java-sdk-ec2 +Long-Name: Amazon Web Services SDK :: EC2 +Url: https://github.com/jenkinsci/aws-java-sdk-plugin +Plugin-Version: 1.12.406-373.v59d2b_d41281b_ +Hudson-Version: 2.361.4 +Jenkins-Version: 2.361.4 +Plugin-Dependencies: aws-java-sdk-minimal:1.12.406-373.v59d2b_d41281b_ +Plugin-Developers: Vincent Latombe:vlatombe:vincent@latombe.net +Support-Dynamic-Loading: true +Plugin-License-Name: MIT License +Plugin-License-Url: https://opensource.org/licenses/MIT +Plugin-ScmConnection: scm:git:https://github.com/jenkinsci/aws-java-sdk- + plugin.git +Plugin-ScmTag: 938ad577f750694635f3c0160ac2110db5d6eb98 +Plugin-ScmUrl: https://github.com/jenkinsci/aws-java-sdk-plugin/ +Plugin-GitHash: 938ad577f750694635f3c0160ac2110db5d6eb98 +Plugin-Module: aws-java-sdk-ec2 +