Skip to content

Commit

Permalink
Add failure to title (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Feb 6, 2021
1 parent 22f6a7c commit de42444
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package io.jenkins.plugins.checks.status;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Result;
import hudson.model.Run;
import io.jenkins.plugins.checks.api.ChecksOutput;
import io.jenkins.plugins.checks.api.TruncatedString;
import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jenkinsci.plugins.workflow.actions.*;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graph.StepNode;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -137,33 +142,93 @@ ChecksOutput extractOutput() {
.withTruncationText(TRUNCATED_MESSAGE);
indentationStack.clear();

table.getRows().forEach(row -> {
String title = null;
for (FlowGraphTable.Row row : table.getRows()) {
final FlowNode flowNode = row.getNode();

Optional<String> stageOrBranchName = getStageOrBranchName(flowNode);
ErrorAction errorAction = flowNode.getError();
WarningAction warningAction = flowNode.getPersistentAction(WarningAction.class);

if (!stageOrBranchName.isPresent()
&& errorAction == null
&& warningAction == null) {
return;
}
if (stageOrBranchName.isPresent() || errorAction != null || warningAction != null) {
final Pair<String, String> nodeInfo = stageOrBranchName.map(s -> processStageOrBranchRow(row, s))
.orElseGet(() -> processErrorOrWarningRow(row, errorAction, warningAction));

final Pair<String, String> nodeInfo = stageOrBranchName.map(s -> processStageOrBranchRow(row, s))
.orElseGet(() -> processErrorOrWarningRow(row, errorAction, warningAction));
// the last title will be used in the ChecksOutput (if any are found)
if (!stageOrBranchName.isPresent()) {
title = getPotentialTitle(flowNode, errorAction);
}

textBuilder.addText(nodeInfo.getLeft());
summaryBuilder.addText(nodeInfo.getRight());
});
textBuilder.addText(nodeInfo.getLeft());
summaryBuilder.addText(nodeInfo.getRight());
}
}

return new ChecksOutput.ChecksOutputBuilder()
.withTitle(extractOutputTitle())
.withTitle(extractOutputTitle(title))
.withSummary(summaryBuilder.build())
.withText(textBuilder.build())
.build();
}

private String getPotentialTitle(FlowNode flowNode, ErrorAction errorAction) {
final String whereBuildFailed = String.format("%s in '%s' step", errorAction == null ? "warning" : "error", flowNode.getDisplayFunctionName());

List<FlowNode> enclosingStagesAndParallels = getEnclosingStagesAndParallels(flowNode);
List<String> enclosingBlockNames = getEnclosingBlockNames(enclosingStagesAndParallels);

return StringUtils.join(new ReverseListIterator(enclosingBlockNames), "/") + ": " + whereBuildFailed;
}

private static boolean isStageNode(@NonNull FlowNode node) {
if (node instanceof StepNode) {
StepDescriptor d = ((StepNode) node).getDescriptor();
return d != null && d.getFunctionName().equals("stage");
}
else {
return false;
}
}

/**
* Get the stage and parallel branch start node IDs (not the body nodes) for this node, innermost first.
* @param node A flownode.
* @return A nonnull, possibly empty list of stage/parallel branch start nodes, innermost first.
*/
@NonNull
private static List<FlowNode> getEnclosingStagesAndParallels(FlowNode node) {
List<FlowNode> enclosingBlocks = new ArrayList<>();
for (FlowNode enclosing : node.getEnclosingBlocks()) {
if (enclosing != null && enclosing.getAction(LabelAction.class) != null) {
if (isStageNode(enclosing) || enclosing.getAction(ThreadNameAction.class) != null) {
enclosingBlocks.add(enclosing);
}
}
}

return enclosingBlocks;
}

@NonNull
private static List<String> getEnclosingBlockNames(@NonNull List<FlowNode> nodes) {
List<String> names = new ArrayList<>();
for (FlowNode n : nodes) {
ThreadNameAction threadNameAction = n.getPersistentAction(ThreadNameAction.class);
LabelAction labelAction = n.getPersistentAction(LabelAction.class);
if (threadNameAction != null) {
// If we're on a parallel branch with the same name as the previous (inner) node, that generally
// means we're in a Declarative parallel stages situation, so don't add the redundant branch name.
if (names.isEmpty() || !threadNameAction.getThreadName().equals(names.get(names.size() - 1))) {
names.add(threadNameAction.getThreadName());
}
}
else if (labelAction != null) {
names.add(labelAction.getDisplayName());
}
}
return names;
}

@CheckForNull
private static String getLog(final FlowNode flowNode) {
LogAction logAction = flowNode.getAction(LogAction.class);
Expand All @@ -174,22 +239,30 @@ private static String getLog(final FlowNode flowNode) {
if (logAction.getLogText().writeLogTo(0, out) == 0) {
return null;
}
return out.toString(StandardCharsets.UTF_8.toString());

String outputString = out.toString(StandardCharsets.UTF_8.toString());
// strip ansi color codes
return outputString.replaceAll("\u001B\\[[;\\d]*m", "");
}
catch (IOException e) {
LOGGER.log(Level.WARNING, String.format("Failed to extract logs for step '%s'", flowNode.getDisplayName()).replaceAll("[\r\n]", ""), e);
return null;
}
}

private String extractOutputTitle() {
private String extractOutputTitle(String title) {
Result result = run.getResult();
if (result == null) {
return "In progress";
}
if (result.isBetterOrEqualTo(Result.SUCCESS)) {
return "Success";
}

if (title != null) {
return title;
}

if (result.isBetterOrEqualTo(Result.UNSTABLE)) {
return "Unstable";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public void shouldPublishStageDetails() {
// Details 6, p1s1 has finished and emitted unstable
details = checksDetails.get(6);
assertThat(details.getOutput()).isPresent().get().satisfies(output -> {
assertThat(output.getTitle()).isPresent().get().isEqualTo("Unstable");
assertThat(output.getTitle()).isPresent().get().isEqualTo("In parallel/p1/p1s1: warning in 'unstable' step");
assertThat(output.getSummary()).isPresent().get().asString().isEqualToIgnoringNewLines(""
+ "### `In parallel / p1 / p1s1 / Set stage result to unstable`\n"
+ "Warning in `unstable` step, with arguments `something went wrong`.\n"
Expand All @@ -195,7 +195,7 @@ public void shouldPublishStageDetails() {
assertThat(details.getStatus()).isEqualTo(ChecksStatus.COMPLETED);
assertThat(details.getConclusion()).isEqualTo(ChecksConclusion.FAILURE);
assertThat(details.getOutput()).isPresent().get().satisfies(output -> {
assertThat(output.getTitle()).isPresent().get().isEqualTo("Failure");
assertThat(output.getTitle()).isPresent().get().isEqualTo("Fails: error in 'archiveArtifacts' step");
assertThat(output.getSummary()).isPresent().get().asString().matches(Pattern.compile(".*"
+ "### `In parallel / p1 / p1s1 / Set stage result to unstable`\\s+"
+ "Warning in `unstable` step, with arguments `something went wrong`\\.\\s+"
Expand Down Expand Up @@ -229,6 +229,29 @@ public void shouldPublishStageDetails() {
});
}

/**
* Validates the a simple successful pipeline works.
*/
@Test
public void shouldPublishSimplePipeline() {
PROPERTIES.setApplicable(true);
PROPERTIES.setSkipped(false);
PROPERTIES.setName("Test Status");
WorkflowJob job = createPipeline();

job.setDefinition(new CpsFlowDefinition(""
+ "node {\n"
+ " echo 'Hello, world'"
+ "}", true));

buildWithResult(job, Result.SUCCESS);

List<ChecksDetails> checksDetails = PUBLISHER_FACTORY.getPublishedChecks();

ChecksDetails details = checksDetails.get(1);
assertThat(details.getOutput()).isPresent().get().satisfies(output -> assertThat(output.getTitle()).isPresent().get().isEqualTo("Success"));
}

static class ChecksProperties extends AbstractStatusChecksProperties {
private boolean applicable;
private boolean skipped;
Expand Down

0 comments on commit de42444

Please sign in to comment.