Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration tests for both Freestyle and Pipeline jobs #81

Merged
merged 1 commit into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,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>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is necessary to allow the Swarm Client JAR to be downloaded from integration test context. Normally, this JAR is made available through the .hpi file via the maven-dependency-plugin (see lines 25–51 above). However, in integration test context the .hpi file is ignored and the test uses the .hpl version of the plugin. As such we need to customize the .hpl metadata to include the Swarm Client JAR on its classpath.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not safe, because it also injects the client's classes to the plugin. And not all this libs are guaranteed to be compatible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not safe, because it also injects the client's classes to the plugin. And not all this libs are guaranteed to be compatible.

It is safe, for the reasons explained in my top-level reply.

</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 @@ -64,5 +103,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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that since this is waiting in node but not sh it only exercises part of jenkinsci/workflow-durable-task-step-plugin#104 (when jenkinsci/workflow-durable-task-step-plugin@58b127f was added).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that since this is waiting in node but not sh it only exercises part of jenkinsci/workflow-durable-task-step-plugin#104

Good point. I added a new test, PipelineJobTest#buildShellScriptAcrossDisconnect, which waits in sh (essentially a port of ExecutorStepTest#buildShellScriptAcrossDisconnect).

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.");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests that a Swarm Client launched with -deleteExistingClients can reconnect to a Jenkins master after the master has been restarted. The -deleteExistingClients only works in this use case when the Jenkins master has the same port before and after the restart; however, RestartableJenkinsRule chooses a random port each time. Therefore, we are forced to skip this test unless the tests are being run with -Dport=<port-number> to fix the port before and after the restart. Unfortunately, this means this test cannot run on ci.jenkins.io since AFAIK there is no isolation between builds there (so tests cannot assume that a particular port will be free).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RestartableJenkinsRule chooses a random port each time

That ought to be fixable, just like it already keeps the same $JENKINS_HOME.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll work on that in a downstream PR so as to keep this PR from developing a dependency on a Jenkins test harness change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. If you want CI on that,

mvn -f jenkins-test-harness clean install source:jar-no-fork deploy:deploy -DaltDeploymentRepository=maven.jenkins-ci.org::default::https://repo.jenkins-ci.org/snapshots/ -DskipTests

and specify the timestamped snapshot for jenkins-test-harness.version. Though I should probably just incrementalify it (JEP-305) to make this easier. Give me a moment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. If you want CI on that,

mvn -f jenkins-test-harness clean install source:jar-no-fork deploy:deploy -DaltDeploymentRepository=maven.jenkins-ci.org::default::https://repo.jenkins-ci.org/snapshots/ -DskipTests

and specify the timestamped snapshot for jenkins-test-harness.version.

Thanks! I did as you suggested in #83.


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);
}
}
}
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 {
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