package org.jenkinsci.plugins.workflow;
import groovy.lang.Closure;
import hudson.model.Result;
import hudson.slaves.DumbSlave;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
import static org.junit.Assert.assertTrue;
import org.junit.Ignore;
import org.jvnet.hudson.test.Issue;
/**
* Tests related to serialization of program state.
*/
public class SerializationTest extends SingleJobTestBase {
/**
* When wokflow execution runs into a serialization problem, can we handle that situation gracefully?
*/
@Test
public void stepExecutionFailsToPersist() throws Exception {
story.addStep(new Statement() {
@Override
public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(join(
"node {",
" persistenceProblem()",
"}"
)));
startBuilding();
waitForWorkflowToSuspend();
// TODO: let the ripple effect of a failure run to the completion.
while (b.isBuilding())
try {
waitForWorkflowToSuspend();
} catch (Exception x) {
// ignore persistence failure
String message = x.getMessage();
if (message == null || !message.contains("Failed to persist")) {
throw x;
}
}
story.j.assertBuildStatus(Result.FAILURE, b);
story.j.assertLogContains("java.lang.RuntimeException: testing the forced persistence failure behaviour", b);
}
});
story.addStep(new Statement() {
@Override
public void evaluate() throws Throwable {
rebuildContext(story.j);
story.j.assertBuildStatus(Result.FAILURE, b);
}
});
}
/**
* {@link Step} that fails to persist. Used to test the behaviour of error reporting/recovery.
*/
public static class PersistenceProblemStep extends AbstractStepImpl {
@DataBoundConstructor
public PersistenceProblemStep() {
super();
}
@TestExtension("stepExecutionFailsToPersist")
public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() {
super(PersistenceProblemStepExecution.class);
}
@Override
public String getFunctionName() {
return "persistenceProblem";
}
@Override
public String getDisplayName() {
return "Problematic Persistence";
}
}
/**
* {@link StepExecution} that fails to serialize.
*
* Used to test the error recovery path of {@link WorkflowJob}.
*/
public static class PersistenceProblemStepExecution extends AbstractStepExecutionImpl {
public final Object notSerializable = new Object();
private Object writeReplace() {
throw new RuntimeException("testing the forced persistence failure behaviour");
}
@Override
public boolean start() throws Exception {
return false;
}
@Override
public void stop(Throwable cause) throws Exception {
// nothing to do here
}
}
}
/**
* Workflow captures a stateful object, and we verify that it survives the restart
*/
@Test public void persistEphemeralObject() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
jenkins().setNumExecutors(0);
DumbSlave s = createSlave(story.j);
String nodeName = s.getNodeName();
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"def s = jenkins.model.Jenkins.instance.getComputer('" + nodeName + "')\n" +
"def r = s.node.rootPath\n" +
"def p = r.getRemote()\n" +
"semaphore 'wait'\n" +
// make sure these values are still alive
"assert s.nodeName=='" + nodeName + "'\n" +
"assert r.getRemote()==p : r.getRemote() + ' vs ' + p;\n" +
"assert r.channel==s.channel : r.channel.toString() + ' vs ' + s.channel\n"));
startBuilding();
SemaphoreStep.waitForStart("wait/1", b);
assertTrue(b.isBuilding());
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
assertThatWorkflowIsSuspended();
SemaphoreStep.success("wait/1", null);
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b));
}
});
}
@Issue("JENKINS-27421")
@Test public void listIterator() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.List java.lang.Object"); // TODO pending https://github.com/jenkinsci/script-security-plugin/pull/96
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"def arr = []; arr += 'one'; arr += 'two'\n" +
"for (int i = 0; i < arr.size(); i++) {def elt = arr[i]; echo \"running C-style loop on ${elt}\"; semaphore \"C-${elt}\"}\n" +
"for (def elt : arr) {echo \"running new-style loop on ${elt}\"; semaphore \"new-${elt}\"}"
, true));
startBuilding();
SemaphoreStep.waitForStart("C-one/1", b);
story.j.waitForMessage("running C-style loop on one", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
SemaphoreStep.success("C-one/1", null);
SemaphoreStep.success("C-two/1", null);
story.j.waitForMessage("running C-style loop on two", b);
SemaphoreStep.waitForStart("new-one/1", b);
story.j.waitForMessage("running new-style loop on one", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
SemaphoreStep.success("new-one/1", null);
SemaphoreStep.success("new-two/1", null);
story.j.waitForCompletion(b);
story.j.assertBuildStatusSuccess(b);
story.j.assertLogContains("running new-style loop on two", b);
}
});
}
@Issue("JENKINS-27421")
@Test public void mapIterator() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"def map = [one: 1, two: 2]\n" +
"@NonCPS def entrySet(m) {m.collect {k, v -> [key: k, value: v]}}; for (def e in entrySet(map)) {echo \"running flattened loop on ${e.key} -> ${e.value}\"; semaphore \"C-${e.key}\"}\n" +
"for (def e : map.entrySet()) {echo \"running new-style loop on ${e.key} -> ${e.value}\"; semaphore \"new-${e.key}\"}"
// TODO check also keySet(), values()
, true));
startBuilding();
SemaphoreStep.waitForStart("C-one/1", b);
story.j.waitForMessage("running flattened loop on one -> 1", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
SemaphoreStep.success("C-one/1", null);
SemaphoreStep.success("C-two/1", null);
story.j.waitForMessage("running flattened loop on two -> 2", b);
SemaphoreStep.waitForStart("new-one/1", b);
story.j.waitForMessage("running new-style loop on one -> 1", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
SemaphoreStep.success("new-one/1", null);
SemaphoreStep.success("new-two/1", null);
story.j.waitForCompletion(b);
/* TODO desired behavior:
story.j.assertBuildStatusSuccess(b);
story.j.assertLogContains("running new-style loop on two -> 2", b);
*/
story.j.assertBuildStatus(Result.FAILURE, b);
story.j.assertLogContains("java.io.NotSerializableException: java.util.LinkedHashMap$Entry", b);
}
});
}
@Test public void nonCps() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"echo \"first parse: ${parse('foo <version>1.0</version> bar')}\"\n" +
"echo \"second parse: ${parse('foo bar')}\"\n" +
"@NonCPS def parse(text) {\n" +
" def matcher = text =~ '<version>(.+)</version>'\n" +
" matcher ? matcher[0][1] : null\n" +
"}\n", true));
b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("first parse: 1.0", b);
story.j.assertLogContains("second parse: null", b);
}
});
}
@Ignore("TODO backed out for JENKINS-34064")
@Issue("JENKINS-26481")
@Test public void eachClosure() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"node {\n" +
" ['a', 'b', 'c'].each {f -> writeFile file: f, text: f}\n" +
" def text = ''\n" +
" ['a', 'b', 'c'].each {f -> semaphore f; text += readFile f}\n" +
" echo text\n" +
"}\n", true));
b = p.scheduleBuild2(0).waitForStart();
SemaphoreStep.waitForStart("a/1", b);
SemaphoreStep.success("a/1", null);
SemaphoreStep.waitForStart("b/1", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
SemaphoreStep.success("b/1", null);
SemaphoreStep.waitForStart("c/1", b);
SemaphoreStep.success("c/1", null);
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b));
story.j.assertLogContains("abc", b);
}
});
}
/**
* Verifies that we are not throwing {@link UnsupportedOperationException} too aggressively.
* In particular:
* <ul>
* <li>on non-CPS-transformed {@link Closure}s
* <li>on closures passed to methods defined in Pipeline script
* <li>on closures passed to methods which did not declare {@link Closure} as a parameter type and so presumably are not going to try to call them
* </ul>
*/
@Issue("JENKINS-26481")
@Test public void eachClosureNonCps() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.List java.lang.Object"); // TODO pending https://github.com/jenkinsci/script-security-plugin/pull/96
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"@NonCPS def fine() {\n" +
" def text = ''\n" +
" ['a', 'b', 'c'].each {it -> text += it}\n" +
" text\n" +
"}\n" +
"def takesMyOwnClosure(body) {\n" +
" node {\n" +
" def list = []\n" +
" list += body\n" +
" echo list[0]()\n" +
" }\n" +
"}\n" +
"takesMyOwnClosure {\n" +
" fine()\n" +
"}\n", true));
b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("abc", b);
}
});
}
@Ignore("TODO JENKINS-31314: calls writeFile just once, echoes null (i.e., return value of writeFile), then succeeds")
@Test public void nonCpsContinuable() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"@NonCPS def shouldBomb() {\n" +
" def text = ''\n" +
" ['a', 'b', 'c'].each {it -> writeFile file: it, text: it; text += it}\n" +
" text\n" +
"}\n" +
"node {\n" +
" echo shouldBomb()\n" +
"}\n", true));
b = story.j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get());
}
});
}
}