diff --git a/pom.xml b/pom.xml index 6067afd0..9cd3aeab 100644 --- a/pom.xml +++ b/pom.xml @@ -85,12 +85,17 @@ org.jenkins-ci.plugins.workflow workflow-api - 1.15 + 2.1-SNAPSHOT + + + org.jenkins-ci.plugins + script-security + 1.18 org.jenkins-ci.plugins.workflow workflow-cps - 1.15 + 2.2-SNAPSHOT test diff --git a/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java b/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java new file mode 100644 index 00000000..e4f028a6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java @@ -0,0 +1,194 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.steps; + +import com.google.inject.Inject; +import hudson.AbortException; +import hudson.Extension; +import hudson.Functions; +import hudson.model.Result; +import java.io.IOException; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.jenkinsci.plugins.workflow.actions.ErrorAction; +import org.jenkinsci.plugins.workflow.actions.LogAction; +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; +import org.jenkinsci.plugins.workflow.graph.BlockEndNode; +import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graph.FlowNodeSerialWalker; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Step to supply contextual information about an error that has been caught. + */ +public class ErrorInfoStep extends AbstractStepImpl { + + public final Throwable error; + + @DataBoundConstructor public ErrorInfoStep(Throwable error) { + this.error = error; + } + + public static class Execution extends AbstractSynchronousStepExecution { + + private static final long serialVersionUID = 1; + @Inject private transient ErrorInfoStep step; + @StepContextParameter private transient FlowExecution execution; + + @Override protected ErrorInfo run() throws Exception { + return new ErrorInfo(step.error, execution); + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "errorInfo"; + } + + @Override public String getDisplayName() { + return "Calculate information about an error"; + } + + // TODO blank config.jelly + + } + + public static class ErrorInfo implements Serializable { + + private static final long serialVersionUID = 1; + private final Throwable error; + private transient FlowExecution execution; + private final FlowExecutionOwner executionOwner; + + ErrorInfo(Throwable error, FlowExecution execution) { + this.error = error; + this.execution = execution; + executionOwner = execution.getOwner(); + } + + private FlowExecution getExecution() throws IOException { + if (execution == null) { + execution = executionOwner.get(); + } + return execution; + } + + /** + * Finds a node which threw this exception or one of its causes. + * Note that {@link Throwable#equals} is just pointer equality, + * which we cannot use since we may be loading deserialized exceptions, + * so we compare by stack trace instead. + */ + private @CheckForNull FlowNode getNode() throws IOException { + Set stackTraces = new HashSet<>(); + for (Throwable t = error; t != null; t = t.getCause()) { + stackTraces.add(Functions.printThrowable(t)); + } + for (FlowNode n : new FlowGraphWalker(getExecution())) { + if (n instanceof BlockEndNode) { + continue; // look for the thing it is enclosing + } + ErrorAction a = n.getAction(ErrorAction.class); + if (a != null) { + if (stackTraces.contains(Functions.printThrowable(a.getError()))) { + return n; + } + } + } + return null; + } + + @Whitelisted + public @Nonnull Throwable getError() { + return error; + } + + /** + * Gets the stack trace of the error, or just the message in the case of {@link AbortException}. + */ + @Whitelisted + public @Nonnull String getStackTrace() { + if (error instanceof AbortException) { + return error.getMessage(); + } else { + return Functions.printThrowable(error); + } + } + + /** + * Gets the {@link Result} of the build if the error were uncaught. + * @return typically {@link Result#FAILURE} but {@link FlowInterruptedException} may override + */ + @Whitelisted + public @Nonnull String getResult() { + Result r; + if (error instanceof FlowInterruptedException) { + r = ((FlowInterruptedException) error).getResult(); + } else { + r = Result.FAILURE; + } + return r.toString(); + } + + /** + * Looks for the URL of the {@link LogAction} last printed before the node which broke. + */ + @Whitelisted + public @CheckForNull String getLogURL() throws IOException { + FlowNode n = getNode(); + if (n != null) { + for (FlowNode n2 : new FlowNodeSerialWalker(n)) { + LogAction a = n2.getAction(LogAction.class); + if (a != null) { + String u = Jenkins.getActiveInstance().getRootUrl(); + if (u == null) { + u = "http://jenkins/"; // placeholder + } + return u + n2.getUrl() + a.getUrlName(); + } + } + } + return null; + } + + // TODO tail of log + // TODO label + + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java new file mode 100644 index 00000000..79490f3b --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java @@ -0,0 +1,98 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.steps; + +import hudson.AbortException; +import hudson.model.Result; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +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 static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class ErrorInfoStepTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public RestartableJenkinsRule s = new RestartableJenkinsRule(); + + @Issue("JENKINS-28119") + @Test public void smokes() { + s.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WorkflowJob p = s.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "try {\n" + + " parallel fine: {\n" + + " semaphore 'fine'\n" + + " }, broken: {\n" + + " echo 'erroneous step'\n" + + " semaphore 'breaking'\n" + + " }\n" + + "} catch (e) {\n" + + " def info = errorInfo(e)\n" + + " semaphore 'caught'\n" + + " currentBuild.result = info.result\n" + + " echo \"caught an instance of ${info.error.getClass()}\"\n" + + " echo info.stackTrace\n" + + " echo \"browse to: ${info.logURL}\"\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("fine/1", b); + SemaphoreStep.failure("breaking/1", new AbortException("oops")); + SemaphoreStep.success("fine/1", null); + SemaphoreStep.waitForStart("caught/1", null); + } + }); + s.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("caught/1", null); + WorkflowJob p = s.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getBuildByNumber(1); + s.j.assertBuildStatus(Result.FAILURE, s.j.waitForCompletion(b)); + s.j.waitForMessage("End of Pipeline", b); // TODO why does it sometimes cut off at "Resuming build"? probably because WorkflowRun.finish sets isBuilding() → false before flushing the log + s.j.assertLogContains("caught an instance of class hudson.AbortException", b); + s.j.assertLogContains("oops", b); + s.j.assertLogNotContains("\tat ", b); + String log = JenkinsRule.getLog(b); + Matcher matcher = Pattern.compile("^browse to: (http.+)$", Pattern.MULTILINE).matcher(log); + assertTrue(log, matcher.find()); + String text = s.j.createWebClient().getPage(new URL(matcher.group(1))).getWebResponse().getContentAsString(); + assertTrue(text, text.contains("erroneous step")); + } + }); + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java index b4608f5d..95ae0f3e 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java @@ -18,8 +18,6 @@ import org.jvnet.hudson.test.RestartableJenkinsRule; import java.util.List; -import org.jenkinsci.plugins.workflow.steps.SleepStep; -import org.jenkinsci.plugins.workflow.steps.TimeoutStepExecution; /** * @author Kohsuke Kawaguchi @@ -64,7 +62,7 @@ public void evaluate() throws Throwable { + " }\n" + " echo 'NotHere'\n" + "}\n")); - WorkflowRun b = story.j.assertBuildStatus(/* TODO JENKINS-25894 should really be ABORTED */Result.FAILURE, p.scheduleBuild2(0).get()); + WorkflowRun b = story.j.assertBuildStatus(Result.ABORTED, p.scheduleBuild2(0).get()); // make sure things that are supposed to run do, and things that are NOT supposed to run do not. story.j.assertLogNotContains("NotHere", b);