From 59fb8b78a1d236c06df234f474170c1cfd17d286 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Fri, 23 Jan 2026 14:44:33 +0000 Subject: [PATCH 01/21] maybe rubbish --- .../testreport/FormattingTestReporter.java | 175 ------------------ .../testreport/HtmlReportPostProcessor.java | 125 +++++++++++++ .../TestReportFormattingPlugin.java | 102 +++------- ...portFormattingPluginIntegrationSpec.groovy | 13 +- 4 files changed, 161 insertions(+), 254 deletions(-) delete mode 100644 gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/FormattingTestReporter.java create mode 100644 gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/FormattingTestReporter.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/FormattingTestReporter.java deleted file mode 100644 index 5ab9da0e..00000000 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/FormattingTestReporter.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.palantir.witchcraft.java.logging.gradle.testreport; - -// CHECKSTYLE:OFF - -import com.palantir.witchcraft.java.logging.format.LogFormatter; -import com.palantir.witchcraft.java.logging.format.LogParser; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.io.Writer; -import java.util.function.BiConsumer; -import org.gradle.api.Action; -import org.gradle.api.internal.tasks.testing.junit.result.TestClassResult; -import org.gradle.api.internal.tasks.testing.junit.result.TestResultsProvider; -import org.gradle.api.internal.tasks.testing.report.HtmlTestReport; -import org.gradle.api.internal.tasks.testing.report.TestReporter; -import org.gradle.api.tasks.testing.TestOutputEvent; -// CHECKSTYLE:ON - -final class FormattingTestReporter implements TestReporter { - - private final Object delegate; - - FormattingTestReporter(Object delegate) { - this.delegate = delegate; - } - - @Override - public void generateReport(TestResultsProvider testResultsProvider, File file) { - if (delegate instanceof TestReporter) { - ((TestReporter) delegate).generateReport(new FormattingTestResultsProvider(testResultsProvider), file); - } else if (delegate instanceof HtmlTestReport) { - ((HtmlTestReport) delegate).generateReport(new FormattingTestResultsProvider(testResultsProvider), file); - } else { - throw new IllegalArgumentException("Unknown delegate class: " + delegate.getClass()); - } - } - - private static final class FormattingTestResultsProvider implements TestResultsProvider { - - private static final LogParser PARSER = new LogParser<>(TestLogFilter.INSTANCE.combineWith( - LogFormatter.INSTANCE, - (include, formatted) -> include - ? writer -> { - writer.write(formatted); - writer.write('\n'); - } - : Writable.NOP)); - private static final BiConsumer LINE_PROCESSOR = (line, outputWriter) -> { - try { - PARSER.tryParse(line) - .orElseGet(() -> out -> { - try { - out.write(line); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }) - .write(outputWriter); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - - private final TestResultsProvider delegate; - - FormattingTestResultsProvider(TestResultsProvider delegate) { - this.delegate = delegate; - } - - @Override - public void writeAllOutput(long classId, TestOutputEvent.Destination destination, Writer writer) { - if (destination == TestOutputEvent.Destination.StdErr - || destination == TestOutputEvent.Destination.StdOut) { - delegate.writeAllOutput(classId, destination, new LineProcessingWriter(writer, LINE_PROCESSOR)); - } else { - delegate.writeAllOutput(classId, destination, writer); - } - } - - private static class LineProcessingWriter extends Writer { - private final Writer delegate; - private final BiConsumer lineProcessor; - private final StringBuilder lineBuffer = new StringBuilder(); - - LineProcessingWriter(Writer delegate, BiConsumer lineProcessor) { - this.delegate = delegate; - this.lineProcessor = lineProcessor; - } - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - for (int i = off; i < off + len; i++) { - char ch = cbuf[i]; - if (ch == '\n') { - processLine(); - } else { - lineBuffer.append(ch); - } - } - } - - @Override - public void flush() throws IOException { - delegate.flush(); - } - - @Override - public void close() throws IOException { - if (!lineBuffer.isEmpty()) { - processLine(); - } - delegate.close(); - } - - private void processLine() throws IOException { - String line = lineBuffer.toString(); - lineProcessor.accept(line, delegate); - delegate.write('\n'); - lineBuffer.setLength(0); - } - } - - @Override - public void writeNonTestOutput(long classId, TestOutputEvent.Destination destination, Writer writer) { - delegate.writeNonTestOutput(classId, destination, writer); - } - - @Override - public void writeTestOutput(long classId, long testId, TestOutputEvent.Destination destination, Writer writer) { - delegate.writeTestOutput(classId, testId, destination, writer); - } - - @Override - public void visitClasses(Action visitor) { - delegate.visitClasses(visitor); - } - - @Override - public boolean hasOutput(long classId, TestOutputEvent.Destination destination) { - return delegate.hasOutput(classId, destination); - } - - @Override - public boolean hasOutput(long classId, long testId, TestOutputEvent.Destination destination) { - return delegate.hasOutput(classId, testId, destination); - } - - @Override - public boolean isHasResults() { - return delegate.isHasResults(); - } - - @Override - public void close() throws IOException { - delegate.close(); - } - } -} diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java new file mode 100644 index 00000000..d4dbf801 --- /dev/null +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -0,0 +1,125 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.witchcraft.java.logging.gradle.testreport; + +import com.palantir.witchcraft.java.logging.format.LogFormatter; +import com.palantir.witchcraft.java.logging.format.LogParser; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Post-processes HTML test report files to apply witchcraft log formatting. + */ +final class HtmlReportPostProcessor { + + private static final LogParser> PARSER = + new LogParser<>(TestLogFilter.INSTANCE.combineWith(LogFormatter.INSTANCE, (include, formatted) -> + include ? Optional.of(formatted) : Optional.empty())); + + private static final Pattern PRE_PATTERN = Pattern.compile("(]*>)(.*?)()", Pattern.DOTALL); + + void processReportDirectory(File reportDir) { + Optional.ofNullable(reportDir) + .filter(File::isDirectory) + .ifPresent(this::walkAndProcessHtmlFiles); + } + + private void walkAndProcessHtmlFiles(File reportDir) { + try (Stream paths = Files.walk(reportDir.toPath())) { + paths.filter(path -> path.toString().endsWith(".html")) + .forEach(this::processHtmlFile); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void processHtmlFile(Path htmlFile) { + try { + String content = Files.readString(htmlFile, StandardCharsets.UTF_8); + String processed = processHtmlContent(content); + + if (!content.equals(processed)) { + Files.writeString(htmlFile, processed, StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + String processHtmlContent(String html) { + Matcher matcher = PRE_PATTERN.matcher(html); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String replacement = matcher.group(1) + formatPreContent(matcher.group(2)) + matcher.group(3); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + + private String formatPreContent(String content) { + return Arrays.stream(content.split("\n")) + .map(this::formatLine) + .flatMap(Optional::stream) + .collect(Collectors.joining("\n")); + } + + /** + * @return formatted line, empty if filtered out, or original if not a witchcraft log + */ + private Optional formatLine(String line) { + String decoded = htmlDecode(line); + + return PARSER.tryParse(decoded) + .map(opt -> opt.map(HtmlReportPostProcessor::htmlEncode)) + .orElse(Optional.of(line)); + } + + private static String htmlDecode(String html) { + return html.replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace(""", "\"") + .replace("'", "'") + .replace("'", "'"); + } + + private static String htmlEncode(String text) { + return text.chars() + .mapToObj(c -> switch (c) { + case '&' -> "&"; + case '<' -> "<"; + case '>' -> ">"; + case '"' -> """; + case '\'' -> "'"; + default -> String.valueOf((char) c); + }) + .collect(Collectors.joining()); + } +} diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index 68454645..5ef27f42 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -16,96 +16,46 @@ package com.palantir.witchcraft.java.logging.gradle.testreport; -// CHECKSTYLE:OFF - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.io.File; +import java.util.Optional; import javax.inject.Inject; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.internal.tasks.testing.report.HtmlTestReport; -import org.gradle.api.internal.tasks.testing.report.TestReporter; +import org.gradle.api.flow.FlowAction; +import org.gradle.api.flow.FlowParameters; +import org.gradle.api.flow.FlowScope; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.testing.AbstractTestTask; -import org.gradle.internal.operations.BuildOperationExecutor; -import org.gradle.internal.operations.BuildOperationRunner; -import org.gradle.util.GradleVersion; -// CHECKSTYLE:ON /** - * In its current form, this plugin may generously be described as "a workaround". - * I've filed gradle#17966 - * upstream to find a better solution. - * We may be able to consume the xml test report and generate our own html based on that - * if the current approach becomes troublesome, that would allow us to color individual - * lines much like our intellij plugin. + * Plugin that formats witchcraft structured logging output in HTML test reports. + * Post-processes generated HTML files to replace JSON log lines with formatted versions. */ public abstract class TestReportFormattingPlugin implements Plugin { - @Override - @SuppressWarnings("Slf4jLogsafeArgs") - public final void apply(Project project) { - project.getTasks().withType(AbstractTestTask.class).configureEach(task -> { - try { - Method method = AbstractTestTask.class.getDeclaredMethod("setTestReporter", TestReporter.class); - method.setAccessible(true); + @Inject + protected abstract FlowScope getFlowScope(); - method.invoke(task, new FormattingTestReporter(getFormattingDelegate())); - } catch (ReflectiveOperationException e) { - project.getLogger() - .error( - "Failed to update task '{}' TestReporter to format structured logging output", - task.getName(), - e); - } - }); + @Override + public void apply(Project project) { + project.getTasks().withType(AbstractTestTask.class).configureEach(task -> + Optional.ofNullable(task.getReports().getHtml().getOutputLocation().getAsFile().getOrNull()) + .ifPresent(reportDir -> getFlowScope().always(FormatReportAction.class, spec -> + spec.getParameters().getReportDir().set(reportDir)))); } - /** - * The internal gradle test report classes changed in 8.11. DefaultTestReport was renamed to HtmlTestReport and - * also stopped extending the TestReporter interface. So the delegate for the formatting reporter varies either - * an HtmlTestReport to be compatible with gradle 8.11+ or DefaultTestReport for lower versions. - */ - private Object getFormattingDelegate() { - boolean greaterThan8Point11 = GradleVersion.current().compareTo(GradleVersion.version("8.11")) >= 0; - if (greaterThan8Point11) { - return new HtmlTestReport(getBuildOperationRunner(), getBuildOperationExecutor()); - } else { - return createDefaultTestReport(); + public abstract static class FormatReportAction implements FlowAction { + interface Parameters extends FlowParameters { + @Input + Property getReportDir(); } - } - /** - * The constructor for DefaultTestReport changed in gradle 8.8. Dynamically invoke based on runtime version. - */ - private TestReporter createDefaultTestReport() { - boolean greaterThan8Point8 = GradleVersion.current().compareTo(GradleVersion.version("8.8")) >= 0; - - try { - Class defaultTestReporterClass = (Class) - Class.forName("org.gradle.api.internal.tasks.testing.report.DefaultTestReport"); - if (greaterThan8Point8) { - return defaultTestReporterClass - .getDeclaredConstructor(BuildOperationRunner.class, BuildOperationExecutor.class) - .newInstance(getBuildOperationRunner(), getBuildOperationExecutor()); - } else { - return defaultTestReporterClass - .getDeclaredConstructor(BuildOperationExecutor.class) - .newInstance(getBuildOperationExecutor()); - } - } catch (InstantiationException - | IllegalAccessException - | InvocationTargetException - | ClassNotFoundException - | NoSuchMethodException e) { - throw new RuntimeException(e); + @Override + public void execute(Parameters parameters) { + Optional.ofNullable(parameters.getReportDir().getOrNull()) + .filter(File::exists) + .ifPresent(reportDir -> new HtmlReportPostProcessor().processReportDirectory(reportDir)); } } - - @Inject - @SuppressWarnings("DesignForExtension") - protected abstract BuildOperationExecutor getBuildOperationExecutor(); - - @Inject - @SuppressWarnings("DesignForExtension") - protected abstract BuildOperationRunner getBuildOperationRunner(); } diff --git a/gradle-witchcraft-logging/src/test/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationSpec.groovy b/gradle-witchcraft-logging/src/test/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationSpec.groovy index 86eac991..1370582e 100644 --- a/gradle-witchcraft-logging/src/test/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationSpec.groovy +++ b/gradle-witchcraft-logging/src/test/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationSpec.groovy @@ -21,7 +21,7 @@ import nebula.test.IntegrationSpec import org.gradle.util.GradleVersion class TestReportFormattingPluginIntegrationSpec extends IntegrationSpec { - private static final List GRADLE_VERSIONS = ["7.6.4", "8.8", GradleVersion.current().getVersion()] + private static final List GRADLE_VERSIONS = ["8.8", GradleVersion.current().getVersion(), "9.3.0"] def '#gradleVersionNumber: Formats test report stdout and stderr'() { gradleVersion = gradleVersionNumber @@ -47,7 +47,11 @@ class TestReportFormattingPluginIntegrationSpec extends IntegrationSpec { testImplementation 'junit:junit:4.13.2' } - sourceCompatibility = 11 + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + """.stripIndent() writeUnitTest(""" @@ -91,7 +95,10 @@ class TestReportFormattingPluginIntegrationSpec extends IntegrationSpec { runTasksSuccessfully('compileTestJava') def testResult = runTasksWithFailure('test') testResult.wasExecuted('compileTestJava') - def htmlReport = file('build/reports/tests/test/classes/com.palantir.SimpleTest.html').text + // Gradle 9+ uses a different report structure + def legacyReportFile = file('build/reports/tests/test/classes/com.palantir.SimpleTest.html') + def newReportFile = file('build/reports/tests/test/com.palantir.SimpleTest/simpleTest.html') + def htmlReport = legacyReportFile.exists() && legacyReportFile.text ? legacyReportFile.text : newReportFile.text htmlReport.contains('==Service==') !htmlReport.contains('service.1') htmlReport.contains('ERROR [2019-05-09T15:32:37.692Z] [main] ROOT: test good {} (good: :-))') From 259ca9d0df04feb37b07ea221141325f3c93b4ca Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Fri, 23 Jan 2026 14:51:46 +0000 Subject: [PATCH 02/21] spotless and the like --- .../testreport/HtmlReportPostProcessor.java | 12 ++++-------- .../testreport/TestReportFormattingPlugin.java | 16 ++++++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index d4dbf801..cf24fc2a 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -36,22 +36,18 @@ */ final class HtmlReportPostProcessor { - private static final LogParser> PARSER = - new LogParser<>(TestLogFilter.INSTANCE.combineWith(LogFormatter.INSTANCE, (include, formatted) -> - include ? Optional.of(formatted) : Optional.empty())); + private static final LogParser> PARSER = new LogParser<>(TestLogFilter.INSTANCE.combineWith( + LogFormatter.INSTANCE, (include, formatted) -> include ? Optional.of(formatted) : Optional.empty())); private static final Pattern PRE_PATTERN = Pattern.compile("(]*>)(.*?)()", Pattern.DOTALL); void processReportDirectory(File reportDir) { - Optional.ofNullable(reportDir) - .filter(File::isDirectory) - .ifPresent(this::walkAndProcessHtmlFiles); + Optional.ofNullable(reportDir).filter(File::isDirectory).ifPresent(this::walkAndProcessHtmlFiles); } private void walkAndProcessHtmlFiles(File reportDir) { try (Stream paths = Files.walk(reportDir.toPath())) { - paths.filter(path -> path.toString().endsWith(".html")) - .forEach(this::processHtmlFile); + paths.filter(path -> path.toString().endsWith(".html")).forEach(this::processHtmlFile); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index 5ef27f42..78a120d5 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -38,14 +38,18 @@ public abstract class TestReportFormattingPlugin implements Plugin { protected abstract FlowScope getFlowScope(); @Override - public void apply(Project project) { - project.getTasks().withType(AbstractTestTask.class).configureEach(task -> - Optional.ofNullable(task.getReports().getHtml().getOutputLocation().getAsFile().getOrNull()) - .ifPresent(reportDir -> getFlowScope().always(FormatReportAction.class, spec -> - spec.getParameters().getReportDir().set(reportDir)))); + public final void apply(Project project) { + project.getTasks().withType(AbstractTestTask.class).configureEach(task -> Optional.ofNullable(task.getReports() + .getHtml() + .getOutputLocation() + .getAsFile() + .getOrNull()) + .ifPresent(reportDir -> getFlowScope().always(FormatReportAction.class, spec -> spec.getParameters() + .getReportDir() + .set(reportDir)))); } - public abstract static class FormatReportAction implements FlowAction { + public static final class FormatReportAction implements FlowAction { interface Parameters extends FlowParameters { @Input Property getReportDir(); From 97ad71abe3a517302f6b9105ff163763646bc4ca Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Fri, 23 Jan 2026 14:57:15 +0000 Subject: [PATCH 03/21] match
 tags within stdout/stderr sections:
 

