diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index dca15ead9..3a5dd86ee 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -28,6 +28,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; import hudson.security.ACL; @@ -36,6 +37,7 @@ import hudson.util.NamingThreadFactory; import hudson.util.StreamCopyThread; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -54,6 +56,7 @@ import java.net.MalformedURLException; import java.net.SocketException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; @@ -88,8 +91,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -99,6 +105,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; @@ -160,7 +168,6 @@ *
Systems not yet tested: *
Committing that file to SCM (say, {@code src/test/resources/sample.jpi}) is - * reasonable for small fake plugins built for this purpose and exercising some bit of code. + *
For small fake plugins built for this purpose and exercising some bit of code, use {@link #addSyntheticPlugin}. * If you wish to test with larger archives of real plugins, this is possible for example by * binding {@code dependency:copy} to the {@code process-test-resources} phase. *
In most cases you do not need this method. Simply add whatever plugins you are @@ -271,6 +280,28 @@ public RealJenkinsRule addPlugins(String... plugins) { return this; } + /** + * Adds a test-only plugin to the controller based on sources defined in this module. + * Useful when you wish to define some types, register some {@link Extension}s, etc. + * and there is no existing plugin that does quite what you want + * (that you are comfortable adding to the test classpath and maintaining the version of). + *
If you also have some test suites based on {@link JenkinsRule},
+ * you may not want to use {@link Extension} since (unlike {@link TestExtension})
+ * it would be loaded in all such tests.
+ * Instead create a {@code package-info.java} specifying an {@code @OptionalPackage}
+ * whose {@code requirePlugins} lists the same {@link SyntheticPlugin#shortName(String)}.
+ * (You will need to {@code .header("Plugin-Dependencies", "variant:0")} to use this API.)
+ * Then use {@code @OptionalExtension} on all your test extensions.
+ * These will then be loaded only in {@link RealJenkinsRule}-based tests requesting this plugin.
+ * @param pkg the Java package containing any classes and resources you want included
+ * @return a builder
+ */
+ public SyntheticPlugin addSyntheticPlugin(Package pkg) {
+ SyntheticPlugin p = new SyntheticPlugin(pkg.getName());
+ syntheticPlugins.add(p);
+ return p;
+ }
+
/**
* Omit some plugins in the test classpath.
* @param plugins one or more code names, like {@code token-macro}
@@ -583,9 +614,10 @@ private void provision() throws Exception {
File plugins = new File(getHome(), "plugins");
Files.createDirectories(plugins.toPath());
+ // set the version to the version of jenkins used for testing to avoid dragging in detached plugins
+ String targetJenkinsVersion;
try (JarFile jf = new JarFile(war)) {
- // set the version to the version of jenkins used for testing to avoid dragging in detached plugins
- String targetJenkinsVersion = jf.getManifest().getMainAttributes().getValue("Jenkins-Version");
+ targetJenkinsVersion = jf.getManifest().getMainAttributes().getValue("Jenkins-Version");
PluginUtils.createRealJenkinsRulePlugin(plugins, targetJenkinsVersion);
}
@@ -671,6 +703,9 @@ private void provision() throws Exception {
}
FileUtils.copyURLToFile(url, new File(plugins, name + ".jpi"));
}
+ for (SyntheticPlugin syntheticPlugin : syntheticPlugins) {
+ syntheticPlugin.writeTo(new File(plugins, syntheticPlugin.shortName + ".jpi"), targetJenkinsVersion);
+ }
System.out.println("Will load plugins: " + Stream.of(plugins.list()).filter(n -> n.matches(".+[.][hj]p[il]")).sorted().collect(Collectors.joining(" ")));
}
@@ -1617,4 +1652,122 @@ private static class OutputPayload implements Serializable {
assumptionFailure = error instanceof AssumptionViolatedException ? error.getMessage() : null;
}
}
+
+ /**
+ * Alternative to {@link #addPlugins} or {@link TestExtension} that lets you build a test-only plugin on the fly.
+ * ({@link ExtensionList#add(Object)} can also be used for certain cases, but not if you need to define new types.)
+ */
+ public final class SyntheticPlugin {
+ private final String pkg;
+ private String shortName;
+ private String version = "1-SNAPSHOT";
+ private Map
+ *
+ */
+ public SyntheticPlugin header(String key, String value) {
+ headers.put(key, value);
+ return this;
+ }
+
+ /**
+ * @return back to the rule builder
+ */
+ public RealJenkinsRule done() {
+ return RealJenkinsRule.this;
+ }
+
+ void writeTo(File jpi, String defaultJenkinsVersion) throws IOException, URISyntaxException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+ String pkgSlash = pkg.replace('.', '/');
+ URL mainU = RealJenkinsRule.class.getClassLoader().getResource(pkgSlash);
+ if (mainU == null) {
+ throw new IOException("Cannot find " + pkgSlash + " in classpath");
+ }
+ Path main = Path.of(mainU.toURI());
+ if (!Files.isDirectory(main)) {
+ throw new IOException(main + " does not exist");
+ }
+ Path metaInf = Path.of(URI.create(mainU.toString().replaceFirst("\\Q" + pkgSlash + "\\E/?$", "META-INF")));
+ if (Files.isDirectory(metaInf)) {
+ zip(zos, metaInf, "META-INF/", pkg);
+ }
+ zip(zos, main, pkgSlash + "/", null);
+ }
+ Manifest mani = new Manifest();
+ Attributes attr = mani.getMainAttributes();
+ attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ attr.putValue("Short-Name", shortName);
+ attr.putValue("Plugin-Version", version);
+ attr.putValue("Jenkins-Version", defaultJenkinsVersion);
+ for (Map.Entry