package org.jenkinsci.plugins.workflow.cps; import com.google.common.base.Function; import com.google.common.collect.Sets; import groovy.lang.Closure; import hudson.model.Result; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import org.codehaus.groovy.runtime.ScriptBytecodeAdapter; import org.hamcrest.Matchers; import org.jenkinsci.plugins.workflow.actions.ErrorAction; import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode; import org.jenkinsci.plugins.workflow.cps.nodes.StepNode; import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; 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.BodyExecution; import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; import static org.junit.Assert.*; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.DataBoundConstructor; public class CpsBodyExecutionTest { @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @Rule public JenkinsRule jenkins = new JenkinsRule(); /** * When the body of a step is synchronous and explodes, the failure should be recorded and the pipeline job * should move on. * * But instead, this hangs because CpsBodyExecution has a bug in how it handles this case. * It tries to launch the body (in this case the 'bodyBlock' method) in a separate CPS thread, * and puts the parent CPS thread on hold. Yet when the child CPS thread ends with an exception, * it fails to record this result correctly, and it gets into the eternal sleep in which * the parent CPS thread expects to be notified of the outcome of the child CPS thread, which * never arrives. */ @Test public void synchronousExceptionInBody() throws Exception { WorkflowJob p = jenkins.createProject(WorkflowJob.class); p.setDefinition(new CpsFlowDefinition("synchronousExceptionInBody()",true)); WorkflowRun b = jenkins.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0)); jenkins.assertLogContains(EmperorHasNoClothes.class.getName(),b); {// assert the shape of FlowNodes FlowGraphWalker w = new FlowGraphWalker(b.getExecution()); List<String> nodes = new ArrayList<>(); for (FlowNode n : w) { String s = n.getClass().getSimpleName(); if (n instanceof StepNode) { StepNode sn = (StepNode) n; s+=":"+sn.getDescriptor().getFunctionName(); } if (n instanceof StepEndNode) { // this should have recorded a failure ErrorAction e = n.getAction(ErrorAction.class); assertEquals(EmperorHasNoClothes.class,e.getError().getClass()); } nodes.add(s); } assertEquals(Arrays.asList( "FlowEndNode", "StepEndNode:synchronousExceptionInBody", // this for the end of invoking a body "StepStartNode:synchronousExceptionInBody", // this for invoking a body // this for the 'synchronousExceptionInBody' invocation because Pipeline Engine // thinks this step has no children. There's a TODO in DSL.invokeStep about // letting steps take over the FlowNode creation that should solve this. "StepAtomNode:synchronousExceptionInBody", "FlowStartNode" ),nodes); } } public static class SynchronousExceptionInBodyStep extends AbstractStepImpl { @DataBoundConstructor public SynchronousExceptionInBodyStep() {} @TestExtension public static class DescriptorImpl extends AbstractStepDescriptorImpl { public DescriptorImpl() { super(Execution.class); } @Override public String getFunctionName() { return "synchronousExceptionInBody"; } } public static class Execution extends AbstractStepExecutionImpl { /** * Invoked as a body that induces a synchronous exception */ public void bodyBlock() { throw new EmperorHasNoClothes(); } @Override public boolean start() throws Exception { Closure body = ScriptBytecodeAdapter.getMethodPointer(this, "bodyBlock"); CpsStepContext cps = (CpsStepContext) getContext(); CpsThread t = CpsThread.current(); cps.newBodyInvoker(t.getGroup().export(body)) .withCallback(BodyExecutionCallback.wrap(cps)) .start(); return false; } @Override public void stop(@Nonnull Throwable cause) throws Exception { throw new UnsupportedOperationException(); } } } @Issue("JENKINS-34637") @Test public void currentExecutions() throws Exception { WorkflowJob p = jenkins.createProject(WorkflowJob.class); p.setDefinition(new CpsFlowDefinition("parallel main: {retainsBody {parallel a: {retainsBody {semaphore 'a'}}, b: {retainsBody {semaphore 'b'}}}}, aside: {semaphore 'c'}", true)); WorkflowRun b = p.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("a/1", b); SemaphoreStep.waitForStart("b/1", b); SemaphoreStep.waitForStart("c/1", b); final RetainsBodyStep.Execution[] execs = new RetainsBodyStep.Execution[3]; StepExecution.applyAll(RetainsBodyStep.Execution.class, new Function<RetainsBodyStep.Execution, Void>() { @Override public Void apply(RetainsBodyStep.Execution exec) { execs[exec.count] = exec; return null; } }).get(); assertNotNull(execs[0]); assertNotNull(execs[1]); assertNotNull(execs[2]); final Set<SemaphoreStep.Execution> semaphores = new HashSet<>(); StepExecution.applyAll(SemaphoreStep.Execution.class, new Function<SemaphoreStep.Execution, Void>() { @Override public Void apply(SemaphoreStep.Execution exec) { if (exec.getStatus().matches("waiting on [ab]/1")) { semaphores.add(exec); } return null; } }).get(); assertThat(semaphores, Matchers.<SemaphoreStep.Execution>iterableWithSize(2)); Collection<StepExecution> currentExecutions1 = execs[1].body.getCurrentExecutions(); // A or B, does not matter assertThat(/* irritatingly, iterableWithSize does not show the collection in its mismatch message */currentExecutions1.toString(), currentExecutions1, Matchers.<StepExecution>iterableWithSize(1)); Collection<StepExecution> currentExecutions2 = execs[2].body.getCurrentExecutions(); assertThat(currentExecutions2, Matchers.<StepExecution>iterableWithSize(1)); assertEquals(semaphores, Sets.union(Sets.newLinkedHashSet(currentExecutions1), Sets.newLinkedHashSet(currentExecutions2))); assertEquals(semaphores, Sets.newLinkedHashSet(execs[0].body.getCurrentExecutions())); // the top-level one execs[0].body.cancel(); SemaphoreStep.success("c/1", null); jenkins.assertBuildStatus(Result.ABORTED, jenkins.waitForCompletion(b)); } public static class RetainsBodyStep extends AbstractStepImpl { @DataBoundConstructor public RetainsBodyStep() {} @TestExtension("currentExecutions") public static class DescriptorImpl extends AbstractStepDescriptorImpl { public DescriptorImpl() {super(Execution.class);} @Override public String getFunctionName() {return "retainsBody";} @Override public boolean takesImplicitBlockArgument() {return true;} } public static class Execution extends AbstractStepExecutionImpl { static int counter; BodyExecution body; int count = counter++; @Override public boolean start() throws Exception { body = getContext().newBodyInvoker().withCallback(BodyExecutionCallback.wrap(getContext())).start(); return false; } @Override public void stop(Throwable cause) throws Exception { throw new AssertionError("block #" + count + " not supposed to be killed directly", cause); } } } }