standard output

...
...
--- .../logging/gradle/testreport/HtmlReportPostProcessor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index cf24fc2a..ad70e976 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -39,7 +39,10 @@ final class HtmlReportPostProcessor { private static final LogParser> PARSER = new LogParser<>(TestLogFilter.INSTANCE.combineWith( LogFormatter.INSTANCE, (include, formatted) -> include ? Optional.of(formatted) : Optional.empty())); - private static final Pattern PRE_PATTERN = Pattern.compile("(]*>)(.*?)(
)", Pattern.DOTALL); + // Match
 tags within stdout/stderr sections: 

standard output

...
...
+ private static final Pattern OUTPUT_SECTION_PATTERN = Pattern.compile( + "(

standard (?:output|error)

\\s*]*>\\s*]*>)(.*?)(
)", + Pattern.DOTALL | Pattern.CASE_INSENSITIVE); void processReportDirectory(File reportDir) { Optional.ofNullable(reportDir).filter(File::isDirectory).ifPresent(this::walkAndProcessHtmlFiles); @@ -67,7 +70,7 @@ private void processHtmlFile(Path htmlFile) { } String processHtmlContent(String html) { - Matcher matcher = PRE_PATTERN.matcher(html); + Matcher matcher = OUTPUT_SECTION_PATTERN.matcher(html); StringBuilder result = new StringBuilder(); while (matcher.find()) { From 5cb6362c4b63b0783a24569e4eddd6b5efe574c1 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Fri, 23 Jan 2026 15:46:21 +0000 Subject: [PATCH 04/21] parity match --- .../testreport/HtmlReportPostProcessor.java | 5 +++-- .../TestReportFormattingPlugin.java | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index ad70e976..bce10164 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -90,13 +90,14 @@ private String formatPreContent(String content) { } /** - * @return formatted line, empty if filtered out, or original if not a witchcraft log + * @return formatted line (with trailing newline), empty if filtered out, + * or original if not a witchcraft log */ private Optional formatLine(String line) { String decoded = htmlDecode(line); return PARSER.tryParse(decoded) - .map(opt -> opt.map(HtmlReportPostProcessor::htmlEncode)) + .map(opt -> opt.map(formatted -> htmlEncode(formatted) + "\n")) .orElse(Optional.of(line)); } diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index 78a120d5..ce1b466c 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -39,14 +39,17 @@ public abstract class TestReportFormattingPlugin implements Plugin { @Override public final void apply(Project project) { - project.getTasks().withType(AbstractTestTask.class).configureEach(task -> Optional.ofNullable(task.getReports() - .getHtml() - .getOutputLocation() - .getAsFile() - .getOrNull()) - .ifPresent(reportDir -> getFlowScope().always(FormatReportAction.class, spec -> spec.getParameters() - .getReportDir() - .set(reportDir)))); + project.getGradle().projectsEvaluated(_gradle -> project.getTasks() + .withType(AbstractTestTask.class) + .forEach(task -> Optional.ofNullable(task.getReports() + .getHtml() + .getOutputLocation() + .getAsFile() + .getOrNull()) + .ifPresent(reportDir -> getFlowScope() + .always(FormatReportAction.class, spec -> spec.getParameters() + .getReportDir() + .set(reportDir))))); } public static final class FormatReportAction implements FlowAction { From 780431b790c6a6468a4ca2906d9df38afb150344 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Fri, 23 Jan 2026 17:01:38 +0000 Subject: [PATCH 05/21] spotless --- .../testreport/TestReportFormattingPluginIntegrationTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java b/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java index f7e7c190..cd63f0b6 100644 --- a/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java +++ b/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java @@ -109,8 +109,7 @@ void formats_test_report_stdout_and_stderr(GradleInvoker gradle, RootProject roo ArbitraryFile newReportFile = rootProject.buildDir().file("reports/tests/test/com.palantir.SimpleTest/simpleTest.html"); - ArbitraryFile reportFile = - legacyReportFile.path().toFile().exists() ? legacyReportFile : newReportFile; + ArbitraryFile reportFile = legacyReportFile.path().toFile().exists() ? legacyReportFile : newReportFile; reportFile .assertThat() From 7f38c02fbbfade9c1f01904379b07c121ddb0e7a Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 10:31:57 +0000 Subject: [PATCH 06/21] tidier --- .../TestReportFormattingPlugin.java | 26 +++++++++---------- gradle/gradle-test-versions.yml | 3 +-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index ce1b466c..a4006bd4 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -25,6 +25,7 @@ import org.gradle.api.flow.FlowParameters; import org.gradle.api.flow.FlowScope; import org.gradle.api.provider.Property; +import org.gradle.api.reporting.DirectoryReport; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.testing.AbstractTestTask; @@ -39,28 +40,27 @@ public abstract class TestReportFormattingPlugin implements Plugin { @Override public final void apply(Project project) { - project.getGradle().projectsEvaluated(_gradle -> project.getTasks() - .withType(AbstractTestTask.class) - .forEach(task -> Optional.ofNullable(task.getReports() - .getHtml() - .getOutputLocation() - .getAsFile() - .getOrNull()) - .ifPresent(reportDir -> getFlowScope() - .always(FormatReportAction.class, spec -> spec.getParameters() - .getReportDir() - .set(reportDir))))); + project.getTasks().withType(AbstractTestTask.class).configureEach(task -> { + getFlowScope().always(FormatReportAction.class, spec -> spec.getParameters() + .getReportDir() + .set(task.getReports().getHtml())); + }); } public static final class FormatReportAction implements FlowAction { interface Parameters extends FlowParameters { @Input - Property getReportDir(); + Property getReportDir(); } @Override public void execute(Parameters parameters) { - Optional.ofNullable(parameters.getReportDir().getOrNull()) + Optional.ofNullable(parameters + .getReportDir() + .get() + .getOutputLocation() + .getAsFile() + .getOrNull()) .filter(File::exists) .ifPresent(reportDir -> new HtmlReportPostProcessor().processReportDirectory(reportDir)); } diff --git a/gradle/gradle-test-versions.yml b/gradle/gradle-test-versions.yml index 21e8fe68..a98569bc 100644 --- a/gradle/gradle-test-versions.yml +++ b/gradle/gradle-test-versions.yml @@ -1,5 +1,4 @@ major-versions: 9: 9.3.0 8: 8.14.3 -extra-versions: - - 8.8 +extra-versions: [] From 21f76f1f1f636e6eaf6abdf285a96881c1f44f1e Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:38:21 +0000 Subject: [PATCH 07/21] check gradle version --- .../java/logging/gradle/WitchcraftLoggingPlugin.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java index 1a1ab7d7..c80bc403 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java @@ -22,6 +22,7 @@ import java.util.Objects; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.util.GradleVersion; /** One-stop-shop for a fantastic developer experience with witchcraft-logging projects. */ public final class WitchcraftLoggingPlugin implements Plugin { @@ -32,6 +33,11 @@ public void apply(Project rootProject) { WitchcraftLoggingPlugin.class.getSimpleName() + " can only be applied to the root project."); } + if (GradleVersion.current().compareTo(GradleVersion.version("8.1")) < 0) { + throw new IllegalArgumentException(WitchcraftLoggingPlugin.class.getSimpleName() + + " can only be applied if using Gradle version 8.1 or newer."); + } + rootProject.getPluginManager().apply(IdeaConfigurationPlugin.class); IdeaConfigurationExtension extension = rootProject.getExtensions().getByType(IdeaConfigurationExtension.class); extension From 7ac0be93d4a59e4c6343674d4681fdac488240c9 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:41:06 +0000 Subject: [PATCH 08/21] optional --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index bce10164..f0a0caa6 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -98,7 +98,7 @@ private Optional formatLine(String line) { return PARSER.tryParse(decoded) .map(opt -> opt.map(formatted -> htmlEncode(formatted) + "\n")) - .orElse(Optional.of(line)); + .orElseGet(() -> Optional.of(line)); } private static String htmlDecode(String html) { From 4b0c1b76e7b3e225909dfc7e29eb91904e5c9663 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:42:01 +0000 Subject: [PATCH 09/21] fixup --- .../testreport/TestReportFormattingPlugin.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index a4006bd4..efaaf288 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -17,7 +17,6 @@ package com.palantir.witchcraft.java.logging.gradle.testreport; import java.io.File; -import java.util.Optional; import javax.inject.Inject; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -55,14 +54,13 @@ interface Parameters extends FlowParameters { @Override public void execute(Parameters parameters) { - Optional.ofNullable(parameters - .getReportDir() - .get() - .getOutputLocation() - .getAsFile() - .getOrNull()) - .filter(File::exists) - .ifPresent(reportDir -> new HtmlReportPostProcessor().processReportDirectory(reportDir)); + File reportDirectory = parameters.getReportDir().get().getOutputLocation().getAsFile().getOrNull(); + + if (reportDirectory == null || !reportDirectory.exists()) { + return; + } + + new HtmlReportPostProcessor().processReportDirectory(reportDirectory); } } } From 968c07d745903e9760a83c5139d6e5e4faea2b11 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:42:46 +0000 Subject: [PATCH 10/21] make utils class --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index f0a0caa6..c79f70ab 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -122,4 +122,6 @@ private static String htmlEncode(String text) { }) .collect(Collectors.joining()); } + + private HtmlReportPostProcessor() {} } From fea4d1af5ee16b8dead15e4f5bd0c0066bd7a010 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:52:55 +0000 Subject: [PATCH 11/21] spotless --- .../gradle/testreport/TestReportFormattingPlugin.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java index efaaf288..7ee32888 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java @@ -54,7 +54,12 @@ interface Parameters extends FlowParameters { @Override public void execute(Parameters parameters) { - File reportDirectory = parameters.getReportDir().get().getOutputLocation().getAsFile().getOrNull(); + File reportDirectory = parameters + .getReportDir() + .get() + .getOutputLocation() + .getAsFile() + .getOrNull(); if (reportDirectory == null || !reportDirectory.exists()) { return; From a60fd3a2882e05b2b599dbab74c0464456fa7e7d Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 14:59:31 +0000 Subject: [PATCH 12/21] whoops --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index c79f70ab..f0a0caa6 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -122,6 +122,4 @@ private static String htmlEncode(String text) { }) .collect(Collectors.joining()); } - - private HtmlReportPostProcessor() {} } From 940ef4bef42fd43e59ab30fc3e1b548255b35d5d Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 26 Jan 2026 16:45:30 +0000 Subject: [PATCH 13/21] dont handroll --- gradle-witchcraft-logging/build.gradle | 1 + .../testreport/HtmlReportPostProcessor.java | 27 +++---------------- versions.lock | 4 +++ versions.props | 1 + 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/gradle-witchcraft-logging/build.gradle b/gradle-witchcraft-logging/build.gradle index dd93c46e..b76eb2dc 100644 --- a/gradle-witchcraft-logging/build.gradle +++ b/gradle-witchcraft-logging/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':witchcraft-logging-formatting') implementation 'com.google.guava:guava' implementation 'com.palantir.gradle.idea-configuration:gradle-idea-configuration' + implementation 'org.apache.commons:commons-text' testImplementation gradleTestKit() testImplementation 'com.google.guava:guava' diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index f0a0caa6..3347ce91 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -30,6 +30,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.text.StringEscapeUtils; /** * Post-processes HTML test report files to apply witchcraft log formatting. @@ -94,32 +95,10 @@ private String formatPreContent(String content) { * or original if not a witchcraft log */ private Optional formatLine(String line) { - String decoded = htmlDecode(line); + String decoded = StringEscapeUtils.unescapeHtml4(line); return PARSER.tryParse(decoded) - .map(opt -> opt.map(formatted -> htmlEncode(formatted) + "\n")) + .map(opt -> opt.map(formatted -> StringEscapeUtils.escapeHtml4(formatted) + "\n")) .orElseGet(() -> Optional.of(line)); } - - private static String htmlDecode(String html) { - return html.replace("<", "<") - .replace(">", ">") - .replace("&", "&") - .replace(""", "\"") - .replace("'", "'") - .replace("'", "'"); - } - - private static String htmlEncode(String text) { - return text.chars() - .mapToObj(c -> switch (c) { - case '&' -> "&"; - case '<' -> "<"; - case '>' -> ">"; - case '"' -> """; - case '\'' -> "'"; - default -> String.valueOf((char) c); - }) - .collect(Collectors.joining()); - } } diff --git a/versions.lock b/versions.lock index ca4e9415..0237040b 100644 --- a/versions.lock +++ b/versions.lock @@ -46,6 +46,10 @@ com.palantir.tritium:tritium-ids:0.117.0 (1 constraints: fc0f93a6) com.palantir.witchcraft.api:witchcraft-logging-api-objects:2.6.0 (1 constraints: 0a050736) +org.apache.commons:commons-lang3:3.20.0 (1 constraints: f70d9342) + +org.apache.commons:commons-text:1.15.0 (1 constraints: 3905383b) + org.eclipse.collections:eclipse-collections:13.0.0 (1 constraints: 1c1091a9) org.eclipse.collections:eclipse-collections-api:13.0.0 (2 constraints: fa22df26) diff --git a/versions.props b/versions.props index 59d9f30b..e68bd3f4 100644 --- a/versions.props +++ b/versions.props @@ -11,6 +11,7 @@ org.mockito:* = 5.21.0 org.slf4j:* = 2.0.17 com.netflix.nebula:nebula-test = 10.6.2 com.palantir.gradle.idea-configuration:gradle-idea-configuration = 0.3.0 +org.apache.commons:commons-text = 1.15.0 # test com.palantir.conjure.java.runtime:* = 8.30.0 From 4cdc67285b9287e71e16f2670c612119e73af06e Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 2 Feb 2026 15:32:25 +0000 Subject: [PATCH 14/21] new framework version --- build.gradle | 2 +- ...ReportFormattingPluginIntegrationTest.java | 26 ++++++++++--------- versions.lock | 8 +++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index e19e993c..6c17239c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { classpath 'com.palantir.jakartapackagealignment:jakarta-package-alignment:0.6.0' classpath 'com.palantir.gradle.jdks:gradle-jdks:0.73.0' classpath 'com.palantir.gradle.jdkslatest:gradle-jdks-latest:0.25.0' - classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.50.0' + classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.52.0' classpath 'com.palantir.gradle.externalpublish:gradle-external-publish-plugin:1.29.0' classpath 'com.gradle.publish:plugin-publish-plugin:2.0.0' classpath 'com.palantir.baseline-error-prone:gradle-baseline-error-prone:0.7.0' diff --git a/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java b/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java index cd63f0b6..8d12db40 100644 --- a/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java +++ b/gradle-witchcraft-logging/src/test/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPluginIntegrationTest.java @@ -20,8 +20,10 @@ import com.palantir.gradle.testing.execution.GradleInvoker; import com.palantir.gradle.testing.execution.InvocationResult; -import com.palantir.gradle.testing.files.arbitrary.ArbitraryFile; import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.junit.InjectByGradleVersion; +import com.palantir.gradle.testing.junit.ParameterizedByGradleVersion; +import com.palantir.gradle.testing.junit.ParameterizedByGradleVersion.WhenVersion; import com.palantir.gradle.testing.project.RootProject; import org.junit.jupiter.api.Test; @@ -65,7 +67,14 @@ public void simpleTest() { """; @Test - void formats_test_report_stdout_and_stderr(GradleInvoker gradle, RootProject rootProject) { + @ParameterizedByGradleVersion( + when = + @WhenVersion( + lessThan = "9.3.0", + stringValue = "reports/tests/test/classes/com.palantir.SimpleTest.html"), + otherwiseString = "reports/tests/test/com.palantir.SimpleTest/simpleTest.html") + void formats_test_report_stdout_and_stderr( + GradleInvoker gradle, RootProject rootProject, @InjectByGradleVersion String reportPath) { rootProject .buildGradle() .plugins() @@ -102,16 +111,9 @@ void formats_test_report_stdout_and_stderr(GradleInvoker gradle, RootProject roo assertThat(testResult).task(":compileTestJava").upToDate(); - // Gradle 8 uses classes/com.palantir.SimpleTest.html - // Gradle 9 uses com.palantir.SimpleTest/simpleTest.html - ArbitraryFile legacyReportFile = - rootProject.buildDir().file("reports/tests/test/classes/com.palantir.SimpleTest.html"); - ArbitraryFile newReportFile = - rootProject.buildDir().file("reports/tests/test/com.palantir.SimpleTest/simpleTest.html"); - - ArbitraryFile reportFile = legacyReportFile.path().toFile().exists() ? legacyReportFile : newReportFile; - - reportFile + rootProject + .buildDir() + .file(reportPath) .assertThat() .content() .contains("==Service==") diff --git a/versions.lock b/versions.lock index 0237040b..dba05ce9 100644 --- a/versions.lock +++ b/versions.lock @@ -84,13 +84,13 @@ com.palantir.conjure.java.runtime:conjure-java-jackson-optimizations:8.30.0 (1 c com.palantir.conjure.java.runtime:conjure-java-jackson-serialization:8.30.0 (1 constraints: 3d055b3b) -com.palantir.gradle.plugintesting:configuration-cache-spec:0.50.0 (1 constraints: 3705333b) +com.palantir.gradle.plugintesting:configuration-cache-spec:0.52.0 (1 constraints: 3905393b) -com.palantir.gradle.plugintesting:discover-tests-cli:0.50.0 (1 constraints: 3705333b) +com.palantir.gradle.plugintesting:discover-tests-cli:0.52.0 (1 constraints: 3905393b) -com.palantir.gradle.plugintesting:gradle-plugin-testing-junit:0.50.0 (1 constraints: 3705333b) +com.palantir.gradle.plugintesting:gradle-plugin-testing-junit:0.52.0 (1 constraints: 3905393b) -com.palantir.gradle.plugintesting:plugin-testing-core:0.50.0 (1 constraints: 3705333b) +com.palantir.gradle.plugintesting:plugin-testing-core:0.52.0 (1 constraints: 3905393b) com.palantir.tritium:tritium-registry:0.123.0 (1 constraints: ac1c3ec1) From 86aef96f8217066c5f7804ee679174bfe706f45a Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 2 Feb 2026 15:40:28 +0000 Subject: [PATCH 15/21] move to java --- .../witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java | 0 .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 +- .../java/logging/gradle/testreport/TestLogFilter.java | 0 .../logging/gradle/testreport/TestReportFormattingPlugin.java | 0 .../witchcraft/java/logging/gradle/testreport/Writable.java | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename gradle-witchcraft-logging/src/main/{groovy => java}/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java (100%) rename gradle-witchcraft-logging/src/main/{groovy => java}/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java (98%) rename gradle-witchcraft-logging/src/main/{groovy => java}/com/palantir/witchcraft/java/logging/gradle/testreport/TestLogFilter.java (100%) rename gradle-witchcraft-logging/src/main/{groovy => java}/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java (100%) rename gradle-witchcraft-logging/src/main/{groovy => java}/com/palantir/witchcraft/java/logging/gradle/testreport/Writable.java (100%) diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java similarity index 100% rename from gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java rename to gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/WitchcraftLoggingPlugin.java diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java similarity index 98% rename from gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java rename to gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index 3347ce91..e920cb47 100644 --- a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -86,7 +86,7 @@ String processHtmlContent(String html) { private String formatPreContent(String content) { return Arrays.stream(content.split("\n")) .map(this::formatLine) - .flatMap(Optional::stream) + .mapMulti(Optional::ifPresent) .collect(Collectors.joining("\n")); } diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestLogFilter.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestLogFilter.java similarity index 100% rename from gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestLogFilter.java rename to gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestLogFilter.java diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java similarity index 100% rename from gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java rename to gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/TestReportFormattingPlugin.java diff --git a/gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/Writable.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/Writable.java similarity index 100% rename from gradle-witchcraft-logging/src/main/groovy/com/palantir/witchcraft/java/logging/gradle/testreport/Writable.java rename to gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/Writable.java From 88bbae6986acc776a5ca9991d7e7b2baca315d7f Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Wed, 4 Feb 2026 12:26:52 +0000 Subject: [PATCH 16/21] tidy --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index e920cb47..f2f9dddb 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -92,7 +92,7 @@ private String formatPreContent(String content) { /** * @return formatted line (with trailing newline), empty if filtered out, - * or original if not a witchcraft log + * or original if not a witchcraft log */ private Optional formatLine(String line) { String decoded = StringEscapeUtils.unescapeHtml4(line); From 19296cdd655c7d97e9e17aff8d5b648256b88e8d Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 9 Feb 2026 13:02:50 +0000 Subject: [PATCH 17/21] should be private --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index f2f9dddb..439386c7 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -70,7 +70,7 @@ private void processHtmlFile(Path htmlFile) { } } - String processHtmlContent(String html) { + private String processHtmlContent(String html) { Matcher matcher = OUTPUT_SECTION_PATTERN.matcher(html); StringBuilder result = new StringBuilder(); From c14cff2ac6c821a82a13d362f692d95f09a0be89 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 9 Feb 2026 13:07:18 +0000 Subject: [PATCH 18/21] simplify! --- .../testreport/HtmlReportPostProcessor.java | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index 439386c7..ea75277f 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -24,9 +24,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.Optional; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,7 +58,10 @@ private void walkAndProcessHtmlFiles(File reportDir) { private void processHtmlFile(Path htmlFile) { try { String content = Files.readString(htmlFile, StandardCharsets.UTF_8); - String processed = processHtmlContent(content); + + String processed = OUTPUT_SECTION_PATTERN + .matcher(content) + .replaceAll(match -> match.group(1) + formatPreContent(match.group(2)) + match.group(3)); if (!content.equals(processed)) { Files.writeString(htmlFile, processed, StandardCharsets.UTF_8); @@ -70,23 +71,10 @@ private void processHtmlFile(Path htmlFile) { } } - private String processHtmlContent(String html) { - Matcher matcher = OUTPUT_SECTION_PATTERN.matcher(html); - StringBuilder result = new StringBuilder(); - - while (matcher.find()) { - String replacement = matcher.group(1) + formatPreContent(matcher.group(2)) + matcher.group(3); - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(result); - - return result.toString(); - } - private String formatPreContent(String content) { - return Arrays.stream(content.split("\n")) + return content.lines() .map(this::formatLine) - .mapMulti(Optional::ifPresent) + .flatMap(Optional::stream) .collect(Collectors.joining("\n")); } From c701f5b3ef26dd885318ca3f711290dd0827f862 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 9 Feb 2026 13:08:42 +0000 Subject: [PATCH 19/21] multimap --- .../java/logging/gradle/testreport/HtmlReportPostProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index ea75277f..28b2941a 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -74,7 +74,7 @@ private void processHtmlFile(Path htmlFile) { private String formatPreContent(String content) { return content.lines() .map(this::formatLine) - .flatMap(Optional::stream) + .mapMulti(Optional::ifPresent) .collect(Collectors.joining("\n")); } From 8493c6d40bb921248a7cdf45945834e659fff221 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 9 Feb 2026 13:11:46 +0000 Subject: [PATCH 20/21] simplify! --- .../logging/gradle/testreport/HtmlReportPostProcessor.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index 28b2941a..ece45e43 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -83,9 +83,7 @@ private String formatPreContent(String content) { * or original if not a witchcraft log */ private Optional formatLine(String line) { - String decoded = StringEscapeUtils.unescapeHtml4(line); - - return PARSER.tryParse(decoded) + return PARSER.tryParse(StringEscapeUtils.unescapeHtml4(line)) .map(opt -> opt.map(formatted -> StringEscapeUtils.escapeHtml4(formatted) + "\n")) .orElseGet(() -> Optional.of(line)); } From 964459b48b371b8290363e4310a614f266ba5330 Mon Sep 17 00:00:00 2001 From: Finlay Williams Date: Mon, 9 Feb 2026 13:22:47 +0000 Subject: [PATCH 21/21] replace all not safe --- .../testreport/HtmlReportPostProcessor.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java index ece45e43..d6fc363c 100644 --- a/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java +++ b/gradle-witchcraft-logging/src/main/java/com/palantir/witchcraft/java/logging/gradle/testreport/HtmlReportPostProcessor.java @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,10 +59,7 @@ private void walkAndProcessHtmlFiles(File reportDir) { private void processHtmlFile(Path htmlFile) { try { String content = Files.readString(htmlFile, StandardCharsets.UTF_8); - - String processed = OUTPUT_SECTION_PATTERN - .matcher(content) - .replaceAll(match -> match.group(1) + formatPreContent(match.group(2)) + match.group(3)); + String processed = processHtmlContent(content); if (!content.equals(processed)) { Files.writeString(htmlFile, processed, StandardCharsets.UTF_8); @@ -71,6 +69,19 @@ private void processHtmlFile(Path htmlFile) { } } + private String processHtmlContent(String html) { + Matcher matcher = OUTPUT_SECTION_PATTERN.matcher(html); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String replacement = matcher.group(1) + formatPreContent(matcher.group(2)) + matcher.group(3); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + private String formatPreContent(String content) { return content.lines() .map(this::formatLine)