diff --git a/plugin/pom.xml b/plugin/pom.xml index 12bf849e..92f5bd20 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -49,8 +49,47 @@ + + org.jenkins-ci.tools + maven-hpi-plugin + 3.3 + + ../client/target + + + + 2.13 + + + + + org.jenkins-ci + symbol-annotation + 1.5 + test + + + org.jenkins-ci.plugins + scm-api + 1.3 + test + + + org.jenkins-ci.plugins + structs + 1.5 + test + + + org.jenkins-ci.plugins + script-security + 1.24 + test + + + com.google.code.findbugs @@ -64,5 +103,54 @@ 3.0.1 provided + + org.jenkins-ci.plugins.workflow + workflow-api + 2.12 + test + + + org.jenkins-ci.plugins.workflow + workflow-basic-steps + 2.3 + test + + + org.jenkins-ci.plugins.workflow + workflow-cps + 2.28 + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + 2.4 + test + + + org.jenkins-ci.plugins.workflow + workflow-job + 2.9 + test + + + org.jenkins-ci.plugins.workflow + workflow-step-api + 2.7 + test + + + org.jenkins-ci.plugins.workflow + workflow-support + ${workflow-support-plugin.version} + test + + + org.jenkins-ci.plugins.workflow + workflow-support + ${workflow-support-plugin.version} + tests + test + diff --git a/plugin/src/test/java/hudson/plugins/swarm/PipelineJobTest.java b/plugin/src/test/java/hudson/plugins/swarm/PipelineJobTest.java new file mode 100644 index 00000000..6577d550 --- /dev/null +++ b/plugin/src/test/java/hudson/plugins/swarm/PipelineJobTest.java @@ -0,0 +1,205 @@ +package hudson.plugins.swarm; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import hudson.Functions; +import hudson.model.Computer; +import hudson.model.Node; +import hudson.plugins.swarm.test.ProcessDestroyer; +import hudson.plugins.swarm.test.TestUtils; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.Assume; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class PipelineJobTest { + + @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + + @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final ProcessDestroyer processDestroyer = new ProcessDestroyer(); + + /** Executes a shell script build on a Swarm Client agent. */ + @Test + public void buildShellScript() throws Exception { + story.then( + s -> { + Node node = + TestUtils.createSwarmClient(story.j, processDestroyer, temporaryFolder); + + WorkflowJob project = story.j.createProject(WorkflowJob.class); + project.setConcurrentBuild(false); + project.setDefinition(new CpsFlowDefinition(getFlow(node, 0), true)); + + WorkflowRun build = story.j.buildAndAssertSuccess(project); + story.j.assertLogContains("ON_SWARM_CLIENT=true", build); + tearDown(); + }); + } + + /** + * Executes a shell script build on a Swarm Client agent that has disconnected while the Jenkins + * master is still running. + */ + @Test + public void buildShellScriptAfterDisconnect() throws Exception { + story.then( + s -> { + Node node = + TestUtils.createSwarmClient(story.j, processDestroyer, temporaryFolder); + + WorkflowJob project = story.j.createProject(WorkflowJob.class); + project.setConcurrentBuild(false); + project.setDefinition(new CpsFlowDefinition(getFlow(node, 1), true)); + + WorkflowRun build = project.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait-0/1", build); + tearDown(); + + TestUtils.createSwarmClient( + node.getNodeName(), story.j, processDestroyer, temporaryFolder); + SemaphoreStep.success("wait-0/1", null); + story.j.assertBuildStatusSuccess(story.j.waitForCompletion(build)); + story.j.assertLogContains("ON_SWARM_CLIENT=true", build); + tearDown(); + }); + } + + /** Same as the preceding test, but waits in "sh" rather than "node." */ + @Test + public void buildShellScriptAcrossDisconnect() throws Exception { + Assume.assumeFalse( + "TODO not sure how to write a corresponding batch script", Functions.isWindows()); + story.then( + s -> { + Node node = + TestUtils.createSwarmClient(story.j, processDestroyer, temporaryFolder); + + WorkflowJob project = story.j.createProject(WorkflowJob.class); + File f1 = new File(story.j.jenkins.getRootDir(), "f1"); + File f2 = new File(story.j.jenkins.getRootDir(), "f2"); + new FileOutputStream(f1).close(); + project.setConcurrentBuild(false); + + StringBuilder sb = new StringBuilder(); + sb.append("node('" + node.getNodeName() + "') {\n"); + sb.append( + " sh 'touch \"" + + f2 + + "\"; while [ -f \"" + + f1 + + "\" ]; do sleep 1; done; echo finished waiting; rm \"" + + f2 + + "\"'\n"); + sb.append(" echo 'OK, done'\n"); + sb.append("}\n"); + project.setDefinition(new CpsFlowDefinition(sb.toString(), true)); + + WorkflowRun build = project.scheduleBuild2(0).waitForStart(); + while (!f2.isFile()) { + Thread.sleep(100); + } + assertTrue(build.isBuilding()); + Computer computer = node.toComputer(); + assertNotNull(computer); + tearDown(); + while (computer.isOnline()) { + Thread.sleep(100); + } + + TestUtils.createSwarmClient( + node.getNodeName(), story.j, processDestroyer, temporaryFolder); + while (computer.isOffline()) { + Thread.sleep(100); + } + assertTrue(f2.isFile()); + assertTrue(f1.delete()); + while (f2.isFile()) { + Thread.sleep(100); + } + + story.j.assertBuildStatusSuccess(story.j.waitForCompletion(build)); + story.j.assertLogContains("finished waiting", build); + story.j.assertLogContains("OK, done", build); + tearDown(); + }); + } + + /** + * Starts a Jenkins job on a Swarm Client agent, restarts Jenkins while the job is running, and + * verifies that the job continues running on the same agent after Jenkins has been restarted. + */ + @Test + public void buildShellScriptAfterRestart() throws Exception { + Assume.assumeNotNull( + System.getProperty("port"), "This test requires a fixed port to be available."); + + story.then( + s -> { + // "-deleteExistingClients" is needed so that the Swarm Client can connect + // after the restart. + Node node = + TestUtils.createSwarmClient( + story.j, + processDestroyer, + temporaryFolder, + "-deleteExistingClients"); + + WorkflowJob project = story.j.createProject(WorkflowJob.class, "test"); + project.setConcurrentBuild(false); + project.setDefinition(new CpsFlowDefinition(getFlow(node, 1), true)); + + WorkflowRun build = project.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait-0/1", build); + }); + story.then( + s -> { + SemaphoreStep.success("wait-0/1", null); + WorkflowJob project = + story.j.jenkins.getItemByFullName("test", WorkflowJob.class); + assertNotNull(project); + WorkflowRun build = project.getBuildByNumber(1); + story.j.assertBuildStatusSuccess(story.j.waitForCompletion(build)); + story.j.assertLogContains("ON_SWARM_CLIENT=true", build); + tearDown(); + }); + } + + private static String getFlow(Node node, int numSemaphores) { + StringBuilder sb = new StringBuilder(); + sb.append("node('" + node.getNodeName() + "') {\n"); + for (int i = 0; i < numSemaphores; i++) { + sb.append(" semaphore 'wait-" + i + "'\n"); + } + // TODO: Once JENKINS-41854 is fixed, remove the next two lines. + sb.append("}\n"); + sb.append("node('" + node.getNodeName() + "') {\n"); + sb.append( + " isUnix() ? sh('echo ON_SWARM_CLIENT=$ON_SWARM_CLIENT') : bat('echo ON_SWARM_CLIENT=%ON_SWARM_CLIENT%')"); + sb.append("}\n"); + + return sb.toString(); + } + + private void tearDown() throws IOException { + try { + processDestroyer.clean(); + } catch (InterruptedException e) { + e.printStackTrace(System.err); + } + } +} diff --git a/plugin/src/test/java/hudson/plugins/swarm/SwarmClientIntegrationTest.java b/plugin/src/test/java/hudson/plugins/swarm/SwarmClientIntegrationTest.java new file mode 100644 index 00000000..9b89e0b8 --- /dev/null +++ b/plugin/src/test/java/hudson/plugins/swarm/SwarmClientIntegrationTest.java @@ -0,0 +1,56 @@ +package hudson.plugins.swarm; + +import hudson.Functions; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Node; +import hudson.plugins.swarm.test.ProcessDestroyer; +import hudson.plugins.swarm.test.TestUtils; +import hudson.tasks.BatchFile; +import hudson.tasks.CommandInterpreter; +import hudson.tasks.Shell; +import java.io.IOException; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsRule; + +public class SwarmClientIntegrationTest { + + @Rule public JenkinsRule j = new JenkinsRule(); + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + + @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final ProcessDestroyer processDestroyer = new ProcessDestroyer(); + + /** Executes a shell script build on a Swarm Client agent. */ + @Test + public void buildShellScript() throws Exception { + Node node = TestUtils.createSwarmClient(j, processDestroyer, temporaryFolder); + + FreeStyleProject project = j.createFreeStyleProject(); + project.setConcurrentBuild(false); + project.setAssignedNode(node); + CommandInterpreter command = + Functions.isWindows() + ? new BatchFile("echo ON_SWARM_CLIENT=%ON_SWARM_CLIENT%") + : new Shell("echo ON_SWARM_CLIENT=$ON_SWARM_CLIENT"); + project.getBuildersList().add(command); + + FreeStyleBuild build = j.buildAndAssertSuccess(project); + j.assertLogContains("ON_SWARM_CLIENT=true", build); + tearDown(); + } + + private void tearDown() throws IOException { + try { + processDestroyer.clean(); + } catch (InterruptedException e) { + e.printStackTrace(System.err); + } + } +} diff --git a/plugin/src/test/java/hudson/plugins/swarm/test/ProcessDestroyer.java b/plugin/src/test/java/hudson/plugins/swarm/test/ProcessDestroyer.java new file mode 100644 index 00000000..c69ec298 --- /dev/null +++ b/plugin/src/test/java/hudson/plugins/swarm/test/ProcessDestroyer.java @@ -0,0 +1,27 @@ +package hudson.plugins.swarm.test; + +import java.util.HashSet; +import java.util.Set; + +/** + * Records processes started during Swarm Client integration tests so that they can be destroyed at + * the end of each test method. + */ +public final class ProcessDestroyer { + + private final Set processes = new HashSet<>(); + + /** Record a spawned process for later cleanup. */ + public void record(Process process) { + processes.add(process); + } + + /** Clean up all processes that were spawned during the test. */ + public void clean() throws InterruptedException { + for (Process process : processes) { + process.destroy(); + process.waitFor(); + } + processes.clear(); + } +} diff --git a/plugin/src/test/java/hudson/plugins/swarm/test/TestUtils.java b/plugin/src/test/java/hudson/plugins/swarm/test/TestUtils.java new file mode 100644 index 00000000..608f024f --- /dev/null +++ b/plugin/src/test/java/hudson/plugins/swarm/test/TestUtils.java @@ -0,0 +1,108 @@ +package hudson.plugins.swarm.test; + +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import hudson.model.Computer; +import hudson.model.Node; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; + +/** Utilities for testing the Swarm Plugin */ +public class TestUtils { + + /** Download the Swarm Client from the given Jenkins URL into the given temporary directory. */ + private static File download(URL jenkinsUrl, File output) throws Exception { + URL input = + jenkinsUrl.toURI().resolve(new URI(null, "swarm/swarm-client.jar", null)).toURL(); + FileUtils.copyURLToFile(input, output); + + assertTrue(output.isFile()); + assertThat(output.length(), greaterThan(1000L)); + return output; + } + + /** Wait for the agent with the given name to come online against the given Jenkins instance. */ + private static Computer waitOnline(String agentName, Jenkins jenkins) + throws InterruptedException, IOException { + Computer computer = jenkins.getComputer(agentName); + while (computer == null) { + Thread.sleep(100); + computer = jenkins.getComputer(agentName); + } + + while (!computer.isOnline()) { + Thread.sleep(100); + } + + return computer; + } + + /** + * Create a new Swarm Client agent on the local host and wait for it to come online before + * returning. + */ + public static Node createSwarmClient( + JenkinsRule j, + ProcessDestroyer processDestroyer, + TemporaryFolder temporaryFolder, + String... args) + throws Exception { + String agentName = "agent" + j.jenkins.getNodes().size(); + return createSwarmClient(agentName, j, processDestroyer, temporaryFolder, args); + } + + /** + * Create a new Swarm Client agent on the local host and wait for it to come online before + * returning. + */ + public static Node createSwarmClient( + String agentName, + JenkinsRule j, + ProcessDestroyer processDestroyer, + TemporaryFolder temporaryFolder, + String... args) + throws Exception { + File swarmClientJar = download(j.getURL(), temporaryFolder.newFile("swarm-client.jar")); + + List command = new ArrayList<>(); + command.add( + System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"); + command.add("-Djava.awt.headless=true"); + command.add("-jar"); + command.add(swarmClientJar.toString()); + command.add("-name"); + command.add(agentName); + command.add("-master"); + command.add(j.getURL().toString()); + // "-disableClientsUniqueId" is needed so that we can call getComputer() in the waitOnline() + // method. + command.add("-disableClientsUniqueId"); + Collections.addAll(command, args); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.environment().put("ON_SWARM_CLIENT", "true"); + pb.redirectOutput(temporaryFolder.newFile("stdout.log")); + pb.redirectError(temporaryFolder.newFile("stderr.log")); + Process process = pb.start(); + processDestroyer.record(process); + + Computer computer = waitOnline(agentName, j.jenkins); + assertNotNull(computer); + assertTrue(computer.isOnline()); + Node node = computer.getNode(); + assertNotNull(node); + return node; + } +}