Skip to content

Commit

Permalink
Merge pull request #782 from jtnord/build-rjrinit-plugin-dynamically
Browse files Browse the repository at this point in the history
Dynamically create the RealJenkinsRuleInit plugin
  • Loading branch information
jtnord authored Jun 10, 2024
2 parents 8f49740 + defc0f7 commit 0622967
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 65 deletions.
62 changes: 0 additions & 62 deletions RealJenkinsRuleInit/pom.xml

This file was deleted.

1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ THE SOFTWARE.

<properties>
<changelist>999999-SNAPSHOT</changelist>
<!-- if bumping the jenkins.version the corresponding entry in RealJenkinsRuleInit/pom should also be updated -->
<jenkins.version>2.361</jenkins.version>
<jmh.version>1.37</jmh.version>
<gitHubRepo>jenkinsci/${project.artifactId}</gitHubRepo>
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/org/jvnet/hudson/test/PluginUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.jvnet.hudson.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

class PluginUtils {

/**
* Creates the plugin used by RealJenkinsRule
* @param destinationDirectory directory to write the plugin to.
* @param baseline the version of Jenkins to target
* @throws IOException if something goes wrong whilst creating the plugin.
* @return File the plugin we just created
*/
@SuppressFBWarnings(value="PATH_TRAVERSAL_IN", justification = "jth is a test utility, this is package scope code")
static File createRealJenkinsRulePlugin(File destinationDirectory, String baseline) throws IOException {
final String pluginName = RealJenkinsRuleInit.class.getSimpleName();

// The manifest is reused in the plugin and the classes jar.
Manifest mf = new Manifest();
Attributes mainAttributes = mf.getMainAttributes();
mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
mainAttributes.putValue("Plugin-Class", RealJenkinsRuleInit.class.getName());
mainAttributes.putValue("Extension-Name", pluginName);
mainAttributes.putValue("Short-Name", pluginName);
mainAttributes.putValue("Long-Name", "RealJenkinsRule initialization wrapper");
mainAttributes.putValue("Plugin-Version", "0-SNAPSHOT (private rjr)");
mainAttributes.putValue("Support-Dynamic-Loading", "true");
mainAttributes.putValue("Jenkins-Version", baseline);

// we need to create a jar for the classes which we can then put into the plugin.
Path tmpClassesJar = Files.createTempFile("rjr", "jar");
try {
try (FileOutputStream fos = new FileOutputStream(tmpClassesJar.toFile());
JarOutputStream classesJarOS = new JarOutputStream(fos, mf)) {
// the actual class
try (InputStream classIS = RealJenkinsRuleInit.class.getResourceAsStream(RealJenkinsRuleInit.class.getSimpleName() + ".class")) {
String path = RealJenkinsRuleInit.class.getPackageName().replace('.', '/');
createJarEntry(classesJarOS, path + '/' + RealJenkinsRuleInit.class.getSimpleName() + ".class", classIS);
}
}

// the actual JPI
File jpi = new File(destinationDirectory, pluginName+".jpi");
try (FileOutputStream fos = new FileOutputStream(jpi); JarOutputStream jos = new JarOutputStream(fos, mf)) {
try (FileInputStream fis = new FileInputStream(tmpClassesJar.toFile())) {
createJarEntry(jos, "WEB-INF/lib/" + pluginName + ".jar", fis);
}
}
return jpi;
} finally {
Files.delete(tmpClassesJar);
}
}

private static void createJarEntry(JarOutputStream jos, String entryName, InputStream data) throws IOException {
JarEntry je = new JarEntry(entryName);
jos.putNextEntry(je);
data.transferTo(jos);
jos.closeEntry();
}

}
8 changes: 7 additions & 1 deletion src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.logging.ConsoleHandler;
Expand Down Expand Up @@ -512,9 +513,14 @@ private void provision() throws Exception {
if (localData != null) {
new HudsonHomeLoader.Local(description.getTestClass().getMethod(description.getMethodName()), localData.value()).copy(getHome());
}

File plugins = new File(getHome(), "plugins");
Files.createDirectories(plugins.toPath());
FileUtils.copyURLToFile(RealJenkinsRule.class.getResource("RealJenkinsRuleInit.jpi"), new File(plugins, "RealJenkinsRuleInit.jpi"));
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");
PluginUtils.createRealJenkinsRulePlugin(plugins, targetJenkinsVersion);
}

