Skip to content

Commit ebad632

Browse files
authored
ExceptionWithLogs type for gradle exec tasks (#118)
ExceptionWithLogs type for gradle exec tasks
1 parent 3230fcf commit ebad632

File tree

24 files changed

+302
-106
lines changed

24 files changed

+302
-106
lines changed

changelog/@unreleased/pr-118.v2.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type: improvement
2+
improvement:
3+
description: ExceptionWithLogs type for gradle exec tasks
4+
links:
5+
- https://github.com/palantir/gradle-failure-reports/pull/118
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apply plugin: 'groovy'
2+
apply plugin: 'com.palantir.external-publish-jar'
3+
4+
5+
dependencies {
6+
annotationProcessor 'org.immutables:value'
7+
compileOnly 'org.immutables:value::annotations'
8+
9+
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
10+
implementation 'com.google.guava:guava'
11+
}

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/FailureReport.java gradle-failure-reports-common/src/main/java/com/palantir/gradle/failurereports/common/FailureReport.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.palantir.gradle.failurereports;
17+
package com.palantir.gradle.failurereports.common;
1818

1919
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2020
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
21-
import com.palantir.gradle.failurereports.util.ImmutablesStyle;
2221
import org.immutables.value.Value;
2322

2423
@ImmutablesStyle

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/util/FailureReporterResources.java gradle-failure-reports-common/src/main/java/com/palantir/gradle/failurereports/common/FailureReporterResources.java

+17-11
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.palantir.gradle.failurereports.util;
17+
package com.palantir.gradle.failurereports.common;
1818

1919
import com.google.common.base.Throwables;
20+
import java.nio.charset.StandardCharsets;
2021
import java.nio.file.Path;
2122
import java.util.Locale;
2223
import java.util.Optional;
23-
import org.gradle.api.Task;
24-
import org.gradle.api.logging.Logging;
25-
import org.slf4j.Logger;
2624

2725
public final class FailureReporterResources {
2826

2927
private static final Integer MAX_TITLE_ERROR_LENGTH = 150;
3028
private static final String ERROR_SEVERITY = "Error";
3129
private static final String EMPTY_SPACE = " ";
3230

33-
public static final Logger log = Logging.getLogger(FailureReporterResources.class);
34-
3531
public static String getFileName(String fullPath) {
3632
return Path.of(fullPath).getFileName().toString();
3733
}
@@ -48,8 +44,8 @@ private static String getRelativePathToProject(Path projectDir, Path fullFilePat
4844
try {
4945
return projectDir.relativize(fullFilePath).toString();
5046
} catch (IllegalArgumentException e) {
51-
log.warn("Unable to relativize path {} from {}", fullFilePath, projectDir, e);
52-
throw e;
47+
throw new RuntimeException(
48+
String.format("Unable to relativize path %s from %s", fullFilePath, projectDir), e);
5349
}
5450
}
5551

@@ -90,9 +86,19 @@ private static String getTruncatedErrorMessage(String errorMessage) {
9086
return errorMessage;
9187
}
9288

93-
public static boolean executedAndFailed(Task task) {
94-
return task.getState().getExecuted()
95-
&& Optional.ofNullable(task.getState().getFailure()).isPresent();
89+
/**
90+
* Keeps the last @param bytesSize of the fullString.
91+
* @param fullString the string that should be truncated if it exceeds the bytesSize.
92+
* @param bytesSize the size in bytes of the fullString that needs to be preserved.
93+
* @return the truncated string prefixed by `...[truncated]` if it exceeds bytesSize, otherwise the fullString
94+
*/
95+
public static String keepLastBytesSizeOutput(String fullString, int bytesSize) {
96+
byte[] bytes = fullString.getBytes(StandardCharsets.UTF_8);
97+
if (bytes.length > bytesSize) {
98+
int startIndex = bytes.length - bytesSize;
99+
return "...[truncated]" + new String(bytes, startIndex, bytesSize, StandardCharsets.UTF_8);
100+
}
101+
return fullString;
96102
}
97103

98104
private FailureReporterResources() {}

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/util/ImmutablesStyle.java gradle-failure-reports-common/src/main/java/com/palantir/gradle/failurereports/common/ImmutablesStyle.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.palantir.gradle.failurereports.util;
17+
package com.palantir.gradle.failurereports.common;
1818

1919
import java.lang.annotation.ElementType;
2020
import java.lang.annotation.Retention;

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/util/ThrowableResources.java gradle-failure-reports-common/src/main/java/com/palantir/gradle/failurereports/common/ThrowableResources.java

+15-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.palantir.gradle.failurereports.util;
17+
package com.palantir.gradle.failurereports.common;
1818

1919
import com.google.common.base.Throwables;
2020
import java.util.stream.Collectors;
@@ -26,24 +26,30 @@ public final class ThrowableResources {
2626
public static final String EXCEPTION_MESSAGE = "* Full exception is:";
2727

2828
public static String formatThrowable(Throwable throwable) {
29+
return String.format("%s\n\n%s", formatCausalChain(throwable), formatStacktrace(throwable));
30+
}
31+
32+
public static String formatThrowableWithMessage(Throwable throwable) {
2933
String errorMessage = getFormattedErrorMessage(Throwables.getRootCause(throwable));
3034
return formatThrowableWithMessage(throwable, errorMessage);
3135
}
3236

3337
public static String formatThrowableWithMessage(Throwable throwable, String errorMessage) {
38+
return String.format("%s\n\n%s\n\n%s", errorMessage, formatCausalChain(throwable), formatStacktrace(throwable));
39+
}
40+
41+
private static String formatCausalChain(Throwable throwable) {
3442
String causalChain = Throwables.getCausalChain(throwable).stream()
3543
.map(ThrowableResources::printThrowableCause)
3644
.collect(Collectors.joining("\n"));
37-
return String.format(
38-
"%s\n\n%s\n%s\n\n%s\n%s",
39-
errorMessage,
40-
CAUSAL_CHAIN,
41-
causalChain,
42-
EXCEPTION_MESSAGE,
43-
Throwables.getStackTraceAsString(throwable));
45+
return String.format("%s\n%s", CAUSAL_CHAIN, causalChain);
46+
}
47+
48+
private static String formatStacktrace(Throwable throwable) {
49+
return String.format("%s\n%s", EXCEPTION_MESSAGE, Throwables.getStackTraceAsString(throwable));
4450
}
4551

46-
public static String printThrowableCause(Throwable throwableCause) {
52+
private static String printThrowableCause(Throwable throwableCause) {
4753
if (throwableCause.getMessage() == null || throwableCause.getMessage().isEmpty()) {
4854
return String.format("\t%s", throwableCause.getClass().getCanonicalName());
4955
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
apply plugin: 'java-library'
22
apply plugin: 'com.palantir.external-publish-jar'
3+
4+
5+
dependencies {
6+
implementation project(':gradle-failure-reports-common')
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.gradle.failurereports.exceptions;
18+
19+
import com.palantir.gradle.failurereports.common.FailureReport;
20+
import com.palantir.gradle.failurereports.common.FailureReporterResources;
21+
import com.palantir.gradle.failurereports.common.ThrowableResources;
22+
23+
/**
24+
* An exception type that allows passing an extra logs field which is rendered in the CircleCi failure report.
25+
* It is useful for surfacing errors derived from a subprocess.
26+
*/
27+
public final class ExceptionWithLogs extends FailureReporterException {
28+
29+
private final String logs;
30+
private final boolean includeStackTrace;
31+
32+
public ExceptionWithLogs(String message, String logs, boolean includeStackTrace) {
33+
this(message, logs, null, includeStackTrace);
34+
}
35+
36+
public ExceptionWithLogs(String message, String logs) {
37+
this(message, logs, null, true);
38+
}
39+
40+
public ExceptionWithLogs(String message, String logs, Throwable throwable) {
41+
this(message, logs, throwable, true);
42+
}
43+
44+
public ExceptionWithLogs(String message, String logs, Throwable throwable, boolean includeStackTrace) {
45+
super(message, throwable);
46+
// keeping only the last 100kb of logs to avoid any potential OOM issues if the logs are really large.
47+
this.logs = FailureReporterResources.keepLastBytesSizeOutput(logs, 100 * 1024);
48+
this.includeStackTrace = includeStackTrace;
49+
}
50+
51+
@Override
52+
public FailureReport getTaskFailureReport(String taskPath, Throwable initialThrowable) {
53+
String maybeIncludeStacktrace =
54+
includeStackTrace ? "\n" + ThrowableResources.formatThrowable(initialThrowable) : "";
55+
return FailureReport.builder()
56+
.header(FailureReporterResources.getTaskErrorHeader(taskPath, getMessage()))
57+
.clickableSource(taskPath)
58+
.errorMessage(String.join("\n", getMessage(), logs, maybeIncludeStacktrace))
59+
.build();
60+
}
61+
}

gradle-failure-reports-exceptions/src/main/java/com/palantir/gradle/failurereports/exceptions/ExceptionWithSuggestion.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616

1717
package com.palantir.gradle.failurereports.exceptions;
1818

19+
import com.palantir.gradle.failurereports.common.FailureReport;
20+
import com.palantir.gradle.failurereports.common.FailureReporterResources;
21+
import com.palantir.gradle.failurereports.common.ThrowableResources;
22+
1923
/**
2024
* An exception type that provides additional context or guidance when errors occur.
2125
* A suggestion can be either a command that fixes the error e.g. "./gradlew --write-locks" or a reference to
2226
* a source file where changes need to be applied e.g. "versions.props:3" (line 3 from versions.props is invalid)
2327
* The suggestion is intended to be quickly actionable by the user to resolve the error.
2428
*/
25-
public class ExceptionWithSuggestion extends RuntimeException {
29+
public final class ExceptionWithSuggestion extends FailureReporterException {
2630

2731
private final String suggestion;
2832

@@ -36,7 +40,12 @@ public ExceptionWithSuggestion(String message, String suggestion, Throwable thro
3640
this.suggestion = suggestion;
3741
}
3842

39-
public final String getSuggestion() {
40-
return suggestion;
43+
@Override
44+
public FailureReport getTaskFailureReport(String taskPath, Throwable initialThrowable) {
45+
return FailureReport.builder()
46+
.header(FailureReporterResources.getTaskErrorHeader(taskPath, getMessage()))
47+
.clickableSource(suggestion)
48+
.errorMessage(ThrowableResources.formatThrowableWithMessage(initialThrowable, getMessage()))
49+
.build();
4150
}
4251
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.gradle.failurereports.exceptions;
18+
19+
import com.palantir.gradle.failurereports.common.FailureReport;
20+
21+
/**
22+
* Subclass of RuntimeException that can display the exception as a Failure Report in Circle CI.
23+
* For examples of usages see {@link ExceptionWithLogs} and {@link ExceptionWithSuggestion}.
24+
*/
25+
public abstract class FailureReporterException extends RuntimeException {
26+
27+
public FailureReporterException(String message) {
28+
super(message);
29+
}
30+
31+
public FailureReporterException(String message, Throwable cause) {
32+
super(message, cause);
33+
}
34+
35+
/**
36+
* Rendering a FailureReporterException exception that is part of the causalChain of {@code initialThrowable} as
37+
* a FailureReport that can be shown in the CircleCI `Tests` tab.
38+
* @param taskPath The Gradle task that caused the exception
39+
* @param initialThrowable the throwable that contains the FailureReporterException exception in the casualChain
40+
* @return the FailureReport object
41+
*/
42+
public abstract FailureReport getTaskFailureReport(String taskPath, Throwable initialThrowable);
43+
}

gradle-failure-reports-exceptions/src/main/java/com/palantir/gradle/failurereports/exceptions/MinimalException.java

-32
This file was deleted.

gradle-failure-reports/build.gradle

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ dependencies {
1010
annotationProcessor 'org.immutables:value'
1111

1212
compileOnly 'com.palantir.gradle.auto-parallelizable:auto-parallelizable-annotations'
13-
14-
annotationProcessor "org.immutables:value"
1513
compileOnly 'org.immutables:value::annotations'
1614

1715
implementation project(':gradle-failure-reports-exceptions')
16+
implementation project(':gradle-failure-reports-common')
1817
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
1918
implementation 'com.palantir.gradle.utils:environment-variables'
2019
implementation 'com.google.guava:guava'

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/BuildFailureReporter.java

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.common.base.Throwables;
2020
import com.google.common.collect.ImmutableList;
21+
import com.palantir.gradle.failurereports.common.FailureReport;
2122
import com.palantir.gradle.failurereports.junit.JunitReporter;
2223
import java.io.File;
2324
import java.io.IOException;

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/CheckstyleFailureReporter.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,22 @@
1717
package com.palantir.gradle.failurereports;
1818

1919
import com.palantir.gradle.failurereports.checkstyle.CheckstyleOutput;
20-
import com.palantir.gradle.failurereports.util.FailureReporterResources;
20+
import com.palantir.gradle.failurereports.common.FailureReport;
21+
import com.palantir.gradle.failurereports.common.FailureReporterResources;
2122
import com.palantir.gradle.failurereports.util.XmlResources;
2223
import java.io.File;
2324
import java.io.IOException;
2425
import java.nio.file.Path;
26+
import java.util.Optional;
2527
import java.util.stream.Stream;
2628
import org.gradle.api.Project;
29+
import org.gradle.api.Task;
2730
import org.gradle.api.plugins.quality.Checkstyle;
2831

2932
public final class CheckstyleFailureReporter {
3033

3134
public static Stream<FailureReport> collect(Project project, Checkstyle checkstyleTask) {
32-
if (!FailureReporterResources.executedAndFailed(checkstyleTask)) {
35+
if (!executedAndFailed(checkstyleTask)) {
3336
return Stream.empty();
3437
}
3538
File checkstyleReportXml = checkstyleTask
@@ -64,5 +67,10 @@ private static Stream<FailureReport> from(Project project, CheckstyleOutput chec
6467
.build()));
6568
}
6669

70+
public static boolean executedAndFailed(Task task) {
71+
return task.getState().getExecuted()
72+
&& Optional.ofNullable(task.getState().getFailure()).isPresent();
73+
}
74+
6775
private CheckstyleFailureReporter() {}
6876
}

gradle-failure-reports/src/main/java/com/palantir/gradle/failurereports/CompileFailuresService.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
import com.google.common.base.Splitter;
2020
import com.palantir.gradle.failurereports.CompileFailuresService.Parameters;
21+
import com.palantir.gradle.failurereports.common.FailureReport;
22+
import com.palantir.gradle.failurereports.common.FailureReporterResources;
2123
import com.palantir.gradle.failurereports.junit.JunitReporter;
22-
import com.palantir.gradle.failurereports.util.FailureReporterResources;
2324
import java.io.File;
2425
import java.nio.file.Path;
2526
import java.util.Optional;

0 commit comments

Comments
 (0)