Skip to content

Commit 6daf44e

Browse files
committed
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
1 parent acb6e65 commit 6daf44e

File tree

6 files changed

+94
-1
lines changed

6 files changed

+94
-1
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* New `LauncherInterceptor` SPI
1010
* New `testfeed` details mode for `ConsoleLauncher`
1111
* New `ConsoleLauncher` subcommand for test discovery without execution
12+
* `ConsoleLauncher` shows expected, actual, and a diff for failed assertions on `CharSequence` objects
1213
* Dry-run mode for test execution
1314
* New `NamespacedHierarchicalStore` for use in third-party test engines
1415
* Stacktrace pruning to hide internal JUnit calls

gradle/libs.versions.toml

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", vers
4141
groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.13" }
4242
groovy2-bom = { module = "org.codehaus.groovy:groovy-bom", version = "2.5.21" }
4343
hamcrest = { module = "org.hamcrest:hamcrest", version = "2.2" }
44+
java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version = "4.12" }
4445
jfrunit = { module = "org.moditect.jfrunit:jfrunit-core", version = "1.0.0.Alpha2" }
4546
jimfs = { module = "com.google.jimfs:jimfs", version = "1.3.0" }
4647
jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }

junit-platform-console/junit-platform-console.gradle.kts

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies {
1515

1616
compileOnly(libs.openTestReporting.events)
1717

18+
implementation(libs.java.diff.utils)
19+
1820
shadowed(libs.picocli)
1921

2022
osgiVerification(projects.junitJupiterEngine)
@@ -27,7 +29,9 @@ tasks {
2729
"--add-modules", "org.opentest4j.reporting.events",
2830
"--add-reads", "${project.projects.junitPlatformReporting.dependencyProject.javaModuleName}=org.opentest4j.reporting.events",
2931
"--add-modules", "info.picocli",
30-
"--add-reads", "${javaModuleName}=info.picocli"
32+
"--add-reads", "${javaModuleName}=info.picocli",
33+
"--add-modules", "io.github.javadiffutils",
34+
"--add-reads", "${javaModuleName}=io.github.javadiffutils"
3135
))
3236
}
3337
shadowJar {

junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ private SummaryGeneratingListener registerListeners(PrintWriter out, Optional<Pa
143143
private Optional<DetailsPrintingListener> createDetailsPrintingListener(PrintWriter out) {
144144
ColorPalette colorPalette = getColorPalette();
145145
Theme theme = outputOptions.getTheme();
146+
146147
switch (outputOptions.getDetails()) {
147148
case SUMMARY:
148149
// summary listener is always created and registered

junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java

+39
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@
1111
package org.junit.platform.console.tasks;
1212

1313
import java.io.PrintWriter;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
import com.github.difflib.text.DiffRow;
19+
import com.github.difflib.text.DiffRowGenerator;
1420

1521
import org.junit.platform.commons.util.ExceptionUtils;
1622
import org.junit.platform.engine.TestExecutionResult;
1723
import org.junit.platform.engine.reporting.ReportEntry;
1824
import org.junit.platform.launcher.TestIdentifier;
1925
import org.junit.platform.launcher.TestPlan;
26+
import org.opentest4j.AssertionFailedError;
27+
import org.opentest4j.ValueWrapper;
2028

2129
/**
2230
* @since 1.0
@@ -27,10 +35,19 @@ class FlatPrintingListener implements DetailsPrintingListener {
2735

2836
private final PrintWriter out;
2937
private final ColorPalette colorPalette;
38+
private final DiffRowGenerator diffRowGenerator;
3039

3140
FlatPrintingListener(PrintWriter out, ColorPalette colorPalette) {
3241
this.out = out;
3342
this.colorPalette = colorPalette;
43+
this.diffRowGenerator = DiffRowGenerator.create() //
44+
.showInlineDiffs(true) //
45+
.mergeOriginalRevised(true) //
46+
.inlineDiffByWord(true) //
47+
.oldTag(f -> "~") //
48+
.newTag(f -> "**") //
49+
.build();
50+
;
3451
}
3552

3653
@Override
@@ -78,9 +95,31 @@ private void printlnTestDescriptor(Style style, String message, TestIdentifier t
7895
}
7996

8097
private void printlnException(Style style, Throwable throwable) {
98+
if (throwable instanceof AssertionFailedError) {
99+
AssertionFailedError assertionFailedError = (AssertionFailedError) throwable;
100+
ValueWrapper expected = assertionFailedError.getExpected();
101+
ValueWrapper actual = assertionFailedError.getActual();
102+
103+
if (isCharSequence(expected) && isCharSequence(actual)) {
104+
printlnMessage(style, "Expected ", expected.getStringRepresentation());
105+
printlnMessage(style, "Actual ", actual.getStringRepresentation());
106+
printlnMessage(style, "Diff ", calculateDiff(expected, actual));
107+
}
108+
}
81109
printlnMessage(style, "Exception", ExceptionUtils.readStackTrace(throwable));
82110
}
83111

112+
private boolean isCharSequence(ValueWrapper value) {
113+
return value != null && CharSequence.class.isAssignableFrom(value.getType());
114+
}
115+
116+
private String calculateDiff(ValueWrapper expected, ValueWrapper actual) {
117+
List<String> expectedLines = Arrays.asList(expected.getStringRepresentation());
118+
List<String> actualLines = Arrays.asList(actual.getStringRepresentation());
119+
List<DiffRow> diffRows = diffRowGenerator.generateDiffRows(expectedLines, actualLines);
120+
return diffRows.stream().map(DiffRow::getOldLine).collect(Collectors.joining("\n"));
121+
}
122+
84123
private void printlnMessage(Style style, String message, String detail) {
85124
println(style, INDENTATION + "=> " + message + ": %s", indented(detail));
86125
}

platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java

+47
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.platform.engine.reporting.ReportEntry;
2727
import org.junit.platform.fakes.TestDescriptorStub;
2828
import org.junit.platform.launcher.TestIdentifier;
29+
import org.opentest4j.AssertionFailedError;
2930

3031
/**
3132
* @since 1.0
@@ -71,6 +72,52 @@ void executionFinishedWithFailure() {
7172
() -> assertEquals(INDENTATION + "=> Exception: java.lang.AssertionError: Boom!", lines[1]));
7273
}
7374

75+
@Nested
76+
class DiffOutputTests {
77+
@Test
78+
void printDiffForStringsInAssertionFailedErrors() {
79+
var stringWriter = new StringWriter();
80+
listener(stringWriter).executionFinished(newTestIdentifier(),
81+
failed(new AssertionFailedError("Detail Message", "Expected content", "Actual content")));
82+
var lines = lines(stringWriter);
83+
84+
assertTrue(lines.length >= 5, "At least 5 lines are expected in failure report!");
85+
assertAll("lines in the output", //
86+
() -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), //
87+
() -> assertEquals(INDENTATION + "=> Expected : Expected content", lines[1]), //
88+
() -> assertEquals(INDENTATION + "=> Actual : Actual content", lines[2]), //
89+
() -> assertEquals(INDENTATION + "=> Diff : ~Expected~**Actual** content", lines[3]), //
90+
() -> assertEquals(INDENTATION + "=> Exception: org.opentest4j.AssertionFailedError: Detail Message",
91+
lines[4]));
92+
}
93+
94+
@Test
95+
void ignoreDiffForNumbersInAssertionFailedErrors() {
96+
var stringWriter = new StringWriter();
97+
listener(stringWriter).executionFinished(newTestIdentifier(),
98+
failed(new AssertionFailedError("Detail Message", 10, 20)));
99+
var lines = lines(stringWriter);
100+
101+
assertTrue(lines.length >= 2, "At least 3 lines are expected in failure report!");
102+
assertAll("lines in the output", //
103+
() -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), //
104+
() -> assertEquals(INDENTATION + "=> Exception: org.opentest4j.AssertionFailedError: Detail Message",
105+
lines[1]));
106+
}
107+
108+
@Test
109+
void ignoreDiffForAnyAssertionErrors() {
110+
var stringWriter = new StringWriter();
111+
listener(stringWriter).executionFinished(newTestIdentifier(), failed(new AssertionError("Detail Message")));
112+
var lines = lines(stringWriter);
113+
114+
assertTrue(lines.length >= 2, "At least 2 lines are expected in failure report!");
115+
assertAll("lines in the output", //
116+
() -> assertEquals("Finished: demo-test ([engine:demo-engine])", lines[0]), //
117+
() -> assertEquals(INDENTATION + "=> Exception: java.lang.AssertionError: Detail Message", lines[1]));
118+
}
119+
}
120+
74121
@Nested
75122
class ColorPaletteTests {
76123

0 commit comments

Comments
 (0)