Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diff section to report of AssertionFailedError failures #3397

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Expand Up @@ -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" }
Expand Down
6 changes: 5 additions & 1 deletion junit-platform-console/junit-platform-console.gradle.kts
Expand Up @@ -15,6 +15,8 @@ dependencies {

compileOnly(libs.openTestReporting.events)

implementation(libs.java.diff.utils)

shadowed(libs.picocli)

osgiVerification(projects.junitJupiterEngine)
Expand All @@ -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 {
Expand Down
Expand Up @@ -143,6 +143,7 @@ private SummaryGeneratingListener registerListeners(PrintWriter out, Optional<Pa
private Optional<DetailsPrintingListener> createDetailsPrintingListener(PrintWriter out) {
ColorPalette colorPalette = getColorPalette();
Theme theme = outputOptions.getTheme();

switch (outputOptions.getDetails()) {
case SUMMARY:
// summary listener is always created and registered
Expand Down
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String> expectedLines = Arrays.asList(expected.getStringRepresentation());
List<String> actualLines = Arrays.asList(actual.getStringRepresentation());
List<DiffRow> 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));
}
Expand Down
Expand Up @@ -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;
}
Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Expand Down