Skip to content

Commit

Permalink
Add integration tests for both Freestyle and Pipeline jobs (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
basil authored May 21, 2019
1 parent 5ac84ed commit c0e44ac
Show file tree
Hide file tree
Showing 5 changed files with 487 additions and 0 deletions.
88 changes: 88 additions & 0 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,47 @@
</artifactItems>
</configuration>
</plugin>
<plugin>
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>maven-hpi-plugin</artifactId>
<version>3.5</version>
<configuration>
<warSourceDirectory>../client/target</warSourceDirectory>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<workflow-support-plugin.version>2.13</workflow-support-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>symbol-annotation</artifactId>
<version>1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
<version>1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.24</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.code.findbugs</groupId>
Expand All @@ -68,5 +107,54 @@
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<version>2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<version>2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<version>2.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>${workflow-support-plugin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>${workflow-support-plugin.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
</dependencies>
</project>
205 changes: 205 additions & 0 deletions plugin/src/test/java/hudson/plugins/swarm/PipelineJobTest.java
Original file line number Diff line number Diff line change
@@ -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 {

This comment has been minimized.

Copy link
@darxriggs

darxriggs May 27, 2019

Contributor

I suggest to annotate this with @After so JUnit takes care of calling it even in case of failing assertions.

This comment has been minimized.

Copy link
@basil

basil May 29, 2019

Author Member

Thanks for the suggestion, @darxriggs. I think this would work for SwarmClientIntegrationTest (which uses JenkinsRule), but I don't think this would work for PipelineJobTest (which uses RestartableJenkinsRule). Empirical observation shows that when @After methods are used with RestartableJenkinsRule, they are executed before the steps in the RestartableJenkinsRule are executed. For the cleanup logic to serve its intended purpose, it needs to run after the test logic. Invoking the cleanup logic from inside a step in the RestartableJenkinsRule seems to be the only way to achieve this with RestartableJenkinsRule (as it exists today). Since this is the case, and since I want SwarmClientIntegrationTest to be consistent with PipelineJobTest, I think the code is best left as-is.

try {
processDestroyer.clean();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {

This comment has been minimized.

Copy link
@darxriggs

darxriggs May 27, 2019

Contributor

I suggest to annotate this with @After so JUnit takes care of calling it.
Otherwise in case of a failing assertion that call in line 46 will not be performed.

This comment has been minimized.

Copy link
@basil

basil May 29, 2019

Author Member

See my reply above.

try {
processDestroyer.clean();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Process> 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();
}
}
Loading

0 comments on commit c0e44ac

Please sign in to comment.