From 6daf44e8fb2720c0bc16b2d510fa91a475a85af8 Mon Sep 17 00:00:00 2001 From: Stefan Bechtold Date: Tue, 18 Jul 2023 22:15:15 +0200 Subject: [PATCH 1/2] add a diff section to report of AssertionFailedError With this commit, a dependency to https://github.com/java-diff-utils/java-diff-utils is introduced. java-diff-utils is used to create a diff from the expected and the actual result and report it. See #3139 --- .../release-notes/release-notes-5.10.0.adoc | 1 + gradle/libs.versions.toml | 1 + .../junit-platform-console.gradle.kts | 6 ++- .../console/tasks/ConsoleTestExecutor.java | 1 + .../console/tasks/FlatPrintingListener.java | 39 +++++++++++++++ .../tasks/FlatPrintingListenerTests.java | 47 +++++++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0.adoc index 513732acd741..f60091a861b5 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0.adoc @@ -9,6 +9,7 @@ * New `LauncherInterceptor` SPI * New `testfeed` details mode for `ConsoleLauncher` * New `ConsoleLauncher` subcommand for test discovery without execution +* `ConsoleLauncher` shows expected, actual, and a diff for failed assertions on `CharSequence` objects * Dry-run mode for test execution * New `NamespacedHierarchicalStore` for use in third-party test engines * Stacktrace pruning to hide internal JUnit calls diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 626d7f221c4d..c6ab4ab36ba4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", vers groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.13" } groovy2-bom = { module = "org.codehaus.groovy:groovy-bom", version = "2.5.21" } hamcrest = { module = "org.hamcrest:hamcrest", version = "2.2" } +java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version = "4.12" } jfrunit = { module = "org.moditect.jfrunit:jfrunit-core", version = "1.0.0.Alpha2" } jimfs = { module = "com.google.jimfs:jimfs", version = "1.3.0" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } diff --git a/junit-platform-console/junit-platform-console.gradle.kts b/junit-platform-console/junit-platform-console.gradle.kts index 91acfd91b6d7..8d027fccd363 100644 --- a/junit-platform-console/junit-platform-console.gradle.kts +++ b/junit-platform-console/junit-platform-console.gradle.kts @@ -15,6 +15,8 @@ dependencies { compileOnly(libs.openTestReporting.events) + implementation(libs.java.diff.utils) + shadowed(libs.picocli) osgiVerification(projects.junitJupiterEngine) @@ -27,7 +29,9 @@ tasks { "--add-modules", "org.opentest4j.reporting.events", "--add-reads", "${project.projects.junitPlatformReporting.dependencyProject.javaModuleName}=org.opentest4j.reporting.events", "--add-modules", "info.picocli", - "--add-reads", "${javaModuleName}=info.picocli" + "--add-reads", "${javaModuleName}=info.picocli", + "--add-modules", "io.github.javadiffutils", + "--add-reads", "${javaModuleName}=io.github.javadiffutils" )) } shadowJar { diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java index 694005933cdf..9995841d1321 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java @@ -143,6 +143,7 @@ private SummaryGeneratingListener registerListeners(PrintWriter out, Optional createDetailsPrintingListener(PrintWriter out) { ColorPalette colorPalette = getColorPalette(); Theme theme = outputOptions.getTheme(); + switch (outputOptions.getDetails()) { case SUMMARY: // summary listener is always created and registered diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java index 304cedc27a2f..2db94a8e0122 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java @@ -11,12 +11,20 @@ package org.junit.platform.console.tasks; import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; +import org.opentest4j.AssertionFailedError; +import org.opentest4j.ValueWrapper; /** * @since 1.0 @@ -27,10 +35,19 @@ class FlatPrintingListener implements DetailsPrintingListener { private final PrintWriter out; private final ColorPalette colorPalette; + private final DiffRowGenerator diffRowGenerator; FlatPrintingListener(PrintWriter out, ColorPalette colorPalette) { this.out = out; this.colorPalette = colorPalette; + this.diffRowGenerator = DiffRowGenerator.create() // + .showInlineDiffs(true) // + .mergeOriginalRevised(true) // + .inlineDiffByWord(true) // + .oldTag(f -> "~") // + .newTag(f -> "**") // + .build(); + ; } @Override @@ -78,9 +95,31 @@ private void printlnTestDescriptor(Style style, String message, TestIdentifier t } private void printlnException(Style style, Throwable throwable) { + if (throwable instanceof AssertionFailedError) { + AssertionFailedError assertionFailedError = (AssertionFailedError) throwable; + ValueWrapper expected = assertionFailedError.getExpected(); + ValueWrapper actual = assertionFailedError.getActual(); + + if (isCharSequence(expected) && isCharSequence(actual)) { + printlnMessage(style, "Expected ", expected.getStringRepresentation()); + printlnMessage(style, "Actual ", actual.getStringRepresentation()); + printlnMessage(style, "Diff ", calculateDiff(expected, actual)); + } + } printlnMessage(style, "Exception", ExceptionUtils.readStackTrace(throwable)); } + private boolean isCharSequence(ValueWrapper value) { + return value != null && CharSequence.class.isAssignableFrom(value.getType()); + } + + private String calculateDiff(ValueWrapper expected, ValueWrapper actual) { + List expectedLines = Arrays.asList(expected.getStringRepresentation()); + List actualLines = Arrays.asList(actual.getStringRepresentation()); + List diffRows = diffRowGenerator.generateDiffRows(expectedLines, actualLines); + return diffRows.stream().map(DiffRow::getOldLine).collect(Collectors.joining("\n")); + } + private void printlnMessage(Style style, String message, String detail) { println(style, INDENTATION + "=> " + message + ": %s", indented(detail)); } diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java index a79bd43f85ae..84c5b902f8ef 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java @@ -26,6 +26,7 @@ import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.fakes.TestDescriptorStub; import org.junit.platform.launcher.TestIdentifier; +import org.opentest4j.AssertionFailedError; /** * @since 1.0 @@ -71,6 +72,52 @@ void executionFinishedWithFailure() { () -> assertEquals(INDENTATION + "=> Exception: java.lang.AssertionError: Boom!", lines[1])); } + @Nested + class DiffOutputTests { + @Test + void printDiffForStringsInAssertionFailedErrors() { + var stringWriter = new StringWriter(); + listener(stringWriter).executionFinished(newTestIdentifier(), + failed(new AssertionFailedError("Detail Message", "Expected content", "Actual content"))); + var lines = lines(stringWriter); + + assertTrue(lines.length >= 5, "At least 5 lines are expected in failure report!"); + assertAll("lines in the output", // + () -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), // + () -> assertEquals(INDENTATION + "=> Expected : Expected content", lines[1]), // + () -> assertEquals(INDENTATION + "=> Actual : Actual content", lines[2]), // + () -> assertEquals(INDENTATION + "=> Diff : ~Expected~**Actual** content", lines[3]), // + () -> assertEquals(INDENTATION + "=> Exception: org.opentest4j.AssertionFailedError: Detail Message", + lines[4])); + } + + @Test + void ignoreDiffForNumbersInAssertionFailedErrors() { + var stringWriter = new StringWriter(); + listener(stringWriter).executionFinished(newTestIdentifier(), + failed(new AssertionFailedError("Detail Message", 10, 20))); + var lines = lines(stringWriter); + + assertTrue(lines.length >= 2, "At least 3 lines are expected in failure report!"); + assertAll("lines in the output", // + () -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), // + () -> assertEquals(INDENTATION + "=> Exception: org.opentest4j.AssertionFailedError: Detail Message", + lines[1])); + } + + @Test + void ignoreDiffForAnyAssertionErrors() { + var stringWriter = new StringWriter(); + listener(stringWriter).executionFinished(newTestIdentifier(), failed(new AssertionError("Detail Message"))); + var lines = lines(stringWriter); + + assertTrue(lines.length >= 2, "At least 2 lines are expected in failure report!"); + assertAll("lines in the output", // + () -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), // + () -> assertEquals(INDENTATION + "=> Exception: java.lang.AssertionError: Detail Message", lines[1])); + } + } + @Nested class ColorPaletteTests { From 5dbc7554e8bea9b928b67bc6b4060c4d1865c878 Mon Sep 17 00:00:00 2001 From: Stefan Bechtold Date: Thu, 20 Jul 2023 01:14:22 +0200 Subject: [PATCH 2/2] require io.github.javadiffutils in module info --- .../src/module/org.junit.platform.console/module-info.java | 1 + 1 file changed, 1 insertion(+) diff --git a/junit-platform-console/src/module/org.junit.platform.console/module-info.java b/junit-platform-console/src/module/org.junit.platform.console/module-info.java index 08d28b434f50..8b1961c9881b 100644 --- a/junit-platform-console/src/module/org.junit.platform.console/module-info.java +++ b/junit-platform-console/src/module/org.junit.platform.console/module-info.java @@ -20,6 +20,7 @@ requires org.junit.platform.engine; requires org.junit.platform.launcher; requires org.junit.platform.reporting; + requires io.github.javadiffutils; provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider; }