Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
9 changes: 7 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,17 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>1.15</version>
<version>2.1-SNAPSHOT</version>
Copy link
Member Author

Choose a reason for hiding this comment

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

</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.18</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>1.15</version>
<version>2.2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* 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<ErrorInfo> {

@Inject private ErrorInfoStep step;
@StepContextParameter private 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<String> 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.getInstance().getRootUrl();
if (u == null) {
u = "http://jenkins/"; // placeholder
}
return u + n2.getUrl() + a.getUrlName();
}
}
}
return null;
}

// TODO tail of log
// TODO label

}

}
Original file line number Diff line number Diff line change
@@ -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"));
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down