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 extends StageContext> 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 extends PluginCompatTesterHook>) 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 extends PluginCompatTesterHook extends StageContext>> clazz) {
- List> sortedHooks = new ArrayList<>();
- for (PluginCompatTesterHook extends StageContext> 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
+