From a7c1f59b4dd2dc2aa5324754b386bd4f656df128 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 10:53:44 -0800 Subject: [PATCH 01/13] kind of working --- .../plugins/checks/api/ChecksOutput.java | 74 +++++++++++++++++-- .../plugins/checks/api/ChecksOutputTest.java | 49 ++++++++---- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index 975501bb..da6459a0 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -62,7 +62,7 @@ public Optional getSummary() { * @return Summary, truncated to maxSize with truncation message if appropriate. */ public Optional getSummary(final int maxSize) { - return Optional.ofNullable(summary).map(s -> s.build(maxSize)); + return truncateSummary(summary, maxSize); } public Optional getText() { @@ -76,13 +76,13 @@ public Optional getText() { * @return Text, truncated to maxSize with truncation message if appropriate. */ public Optional getText(final int maxSize) { - return Optional.ofNullable(text) - .map(s -> new TruncatedString.Builder() - .setChunkOnNewlines() - .setTruncateStart() - .addText(s.toString()) - .build() - .buildByChars(maxSize)); + return Optional.ofNullable(text).map(s -> new TruncatedString.Builder() + .setChunkOnNewlines() + .setTruncateStart() + .withTruncationText("Output truncated.\n") + .addText(s.toString()) + .build() + .buildByChars(maxSize)); } public List getChecksAnnotations() { @@ -104,6 +104,64 @@ public String toString() { + '}'; } + private Optional truncateSummary(final TruncatedString summary, final int maxSize) { + if (summary == null) { + return Optional.empty(); + } + + String content = summary.toString(); + if (!content.contains("")) { + return Optional.of(summary.build(maxSize)); + } + + // Find the build log section + int detailsStart = content.indexOf("
"); + int detailsEnd = content.indexOf("
") + "".length(); + + if (detailsStart == -1 || detailsEnd == -1) { + return Optional.of(summary.build(maxSize)); + } + + // Split into pre-details, details block, and post-details + String preDetails = content.substring(0, detailsStart); + String details = content.substring(detailsStart, detailsEnd); + String postDetails = content.substring(detailsEnd); + + // Find the actual log content within the details + int logStart = details.indexOf("```\n") + 4; + int logEnd = details.lastIndexOf("\n```"); + + if (logStart == -1 || logEnd == -1) { + return Optional.of(summary.build(maxSize)); + } + + String beforeLog = details.substring(0, logStart); + String log = details.substring(logStart, logEnd); + String afterLog = details.substring(logEnd); + + // Calculate available space for log + int nonLogLength = preDetails.length() + beforeLog.length() + afterLog.length() + postDetails.length(); + int availableForLog = maxSize - nonLogLength; + + if (availableForLog <= 0) { + // If no space for log, truncate the whole content + return Optional.of(summary.build(maxSize)); + } + + // Truncate the log using TruncatedString + log = new TruncatedString.Builder() + .setChunkOnNewlines() + .setTruncateStart() + .withTruncationText("Build log truncated.\n") + .addText(log) + .build() + .buildByChars(availableForLog); + + // Reconstruct the content + String truncatedContent = preDetails + beforeLog + log + afterLog + postDetails; + return Optional.of(truncatedContent); + } + /** * Builder for {@link ChecksOutput}. */ diff --git a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java index 44a6fb68..053324cb 100644 --- a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java +++ b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java @@ -98,13 +98,32 @@ void shouldCopyConstructCorrectly() { } @Test - void shouldTruncateTextFromStart() { - String longText = "This is the beginning.\n" + "Middle part.\n".repeat(10) + "This is the end.\n"; + void shouldTruncateSummaryLogFromStart() { + String summary = "### `Fails / Shell Script`\n" + + "Error in `sh` step.\n" + + "```\n" + + "script returned exit code 1\n" + + "```\n" + + "
\n" + + "Build log\n" + + "\n" + + "```\n" + + "+ echo 'First line of log'\n" + + "First line of log\n" + + "+ echo 'Second line of log'\n" + + "Second line of log\n" + + "+ echo 'Third line of log'\n" + + "Third line of log\n" + + "+ exit 1\n" + + "```\n" + + "
\n" + + "\n"; + ChecksOutput checksOutput = new ChecksOutputBuilder() - .withText(longText) + .withSummary(summary) .build(); - String truncated = checksOutput.getText(75).orElse(""); + String truncated = checksOutput.getSummary(200).orElse(""); assertThat(truncated) .startsWith("Output truncated.") @@ -112,19 +131,19 @@ void shouldTruncateTextFromStart() { assertThat(truncated.length()).isLessThanOrEqualTo(75); } - @Test - void shouldNotTruncateShortText() { - String shortText = "This is a short text that should not be truncated."; - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withText(shortText) - .build(); +// @Test +// void shouldNotTruncateShortText() { +// String shortText = "This is a short text that should not be truncated."; +// ChecksOutput checksOutput = new ChecksOutputBuilder() +// .withText(shortText) +// .build(); - String result = checksOutput.getText(100).orElse(""); +// String result = checksOutput.getText(100).orElse(""); - assertThat(result) - .isEqualTo(shortText) - .doesNotContain("Output truncated."); - } +// assertThat(result) +// .isEqualTo(shortText) +// .doesNotContain("Output truncated."); +// } private List createAnnotations() { final ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() From e3fef676c0cbc828551d00f56e1e97566a66b6a4 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 11:12:41 -0800 Subject: [PATCH 02/13] testing --- .../plugins/checks/api/ChecksOutput.java | 7 + .../plugins/checks/api/ChecksOutputTest.java | 130 ++++++++++++++---- 2 files changed, 111 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index da6459a0..a50f210d 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -104,6 +104,13 @@ public String toString() { + '}'; } + /** + * Truncates the summary to the given maxSize. Tries to truncate from start of build log section if possible. + * + * @param summary the summary to truncate + * @param maxSize the maximum size to truncate to + * @return the truncated summary + */ private Optional truncateSummary(final TruncatedString summary, final int maxSize) { if (summary == null) { return Optional.empty(); diff --git a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java index 053324cb..c09ed8fe 100644 --- a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java +++ b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java @@ -30,10 +30,9 @@ void shouldBuildCorrectlyWithAllFields() { .addImage(images.get(1)) .build(); - ChecksOutputAssert.assertThat(checksOutput) - .hasTitle(Optional.of(TITLE)) - .hasSummary(Optional.of(SUMMARY)) - .hasText(Optional.of(TEXT)); + assertThat(checksOutput.getTitle()).isEqualTo(Optional.of(TITLE)); + assertThat(checksOutput.getSummary()).isEqualTo(Optional.of(SUMMARY)); + assertThat(checksOutput.getText()).isEqualTo(Optional.of(TEXT)); assertThat(checksOutput.getChecksAnnotations()) .usingFieldByFieldElementComparator() .containsExactlyInAnyOrderElementsOf(annotations); @@ -85,10 +84,9 @@ void shouldCopyConstructCorrectly() { .build(); ChecksOutput copied = new ChecksOutput(checksOutput); - ChecksOutputAssert.assertThat(copied) - .hasTitle(Optional.of(TITLE)) - .hasSummary(Optional.of(SUMMARY)) - .hasText(Optional.of(TEXT)); + assertThat(copied.getTitle()).isEqualTo(Optional.of(TITLE)); + assertThat(copied.getSummary()).isEqualTo(Optional.of(SUMMARY)); + assertThat(copied.getText()).isEqualTo(Optional.of(TEXT)); assertThat(copied.getChecksAnnotations()) .usingFieldByFieldElementComparator() .containsExactlyInAnyOrderElementsOf(annotations); @@ -118,32 +116,112 @@ void shouldTruncateSummaryLogFromStart() { + "```\n" + "\n" + "\n"; + int maxSize = 200; ChecksOutput checksOutput = new ChecksOutputBuilder() .withSummary(summary) .build(); - String truncated = checksOutput.getSummary(200).orElse(""); - + String truncated = checksOutput.getSummary(maxSize).orElse(""); + String expected = "### `Fails / Shell Script`\n" + + "Error in `sh` step.\n" + + "```\n" + + "script returned exit code 1\n" + + "```\n" + + "
\n" + + "Build log\n" + + "\n" + + "```\n" + + "Build log truncated.\n" + + "Third line of log\n" + + "+ exit 1\n" + + "```\n" + + "
\n" + + "\n"; + assertThat(truncated) - .startsWith("Output truncated.") - .endsWith("This is the end.\n"); - assertThat(truncated.length()).isLessThanOrEqualTo(75); + .isEqualTo(expected); + assertThat(truncated.length()).isLessThanOrEqualTo(maxSize); } -// @Test -// void shouldNotTruncateShortText() { -// String shortText = "This is a short text that should not be truncated."; -// ChecksOutput checksOutput = new ChecksOutputBuilder() -// .withText(shortText) -// .build(); - -// String result = checksOutput.getText(100).orElse(""); - -// assertThat(result) -// .isEqualTo(shortText) -// .doesNotContain("Output truncated."); -// } + @Test + void shouldHandleNullSummary() { + ChecksOutput checksOutput = new ChecksOutputBuilder().build(); + assertThat(checksOutput.getSummary(100)).isEmpty(); + } + + @Test + void shouldNotTruncateSummaryWithoutBuildLog() { + String summary = "### Simple Summary\nWithout any build logs\n"; + ChecksOutput checksOutput = new ChecksOutputBuilder() + .withSummary(summary) + .build(); + + assertThat(checksOutput.getSummary(100).orElse("")) + .isEqualTo(summary); + } + + @Test + void shouldHandleMalformedBuildLog() { + String summary = "### Header\n
\nMalformed log without closing tags"; + ChecksOutput checksOutput = new ChecksOutputBuilder() + .withSummary(summary) + .build(); + + assertThat(checksOutput.getSummary(100).orElse("")) + .isEqualTo(summary); + } + + @Test + void shouldHandleVerySmallMaxSize() { + String summary = "### `Fails / Shell Script`\n" + + "
\n" + + "Build log\n" + + "```\n" + + "+ echo 'First line'\n" + + "First line\n" + + "```\n" + + "
\n"; + + ChecksOutput checksOutput = new ChecksOutputBuilder() + .withSummary(summary) + .build(); + + // Size so small that only header and truncation message fit + String truncated = checksOutput.getSummary(50).orElse(""); + assertThat(truncated) + .startsWith("### `Fails / Shell Script`\n") + .doesNotContain("
") + .doesNotContain("Build log") + .doesNotContain("Build log truncated.") + .contains("Output truncated.") + .hasSizeLessThanOrEqualTo(50); + } + + @Test + void shouldPreserveMarkdownStructure() { + String summary = "### `Test`\n" + + "
\n" + + "Build log\n" + + "```\n" + + "Line 1\n" + + "Line 2\n" + + "Line 3\n" + + "```\n" + + "
\n"; + + ChecksOutput checksOutput = new ChecksOutputBuilder() + .withSummary(summary) + .build(); + + String truncated = checksOutput.getSummary(100).orElse(""); + assertThat(truncated) + .contains("
") + .contains("
") + .contains("```\n") + .endsWith("```\n
\n") + .hasSizeLessThanOrEqualTo(100); + } private List createAnnotations() { final ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() From 861bcf5f8012d58199b27df3ee6201d151608c76 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 11:13:57 -0800 Subject: [PATCH 03/13] revert text change --- .../java/io/jenkins/plugins/checks/api/ChecksOutput.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index a50f210d..b361e9d8 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -76,13 +76,7 @@ public Optional getText() { * @return Text, truncated to maxSize with truncation message if appropriate. */ public Optional getText(final int maxSize) { - return Optional.ofNullable(text).map(s -> new TruncatedString.Builder() - .setChunkOnNewlines() - .setTruncateStart() - .withTruncationText("Output truncated.\n") - .addText(s.toString()) - .build() - .buildByChars(maxSize)); + return Optional.ofNullable(text).map(s -> s.build(maxSize)); } public List getChecksAnnotations() { From 01fd7971a7dadafa22230a0fe9e450e49ecef35a Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 11:29:59 -0800 Subject: [PATCH 04/13] adjust tests --- .../plugins/checks/api/ChecksOutputTest.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java index c09ed8fe..f53ed4f5 100644 --- a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java +++ b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java @@ -151,24 +151,30 @@ void shouldHandleNullSummary() { } @Test - void shouldNotTruncateSummaryWithoutBuildLog() { - String summary = "### Simple Summary\nWithout any build logs\n"; - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withSummary(summary) - .build(); - - assertThat(checksOutput.getSummary(100).orElse("")) - .isEqualTo(summary); - } + void shouldTruncateSummaryFromEndWithoutBuildLog() { + String summary = "### Test Results\n" + + "Found 5 test failures in the build:\n" + + "- `TestClass1.testMethod1`: Assertion failed, expected true but was false\n" + + "- `TestClass2.testMethod2`: NullPointerException at line 42\n" + + "- `TestClass3.testMethod3`: Expected exception was not thrown\n" + + "- `TestClass4.testMethod4`: Timeout after 5 seconds\n" + + "- `TestClass5.testMethod5`: Invalid test data\n"; - @Test - void shouldHandleMalformedBuildLog() { - String summary = "### Header\n
\nMalformed log without closing tags"; ChecksOutput checksOutput = new ChecksOutputBuilder() .withSummary(summary) .build(); - assertThat(checksOutput.getSummary(100).orElse("")) + // Test with a size that only fits the header and first failure + String truncated = checksOutput.getSummary(70).orElse(""); + assertThat(truncated) + .startsWith("### Test Results\n") + .contains("Found 5 test failures") + .doesNotContain("TestClass5.testMethod5") + .contains("Output truncated.") + .hasSizeLessThanOrEqualTo(70); + + // Verify that with sufficient size, we get the full content + assertThat(checksOutput.getSummary(500).orElse("")) .isEqualTo(summary); } From b29ecd3f31bbc3e9b769919464c652a293f3b0f6 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 11:38:42 -0800 Subject: [PATCH 05/13] checkstyle --- .../jenkins/plugins/checks/api/ChecksOutput.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index b361e9d8..dc55ec78 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -101,18 +101,18 @@ public String toString() { /** * Truncates the summary to the given maxSize. Tries to truncate from start of build log section if possible. * - * @param summary the summary to truncate + * @param summaryToTruncate the summary to truncate * @param maxSize the maximum size to truncate to * @return the truncated summary */ - private Optional truncateSummary(final TruncatedString summary, final int maxSize) { - if (summary == null) { + private Optional truncateSummary(final TruncatedString summaryToTruncate, final int maxSize) { + if (summaryToTruncate == null) { return Optional.empty(); } - String content = summary.toString(); + String content = summaryToTruncate.toString(); if (!content.contains("")) { - return Optional.of(summary.build(maxSize)); + return Optional.of(summaryToTruncate.build(maxSize)); } // Find the build log section @@ -120,7 +120,7 @@ private Optional truncateSummary(final TruncatedString summary, final in int detailsEnd = content.indexOf("
") + "
".length(); if (detailsStart == -1 || detailsEnd == -1) { - return Optional.of(summary.build(maxSize)); + return Optional.of(summaryToTruncate.build(maxSize)); } // Split into pre-details, details block, and post-details @@ -133,7 +133,7 @@ private Optional truncateSummary(final TruncatedString summary, final in int logEnd = details.lastIndexOf("\n```"); if (logStart == -1 || logEnd == -1) { - return Optional.of(summary.build(maxSize)); + return Optional.of(summaryToTruncate.build(maxSize)); } String beforeLog = details.substring(0, logStart); @@ -146,7 +146,7 @@ private Optional truncateSummary(final TruncatedString summary, final in if (availableForLog <= 0) { // If no space for log, truncate the whole content - return Optional.of(summary.build(maxSize)); + return Optional.of(summaryToTruncate.build(maxSize)); } // Truncate the log using TruncatedString From 7a18a81e189f073831faaef3f2831f6509f9b32c Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 11:44:56 -0800 Subject: [PATCH 06/13] fix spotbugs --- .../java/io/jenkins/plugins/checks/api/ChecksOutput.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index dc55ec78..475e0829 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -62,6 +62,9 @@ public Optional getSummary() { * @return Summary, truncated to maxSize with truncation message if appropriate. */ public Optional getSummary(final int maxSize) { + if (summary == null) { + return Optional.empty(); + } return truncateSummary(summary, maxSize); } @@ -106,10 +109,6 @@ public String toString() { * @return the truncated summary */ private Optional truncateSummary(final TruncatedString summaryToTruncate, final int maxSize) { - if (summaryToTruncate == null) { - return Optional.empty(); - } - String content = summaryToTruncate.toString(); if (!content.contains("")) { return Optional.of(summaryToTruncate.build(maxSize)); From 129876221ebda7118392e5ca4afd2fc35c3d042b Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 16:39:19 -0800 Subject: [PATCH 07/13] move implementation to flowexecutionanalyzer --- .../plugins/checks/api/ChecksOutput.java | 66 +-------- .../checks/status/FlowExecutionAnalyzer.java | 17 ++- .../plugins/checks/api/ChecksOutputTest.java | 134 ------------------ 3 files changed, 15 insertions(+), 202 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index 475e0829..ae09bb10 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -62,10 +62,7 @@ public Optional getSummary() { * @return Summary, truncated to maxSize with truncation message if appropriate. */ public Optional getSummary(final int maxSize) { - if (summary == null) { - return Optional.empty(); - } - return truncateSummary(summary, maxSize); + return Optional.ofNullable(summary).map(s -> s.build(maxSize)); } public Optional getText() { @@ -101,67 +98,6 @@ public String toString() { + '}'; } - /** - * Truncates the summary to the given maxSize. Tries to truncate from start of build log section if possible. - * - * @param summaryToTruncate the summary to truncate - * @param maxSize the maximum size to truncate to - * @return the truncated summary - */ - private Optional truncateSummary(final TruncatedString summaryToTruncate, final int maxSize) { - String content = summaryToTruncate.toString(); - if (!content.contains("")) { - return Optional.of(summaryToTruncate.build(maxSize)); - } - - // Find the build log section - int detailsStart = content.indexOf("
"); - int detailsEnd = content.indexOf("
") + "".length(); - - if (detailsStart == -1 || detailsEnd == -1) { - return Optional.of(summaryToTruncate.build(maxSize)); - } - - // Split into pre-details, details block, and post-details - String preDetails = content.substring(0, detailsStart); - String details = content.substring(detailsStart, detailsEnd); - String postDetails = content.substring(detailsEnd); - - // Find the actual log content within the details - int logStart = details.indexOf("```\n") + 4; - int logEnd = details.lastIndexOf("\n```"); - - if (logStart == -1 || logEnd == -1) { - return Optional.of(summaryToTruncate.build(maxSize)); - } - - String beforeLog = details.substring(0, logStart); - String log = details.substring(logStart, logEnd); - String afterLog = details.substring(logEnd); - - // Calculate available space for log - int nonLogLength = preDetails.length() + beforeLog.length() + afterLog.length() + postDetails.length(); - int availableForLog = maxSize - nonLogLength; - - if (availableForLog <= 0) { - // If no space for log, truncate the whole content - return Optional.of(summaryToTruncate.build(maxSize)); - } - - // Truncate the log using TruncatedString - log = new TruncatedString.Builder() - .setChunkOnNewlines() - .setTruncateStart() - .withTruncationText("Build log truncated.\n") - .addText(log) - .build() - .buildByChars(availableForLog); - - // Reconstruct the content - String truncatedContent = preDetails + beforeLog + log + afterLog + postDetails; - return Optional.of(truncatedContent); - } - /** * Builder for {@link ChecksOutput}. */ 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 aec20c32..2f4a5a2d 100644 --- a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java +++ b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java @@ -36,6 +36,7 @@ class FlowExecutionAnalyzer { private static final Logger LOGGER = Logger.getLogger(FlowExecutionAnalyzer.class.getName()); private static final String TRUNCATED_MESSAGE = "\n\nOutput truncated."; + private static final int MAX_MESSAGE_SIZE_TO_CHECKS_API = 65_535; private final Run run; private final FlowExecution execution; @@ -132,7 +133,9 @@ private Pair processErrorOrWarningRow(final FlowGraphTable.Row r nodeTextBuilder.append(String.format("**Error**: *%s*", displayName)); nodeSummaryBuilder.append(String.format("```%n%s%n```%n", displayName)); if (!suppressLogs) { - String log = getLog(flowNode); + // -2 for "\n\n" at the end of the summary + int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - 2; + String log = getLog(flowNode, maxMessageSize); if (StringUtils.isNotBlank(log)) { nodeSummaryBuilder.append(String.format("
%nBuild log%n%n```%n%s%n```%n
", log)); } @@ -197,7 +200,7 @@ private String getPotentialTitle(final FlowNode flowNode, final ErrorAction erro } @CheckForNull - private static String getLog(final FlowNode flowNode) { + private static String getLog(final FlowNode flowNode, final int maxMessageSize) { LogAction logAction = flowNode.getAction(LogAction.class); if (logAction == null) { return null; @@ -209,7 +212,15 @@ private static String getLog(final FlowNode flowNode) { String outputString = out.toString(StandardCharsets.UTF_8); // strip ansi color codes - return outputString.replaceAll("\u001B\\[[;\\d]*m", ""); + String log = outputString.replaceAll("\u001B\\[[;\\d]*m", ""); + + return new TruncatedString.Builder() + .setChunkOnNewlines() + .setTruncateStart() + .withTruncationText(TRUNCATED_MESSAGE) + .addText(log) + .build() + .build(maxMessageSize); } catch (IOException e) { LOGGER.log(Level.WARNING, String.format("Failed to extract logs for step '%s'", diff --git a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java index f53ed4f5..e343e015 100644 --- a/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java +++ b/src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java @@ -95,140 +95,6 @@ void shouldCopyConstructCorrectly() { .containsExactlyInAnyOrderElementsOf(images); } - @Test - void shouldTruncateSummaryLogFromStart() { - String summary = "### `Fails / Shell Script`\n" - + "Error in `sh` step.\n" - + "```\n" - + "script returned exit code 1\n" - + "```\n" - + "
\n" - + "Build log\n" - + "\n" - + "```\n" - + "+ echo 'First line of log'\n" - + "First line of log\n" - + "+ echo 'Second line of log'\n" - + "Second line of log\n" - + "+ echo 'Third line of log'\n" - + "Third line of log\n" - + "+ exit 1\n" - + "```\n" - + "
\n" - + "\n"; - int maxSize = 200; - - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withSummary(summary) - .build(); - - String truncated = checksOutput.getSummary(maxSize).orElse(""); - String expected = "### `Fails / Shell Script`\n" - + "Error in `sh` step.\n" - + "```\n" - + "script returned exit code 1\n" - + "```\n" - + "
\n" - + "Build log\n" - + "\n" - + "```\n" - + "Build log truncated.\n" - + "Third line of log\n" - + "+ exit 1\n" - + "```\n" - + "
\n" - + "\n"; - - assertThat(truncated) - .isEqualTo(expected); - assertThat(truncated.length()).isLessThanOrEqualTo(maxSize); - } - - @Test - void shouldHandleNullSummary() { - ChecksOutput checksOutput = new ChecksOutputBuilder().build(); - assertThat(checksOutput.getSummary(100)).isEmpty(); - } - - @Test - void shouldTruncateSummaryFromEndWithoutBuildLog() { - String summary = "### Test Results\n" - + "Found 5 test failures in the build:\n" - + "- `TestClass1.testMethod1`: Assertion failed, expected true but was false\n" - + "- `TestClass2.testMethod2`: NullPointerException at line 42\n" - + "- `TestClass3.testMethod3`: Expected exception was not thrown\n" - + "- `TestClass4.testMethod4`: Timeout after 5 seconds\n" - + "- `TestClass5.testMethod5`: Invalid test data\n"; - - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withSummary(summary) - .build(); - - // Test with a size that only fits the header and first failure - String truncated = checksOutput.getSummary(70).orElse(""); - assertThat(truncated) - .startsWith("### Test Results\n") - .contains("Found 5 test failures") - .doesNotContain("TestClass5.testMethod5") - .contains("Output truncated.") - .hasSizeLessThanOrEqualTo(70); - - // Verify that with sufficient size, we get the full content - assertThat(checksOutput.getSummary(500).orElse("")) - .isEqualTo(summary); - } - - @Test - void shouldHandleVerySmallMaxSize() { - String summary = "### `Fails / Shell Script`\n" - + "
\n" - + "Build log\n" - + "```\n" - + "+ echo 'First line'\n" - + "First line\n" - + "```\n" - + "
\n"; - - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withSummary(summary) - .build(); - - // Size so small that only header and truncation message fit - String truncated = checksOutput.getSummary(50).orElse(""); - assertThat(truncated) - .startsWith("### `Fails / Shell Script`\n") - .doesNotContain("
") - .doesNotContain("Build log") - .doesNotContain("Build log truncated.") - .contains("Output truncated.") - .hasSizeLessThanOrEqualTo(50); - } - - @Test - void shouldPreserveMarkdownStructure() { - String summary = "### `Test`\n" - + "
\n" - + "Build log\n" - + "```\n" - + "Line 1\n" - + "Line 2\n" - + "Line 3\n" - + "```\n" - + "
\n"; - - ChecksOutput checksOutput = new ChecksOutputBuilder() - .withSummary(summary) - .build(); - - String truncated = checksOutput.getSummary(100).orElse(""); - assertThat(truncated) - .contains("
") - .contains("
") - .contains("```\n") - .endsWith("```\n
\n") - .hasSizeLessThanOrEqualTo(100); - } - private List createAnnotations() { final ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() .withPath("src/main/java/1.java") From 88f7eb2f0af7d7e56783f2c5b909482884323a2c Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 17:02:07 -0800 Subject: [PATCH 08/13] move logic to the flow execution analyzer --- .../checks/status/FlowExecutionAnalyzer.java | 3 +- .../BuildStatusChecksPublisherITest.java | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) 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 2f4a5a2d..1f076597 100644 --- a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java +++ b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java @@ -36,6 +36,7 @@ class FlowExecutionAnalyzer { private static final Logger LOGGER = Logger.getLogger(FlowExecutionAnalyzer.class.getName()); private static final String TRUNCATED_MESSAGE = "\n\nOutput truncated."; + private static final String TRUNCATED_MESSAGE_BUILD_LOG = "Build log truncated.\n\n"; private static final int MAX_MESSAGE_SIZE_TO_CHECKS_API = 65_535; private final Run run; @@ -217,7 +218,7 @@ private static String getLog(final FlowNode flowNode, final int maxMessageSize) return new TruncatedString.Builder() .setChunkOnNewlines() .setTruncateStart() - .withTruncationText(TRUNCATED_MESSAGE) + .withTruncationText(TRUNCATED_MESSAGE_BUILD_LOG) .addText(log) .build() .build(maxMessageSize); 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 3e64b049..dc137e5f 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -297,6 +297,54 @@ public void shouldPublishStageDetailsWithoutLogsIfRequested() { }); } + /** + * Test that log messages are properly truncated when they exceed the maximum size limit. + */ + @Test + public void shouldTruncateLogsWhenExceedingMaxSize() { + getProperties().setApplicable(true); + getProperties().setSkipped(false); + getProperties().setName("Test Status"); + getProperties().setSuppressLogs(false); + WorkflowJob job = createPipeline(); + + // Create a pipeline that generates a large log output + job.setDefinition(new CpsFlowDefinition("" + + "node {\n" + + " stage('Large Log Stage') {\n" + + " // Generate a large log by executing shell commands\n" + + " sh '''\n" + + " for i in {1..1000}; do\n" + + " echo \"Line $i: $(date) - This is a very long log line that will be repeated many times to test truncation. Adding some extra system information here.\"\n" + + " done\n" + + " exit 1\n" + + " '''\n" + + " error('Pipeline failed with large logs')\n" + + " }\n" + + "}", true)); + + buildWithResult(job, Result.FAILURE); + + List checksDetails = getFactory().getPublishedChecks(); + + // Get the final check details which should contain the truncated logs + ChecksDetails details = checksDetails.get(checksDetails.size() - 1); + assertThat(details.getStatus()).isEqualTo(ChecksStatus.COMPLETED); + assertThat(details.getConclusion()).isEqualTo(ChecksConclusion.FAILURE); + assertThat(details.getOutput()).isPresent().get().satisfies(output -> { + assertThat(output.getSummary()).isPresent().get().satisfies(summary -> { + // Verify the log section exists and is truncated + assertThat(summary).contains("
"); + assertThat(summary).contains("Build log"); + assertThat(summary).contains("Build log truncated."); + // Verify the truncation message appears at the start of the log section to show that truncation occurred at start + assertThat(summary).matches(Pattern.compile(".*Build log\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL)); + // Verify the total size is within limits + assertThat(summary.length()).isLessThanOrEqualTo(65535); + }); + }); + } + /** * Validates that a simple successful pipeline works. */ From c48c52150b2c75af5e2ffc0f4def36fb40d48251 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 17:03:25 -0800 Subject: [PATCH 09/13] fix lint --- .../plugins/checks/status/BuildStatusChecksPublisherITest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dc137e5f..05d3a897 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -340,7 +340,7 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { // Verify the truncation message appears at the start of the log section to show that truncation occurred at start assertThat(summary).matches(Pattern.compile(".*Build log\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL)); // Verify the total size is within limits - assertThat(summary.length()).isLessThanOrEqualTo(65535); + assertThat(summary.length()).isLessThanOrEqualTo(65_535); }); }); } From a6344865a6b7a669b98c0183b183b180d0ecd57e Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 17:05:08 -0800 Subject: [PATCH 10/13] test that end log exists --- .../plugins/checks/status/BuildStatusChecksPublisherITest.java | 2 ++ 1 file changed, 2 insertions(+) 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 05d3a897..0aff449e 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -317,6 +317,7 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { + " for i in {1..1000}; do\n" + " echo \"Line $i: $(date) - This is a very long log line that will be repeated many times to test truncation. Adding some extra system information here.\"\n" + " done\n" + + " echo 'Generating some additional system logs...'\n" + " exit 1\n" + " '''\n" + " error('Pipeline failed with large logs')\n" @@ -337,6 +338,7 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { assertThat(summary).contains("
"); assertThat(summary).contains("Build log"); assertThat(summary).contains("Build log truncated."); + assertThat(summary).contains("Generating some additional system logs..."); // Verify the truncation message appears at the start of the log section to show that truncation occurred at start assertThat(summary).matches(Pattern.compile(".*Build log\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL)); // Verify the total size is within limits From 82a224c51a4d33d5132e90f0d5d9946174c83d70 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 17:15:42 -0800 Subject: [PATCH 11/13] adjust script --- .../status/BuildStatusChecksPublisherITest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 0aff449e..fa0d7a5b 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -313,13 +313,14 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { + "node {\n" + " stage('Large Log Stage') {\n" + " // Generate a large log by executing shell commands\n" - + " sh '''\n" - + " for i in {1..1000}; do\n" - + " echo \"Line $i: $(date) - This is a very long log line that will be repeated many times to test truncation. Adding some extra system information here.\"\n" - + " done\n" + + " def logContent = (1..1000).collect { i ->\n" + + " \"Line ${i}: This is a very long log line that will be repeated many times to test truncation. Adding some extra system information here.\"\n" + + " }.join('\\n')\n" + + " sh \"\"\"\n" + + " echo \"${logContent}\"\n" + " echo 'Generating some additional system logs...'\n" + " exit 1\n" - + " '''\n" + + " \"\"\"\n" + " error('Pipeline failed with large logs')\n" + " }\n" + "}", true)); From a169226d770b82f687368a309536c8b7b3dace87 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Fri, 14 Feb 2025 17:31:05 -0800 Subject: [PATCH 12/13] crossplatform support, bug fix --- .../checks/status/FlowExecutionAnalyzer.java | 5 +++-- .../BuildStatusChecksPublisherITest.java | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 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 1f076597..5f9e36ec 100644 --- a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java +++ b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java @@ -135,10 +135,11 @@ private Pair processErrorOrWarningRow(final FlowGraphTable.Row r nodeSummaryBuilder.append(String.format("```%n%s%n```%n", displayName)); if (!suppressLogs) { // -2 for "\n\n" at the end of the summary - int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - 2; + String logTemplate = "
%nBuild log%n%n```%n%s%n```%n
"; + int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - logTemplate.length() - 2; String log = getLog(flowNode, maxMessageSize); if (StringUtils.isNotBlank(log)) { - nodeSummaryBuilder.append(String.format("
%nBuild log%n%n```%n%s%n```%n
", log)); + nodeSummaryBuilder.append(String.format(logTemplate, log)); } } } 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 fa0d7a5b..7be6d9b5 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -312,15 +312,17 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { job.setDefinition(new CpsFlowDefinition("" + "node {\n" + " stage('Large Log Stage') {\n" - + " // Generate a large log by executing shell commands\n" + + " // Generate a large log using Jenkins' built-in commands\n" + " def logContent = (1..1000).collect { i ->\n" + " \"Line ${i}: This is a very long log line that will be repeated many times to test truncation. Adding some extra system information here.\"\n" + " }.join('\\n')\n" - + " sh \"\"\"\n" - + " echo \"${logContent}\"\n" - + " echo 'Generating some additional system logs...'\n" - + " exit 1\n" - + " \"\"\"\n" + + " // Use writeFile and bat/sh based on platform\n" + + " writeFile file: 'large_log.txt', text: logContent\n" + + " if (isUnix()) {\n" + + " sh 'cat large_log.txt && exit 1'\n" + + " } else {\n" + + " bat 'type large_log.txt && exit /b 1'\n" + + " }\n" + " error('Pipeline failed with large logs')\n" + " }\n" + "}", true)); @@ -339,8 +341,9 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { assertThat(summary).contains("
"); assertThat(summary).contains("Build log"); assertThat(summary).contains("Build log truncated."); - assertThat(summary).contains("Generating some additional system logs..."); - // Verify the truncation message appears at the start of the log section to show that truncation occurred at start + assertThat(summary).doesNotContain("Line 1:"); // Should be truncated from the start + assertThat(summary).contains("exit"); // Should see the exit command at the end + // Verify the truncation message appears at the start of the log section assertThat(summary).matches(Pattern.compile(".*Build log\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL)); // Verify the total size is within limits assertThat(summary.length()).isLessThanOrEqualTo(65_535); From 7ebe6542a6e9bda21e5d6d5153135301365ec9e4 Mon Sep 17 00:00:00 2001 From: Sarah Deitke Date: Sun, 16 Feb 2025 09:27:15 -0800 Subject: [PATCH 13/13] add buffer, additional assertion --- .../jenkins/plugins/checks/status/FlowExecutionAnalyzer.java | 4 ++-- .../checks/status/BuildStatusChecksPublisherITest.java | 1 + 2 files changed, 3 insertions(+), 2 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 5f9e36ec..e55d0aa5 100644 --- a/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java +++ b/src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java @@ -134,9 +134,9 @@ private Pair processErrorOrWarningRow(final FlowGraphTable.Row r nodeTextBuilder.append(String.format("**Error**: *%s*", displayName)); nodeSummaryBuilder.append(String.format("```%n%s%n```%n", displayName)); if (!suppressLogs) { - // -2 for "\n\n" at the end of the summary + // -2 for "\n\n" at the end of the summary and -30 for buffer String logTemplate = "
%nBuild log%n%n```%n%s%n```%n
"; - int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - logTemplate.length() - 2; + int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - logTemplate.length() - 32; String log = getLog(flowNode, maxMessageSize); if (StringUtils.isNotBlank(log)) { nodeSummaryBuilder.append(String.format(logTemplate, log)); 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 7be6d9b5..05dbe894 100644 --- a/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java @@ -339,6 +339,7 @@ public void shouldTruncateLogsWhenExceedingMaxSize() { assertThat(output.getSummary()).isPresent().get().satisfies(summary -> { // Verify the log section exists and is truncated assertThat(summary).contains("
"); + assertThat(summary).contains("
"); assertThat(summary).contains("Build log"); assertThat(summary).contains("Build log truncated."); assertThat(summary).doesNotContain("Line 1:"); // Should be truncated from the start