Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed the truncate summary to cut build logs from top #272

Merged
merged 13 commits into from
Feb 16, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,7 @@
* @return Text, truncated to maxSize with truncation message if appropriate.
*/
public Optional<String> 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 -> s.build(maxSize));

Check warning on line 79 in src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 79 is not covered by tests

Check warning on line 79 in src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:compile

NORMAL: build(int) in io.jenkins.plugins.checks.api.TruncatedString has been deprecated
}

public List<ChecksAnnotation> getChecksAnnotations() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
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;
private final FlowExecution execution;
Expand Down Expand Up @@ -132,9 +134,12 @@
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
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't it be approx -70 (or even -100 to be safe?)

echo '<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>' | wc
       1       2      68

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are looking at an older version of the code, I have included the log format in the max size. I added an additional 30 character buffer to be safe though

                // -2 for "\n\n" at the end of the summary
                String logTemplate = "<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>";
                int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - logTemplate.length() - 2;

String logTemplate = "<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>";
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("<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>", log));
nodeSummaryBuilder.append(String.format(logTemplate, log));
}
}
}
Expand Down Expand Up @@ -197,7 +202,7 @@
}

@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;
Expand All @@ -209,7 +214,15 @@

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_BUILD_LOG)
.addText(log)
.build()
.build(maxMessageSize);

Check warning on line 225 in src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:compile

NORMAL: build(int) in io.jenkins.plugins.checks.api.TruncatedString has been deprecated
}
catch (IOException e) {
LOGGER.log(Level.WARNING, String.format("Failed to extract logs for step '%s'",
Expand Down
43 changes: 6 additions & 37 deletions src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@
.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()

Check warning on line 37 in src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:testCompile

NORMAL: usingFieldByFieldElementComparator() in org.assertj.core.api.AbstractIterableAssert has been deprecated
.containsExactlyInAnyOrderElementsOf(annotations);
assertThat(checksOutput.getChecksImages())
.usingFieldByFieldElementComparator()
Expand Down Expand Up @@ -85,47 +84,17 @@
.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()

Check warning on line 91 in src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:testCompile

NORMAL: usingFieldByFieldElementComparator() in org.assertj.core.api.AbstractIterableAssert has been deprecated
.containsExactlyInAnyOrderElementsOf(annotations);
assertThat(copied.getChecksImages())
.usingFieldByFieldElementComparator()
.containsExactlyInAnyOrderElementsOf(images);
}

@Test
void shouldTruncateTextFromStart() {
String longText = "This is the beginning.\n" + "Middle part.\n".repeat(10) + "This is the end.\n";
ChecksOutput checksOutput = new ChecksOutputBuilder()
.withText(longText)
.build();

String truncated = checksOutput.getText(75).orElse("");

assertThat(truncated)
.startsWith("Output truncated.")
.endsWith("This is the end.\n");
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();

String result = checksOutput.getText(100).orElse("");

assertThat(result)
.isEqualTo(shortText)
.doesNotContain("Output truncated.");
}

private List<ChecksAnnotation> createAnnotations() {
final ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder()
.withPath("src/main/java/1.java")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,60 @@ 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 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"
+ " // 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));

buildWithResult(job, Result.FAILURE);

List<ChecksDetails> 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("<details>");
Copy link
Member

Choose a reason for hiding this comment

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

could you also check for </details> to make sure its not dropped?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

assertThat(summary).contains("Build log");
assertThat(summary).contains("Build log truncated.");
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(".*<summary>Build log</summary>\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL));
// Verify the total size is within limits
assertThat(summary.length()).isLessThanOrEqualTo(65_535);
});
});
}

/**
* Validates that a simple successful pipeline works.
*/
Expand Down