From de4244431b558c17d230697acb008e6fdb2ef411 Mon Sep 17 00:00:00 2001 From: Tim Jacomb <21194782+timja@users.noreply.github.com> Date: Sat, 6 Feb 2021 16:55:58 +0000 Subject: [PATCH] Add failure to title (#76) --- .../checks/status/FlowExecutionAnalyzer.java | 101 +++++++++++++++--- .../BuildStatusChecksPublisherITest.java | 27 ++++- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java index 299ae580..0e970352 100644 --- a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java +++ b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java @@ -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; @@ -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 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 nodeInfo = stageOrBranchName.map(s -> processStageOrBranchRow(row, s)) + .orElseGet(() -> processErrorOrWarningRow(row, errorAction, warningAction)); - final Pair 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 enclosingStagesAndParallels = getEnclosingStagesAndParallels(flowNode); + List 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 getEnclosingStagesAndParallels(FlowNode node) { + List 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 getEnclosingBlockNames(@NonNull List nodes) { + List 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); @@ -174,7 +239,10 @@ 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); @@ -182,7 +250,7 @@ private static String getLog(final FlowNode flowNode) { } } - private String extractOutputTitle() { + private String extractOutputTitle(String title) { Result result = run.getResult(); if (result == null) { return "In progress"; @@ -190,6 +258,11 @@ private String extractOutputTitle() { if (result.isBetterOrEqualTo(Result.SUCCESS)) { return "Success"; } + + if (title != null) { + return title; + } + if (result.isBetterOrEqualTo(Result.UNSTABLE)) { return "Unstable"; } diff --git a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java index e921ba53..e5180775 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -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" @@ -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+" @@ -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 = 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;