Skip to content

Commit bfdc7df

Browse files
authored
Changed the truncate summary to cut build logs from top (#272)
1 parent 6194699 commit bfdc7df

File tree

4 files changed

+79
-48
lines changed

4 files changed

+79
-48
lines changed

src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,7 @@ public Optional<String> getText() {
7676
* @return Text, truncated to maxSize with truncation message if appropriate.
7777
*/
7878
public Optional<String> getText(final int maxSize) {
79-
return Optional.ofNullable(text)
80-
.map(s -> new TruncatedString.Builder()
81-
.setChunkOnNewlines()
82-
.setTruncateStart()
83-
.addText(s.toString())
84-
.build()
85-
.buildByChars(maxSize));
79+
return Optional.ofNullable(text).map(s -> s.build(maxSize));
8680
}
8781

8882
public List<ChecksAnnotation> getChecksAnnotations() {

src/main/java/io/jenkins/plugins/checks/status/FlowExecutionAnalyzer.java

+17-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
class FlowExecutionAnalyzer {
3737
private static final Logger LOGGER = Logger.getLogger(FlowExecutionAnalyzer.class.getName());
3838
private static final String TRUNCATED_MESSAGE = "\n\nOutput truncated.";
39+
private static final String TRUNCATED_MESSAGE_BUILD_LOG = "Build log truncated.\n\n";
40+
private static final int MAX_MESSAGE_SIZE_TO_CHECKS_API = 65_535;
3941

4042
private final Run<?, ?> run;
4143
private final FlowExecution execution;
@@ -132,9 +134,12 @@ private Pair<String, String> processErrorOrWarningRow(final FlowGraphTable.Row r
132134
nodeTextBuilder.append(String.format("**Error**: *%s*", displayName));
133135
nodeSummaryBuilder.append(String.format("```%n%s%n```%n", displayName));
134136
if (!suppressLogs) {
135-
String log = getLog(flowNode);
137+
// -2 for "\n\n" at the end of the summary and -30 for buffer
138+
String logTemplate = "<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>";
139+
int maxMessageSize = MAX_MESSAGE_SIZE_TO_CHECKS_API - nodeSummaryBuilder.length() - logTemplate.length() - 32;
140+
String log = getLog(flowNode, maxMessageSize);
136141
if (StringUtils.isNotBlank(log)) {
137-
nodeSummaryBuilder.append(String.format("<details>%n<summary>Build log</summary>%n%n```%n%s%n```%n</details>", log));
142+
nodeSummaryBuilder.append(String.format(logTemplate, log));
138143
}
139144
}
140145
}
@@ -197,7 +202,7 @@ private String getPotentialTitle(final FlowNode flowNode, final ErrorAction erro
197202
}
198203

199204
@CheckForNull
200-
private static String getLog(final FlowNode flowNode) {
205+
private static String getLog(final FlowNode flowNode, final int maxMessageSize) {
201206
LogAction logAction = flowNode.getAction(LogAction.class);
202207
if (logAction == null) {
203208
return null;
@@ -209,7 +214,15 @@ private static String getLog(final FlowNode flowNode) {
209214

210215
String outputString = out.toString(StandardCharsets.UTF_8);
211216
// strip ansi color codes
212-
return outputString.replaceAll("\u001B\\[[;\\d]*m", "");
217+
String log = outputString.replaceAll("\u001B\\[[;\\d]*m", "");
218+
219+
return new TruncatedString.Builder()
220+
.setChunkOnNewlines()
221+
.setTruncateStart()
222+
.withTruncationText(TRUNCATED_MESSAGE_BUILD_LOG)
223+
.addText(log)
224+
.build()
225+
.build(maxMessageSize);
213226
}
214227
catch (IOException e) {
215228
LOGGER.log(Level.WARNING, String.format("Failed to extract logs for step '%s'",

src/test/java/io/jenkins/plugins/checks/api/ChecksOutputTest.java

+6-37
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ void shouldBuildCorrectlyWithAllFields() {
3030
.addImage(images.get(1))
3131
.build();
3232

33-
ChecksOutputAssert.assertThat(checksOutput)
34-
.hasTitle(Optional.of(TITLE))
35-
.hasSummary(Optional.of(SUMMARY))
36-
.hasText(Optional.of(TEXT));
33+
assertThat(checksOutput.getTitle()).isEqualTo(Optional.of(TITLE));
34+
assertThat(checksOutput.getSummary()).isEqualTo(Optional.of(SUMMARY));
35+
assertThat(checksOutput.getText()).isEqualTo(Optional.of(TEXT));
3736
assertThat(checksOutput.getChecksAnnotations())
3837
.usingFieldByFieldElementComparator()
3938
.containsExactlyInAnyOrderElementsOf(annotations);
@@ -85,10 +84,9 @@ void shouldCopyConstructCorrectly() {
8584
.build();
8685

8786
ChecksOutput copied = new ChecksOutput(checksOutput);
88-
ChecksOutputAssert.assertThat(copied)
89-
.hasTitle(Optional.of(TITLE))
90-
.hasSummary(Optional.of(SUMMARY))
91-
.hasText(Optional.of(TEXT));
87+
assertThat(copied.getTitle()).isEqualTo(Optional.of(TITLE));
88+
assertThat(copied.getSummary()).isEqualTo(Optional.of(SUMMARY));
89+
assertThat(copied.getText()).isEqualTo(Optional.of(TEXT));
9290
assertThat(copied.getChecksAnnotations())
9391
.usingFieldByFieldElementComparator()
9492
.containsExactlyInAnyOrderElementsOf(annotations);
@@ -97,35 +95,6 @@ void shouldCopyConstructCorrectly() {
9795
.containsExactlyInAnyOrderElementsOf(images);
9896
}
9997

100-
@Test
101-
void shouldTruncateTextFromStart() {
102-
String longText = "This is the beginning.\n" + "Middle part.\n".repeat(10) + "This is the end.\n";
103-
ChecksOutput checksOutput = new ChecksOutputBuilder()
104-
.withText(longText)
105-
.build();
106-
107-
String truncated = checksOutput.getText(75).orElse("");
108-
109-
assertThat(truncated)
110-
.startsWith("Output truncated.")
111-
.endsWith("This is the end.\n");
112-
assertThat(truncated.length()).isLessThanOrEqualTo(75);
113-
}
114-
115-
@Test
116-
void shouldNotTruncateShortText() {
117-
String shortText = "This is a short text that should not be truncated.";
118-
ChecksOutput checksOutput = new ChecksOutputBuilder()
119-
.withText(shortText)
120-
.build();
121-
122-
String result = checksOutput.getText(100).orElse("");
123-
124-
assertThat(result)
125-
.isEqualTo(shortText)
126-
.doesNotContain("Output truncated.");
127-
}
128-
12998
private List<ChecksAnnotation> createAnnotations() {
13099
final ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder()
131100
.withPath("src/main/java/1.java")

src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java

+55
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,61 @@ public void shouldPublishStageDetailsWithoutLogsIfRequested() {
297297
});
298298
}
299299

300+
/**
301+
* Test that log messages are properly truncated when they exceed the maximum size limit.
302+
*/
303+
@Test
304+
public void shouldTruncateLogsWhenExceedingMaxSize() {
305+
getProperties().setApplicable(true);
306+
getProperties().setSkipped(false);
307+
getProperties().setName("Test Status");
308+
getProperties().setSuppressLogs(false);
309+
WorkflowJob job = createPipeline();
310+
311+
// Create a pipeline that generates a large log output
312+
job.setDefinition(new CpsFlowDefinition(""
313+
+ "node {\n"
314+
+ " stage('Large Log Stage') {\n"
315+
+ " // Generate a large log using Jenkins' built-in commands\n"
316+
+ " def logContent = (1..1000).collect { i ->\n"
317+
+ " \"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"
318+
+ " }.join('\\n')\n"
319+
+ " // Use writeFile and bat/sh based on platform\n"
320+
+ " writeFile file: 'large_log.txt', text: logContent\n"
321+
+ " if (isUnix()) {\n"
322+
+ " sh 'cat large_log.txt && exit 1'\n"
323+
+ " } else {\n"
324+
+ " bat 'type large_log.txt && exit /b 1'\n"
325+
+ " }\n"
326+
+ " error('Pipeline failed with large logs')\n"
327+
+ " }\n"
328+
+ "}", true));
329+
330+
buildWithResult(job, Result.FAILURE);
331+
332+
List<ChecksDetails> checksDetails = getFactory().getPublishedChecks();
333+
334+
// Get the final check details which should contain the truncated logs
335+
ChecksDetails details = checksDetails.get(checksDetails.size() - 1);
336+
assertThat(details.getStatus()).isEqualTo(ChecksStatus.COMPLETED);
337+
assertThat(details.getConclusion()).isEqualTo(ChecksConclusion.FAILURE);
338+
assertThat(details.getOutput()).isPresent().get().satisfies(output -> {
339+
assertThat(output.getSummary()).isPresent().get().satisfies(summary -> {
340+
// Verify the log section exists and is truncated
341+
assertThat(summary).contains("<details>");
342+
assertThat(summary).contains("</details>");
343+
assertThat(summary).contains("Build log");
344+
assertThat(summary).contains("Build log truncated.");
345+
assertThat(summary).doesNotContain("Line 1:"); // Should be truncated from the start
346+
assertThat(summary).contains("exit"); // Should see the exit command at the end
347+
// Verify the truncation message appears at the start of the log section
348+
assertThat(summary).matches(Pattern.compile(".*<summary>Build log</summary>\\s+\\n```\\s*\\nBuild log truncated.\\n\\n.*", Pattern.DOTALL));
349+
// Verify the total size is within limits
350+
assertThat(summary.length()).isLessThanOrEqualTo(65_535);
351+
});
352+
});
353+
}
354+
300355
/**
301356
* Validates that a simple successful pipeline works.
302357
*/

0 commit comments

Comments
 (0)