if (includeTestClasspathPlugins) {
// Adapted from UnitTestSupportingPluginManager & JenkinsRule.recipeLoadCurrentPlugin:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,27 @@
import hudson.Plugin;
import java.net.URL;
import java.net.URLClassLoader;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import jenkins.model.Jenkins;

/**
* Plugin for use <b>internally only</b> by {@link RealJenkinsRule}, do not use this from plugin test code!
* <p>
* <strong>NOTE</strong>: this and only this class is added into a dynamically generated plugin, see {@link PluginUtils#createRealJenkinsRulePlugin(java.io.File, String)}.
* In order for this to occur correctly there need to be no inner classes or other code dependencies here (except what can be loaded by reflection).
*/
@Restricted(NoExternalUse.class)
public class RealJenkinsRuleInit extends Plugin {

@SuppressWarnings("deprecation") // @Initializer just gets run too late, even with before = InitMilestone.PLUGINS_PREPARED
public RealJenkinsRuleInit() {}

@Override
public void start() throws Exception {
new URLClassLoader(new URL[] {new URL(System.getProperty("RealJenkinsRule.location"))}, ClassLoader.getSystemClassLoader().getParent()).
new URLClassLoader("RealJenkinsRule",new URL[] {new URL(System.getProperty("RealJenkinsRule.location"))}, ClassLoader.getSystemClassLoader().getParent()).
loadClass("org.jvnet.hudson.test.RealJenkinsRule$Init2").
getMethod("run", Object.class).
invoke(null, Jenkins.get());
Expand Down
Binary file not shown.
26 changes: 26 additions & 0 deletions src/test/java/org/jvnet/hudson/test/PluginUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jvnet.hudson.test;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;

import org.hamcrest.Matchers;
import org.hamcrest.io.FileMatchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;

class PluginUtilsTest {

@Test
void createRealJenkinsRulePlugin(@TempDir File tmpDir) throws IOException {
File plugin = PluginUtils.createRealJenkinsRulePlugin(tmpDir, "3.6666");
assertThat(plugin, FileMatchers.anExistingFile());
try (JarFile hpi = new JarFile(plugin)) {
assertThat(hpi.getManifest().getMainAttributes(), hasEntry(hasToString("Jenkins-Version"), is("3.6666")));
}
}
}
15 changes: 15 additions & 0 deletions src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
Expand All @@ -43,6 +44,7 @@
import hudson.Functions;
import hudson.Launcher;
import hudson.Main;
import hudson.PluginWrapper;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleProject;
Expand All @@ -58,6 +60,7 @@
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import javax.servlet.Filter;
Expand Down Expand Up @@ -356,4 +359,16 @@ private static void hangs(JenkinsRule r) throws Throwable {
Thread.sleep(Long.MAX_VALUE);
}

@Test
public void noDetachedPlugins() throws Throwable {
// we should be the only plugin in Jenkins.
rr.then(RealJenkinsRuleTest::_noDetachedPlugins);
}

private static void _noDetachedPlugins(JenkinsRule r) throws Throwable {
// only RealJenkinsRuleInit should be present
List<PluginWrapper> plugins = r.jenkins.getPluginManager().getPlugins();
assertThat(plugins, hasSize(1));
assertThat(plugins.get(0).getShortName(), is("RealJenkinsRuleInit"));
}
}

0 comments on commit 0622967

Please sign in to comment.