diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index c37680f13b5d..5561aed07558 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -4,6 +4,7 @@ *Date of Release:* ❓ *Scope:* ❓ +* `ConsoleLauncher` output shows extra diff message for failed assertions on two `CharSequence` objects For a complete list of all _closed_ issues and pull requests for this release, consult the link:{junit5-repo}+/milestone/75?closed=1+[5.12.0-M1] milestone page in the diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a66e230564e..c54d3cd10406 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ jfrunit = { module = "org.moditect.jfrunit:jfrunit-core", version = "1.0.0.Alpha jimfs = { module = "com.google.jimfs:jimfs", version = "1.3.0" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version = "4.12" } joox = { module = "org.jooq:joox", version = "2.0.1" } jte = { module = "gg.jte:jte", version = "3.1.12" } junit4 = { module = "junit:junit", version = { require = "[4.12,)", prefer = "4.13.2" } } diff --git a/junit-platform-console/junit-platform-console.gradle.kts b/junit-platform-console/junit-platform-console.gradle.kts index be24d568d804..591450399a18 100644 --- a/junit-platform-console/junit-platform-console.gradle.kts +++ b/junit-platform-console/junit-platform-console.gradle.kts @@ -16,6 +16,8 @@ dependencies { compileOnly(libs.openTestReporting.events) + implementation(libs.java.diff.utils) + shadowed(libs.picocli) osgiVerification(projects.junitJupiterEngine) @@ -28,7 +30,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 1cdddf814e10..57d92922341d 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 @@ -36,6 +36,8 @@ import org.junit.platform.launcher.listeners.SummaryGeneratingListener; import org.junit.platform.launcher.listeners.TestExecutionSummary; import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener; +import org.opentest4j.AssertionFailedError; +import org.opentest4j.ValueWrapper; /** * @since 1.0 @@ -47,6 +49,8 @@ public class ConsoleTestExecutor { private final TestConsoleOutputOptions outputOptions; private final Supplier launcherSupplier; + private TestPlan testPlanListeners; + public ConsoleTestExecutor(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions) { this(discoveryOptions, outputOptions, LauncherFactory::create); } @@ -77,7 +81,6 @@ private void discoverTests(PrintWriter out) { LauncherDiscoveryRequest discoveryRequest = new DiscoveryRequestCreator().toDiscoveryRequest(discoveryOptions); TestPlan testPlan = launcher.discover(discoveryRequest); - commandLineTestPrinter.ifPresent(printer -> printer.listTests(testPlan)); if (outputOptions.getDetails() != Details.NONE) { printFoundTestsSummary(out, testPlan); @@ -97,12 +100,12 @@ private static void printFoundTestsSummary(PrintWriter out, TestPlan testPlan) { private TestExecutionSummary executeTests(PrintWriter out, Optional reportsDir) { Launcher launcher = launcherSupplier.get(); SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher); - LauncherDiscoveryRequest discoveryRequest = new DiscoveryRequestCreator().toDiscoveryRequest(discoveryOptions); launcher.execute(discoveryRequest); - TestExecutionSummary summary = summaryListener.getSummary(); if (summary.getTotalFailureCount() > 0 || outputOptions.getDetails() != Details.NONE) { + //get testPlan from summaryListener + testPlanListeners = summaryListener.getTestPlan(); printSummary(summary, out); } @@ -181,10 +184,33 @@ private void printSummary(TestExecutionSummary summary, PrintWriter out) { // Otherwise the failures have already been printed in detail if (EnumSet.of(Details.NONE, Details.SUMMARY, Details.TREE).contains(outputOptions.getDetails())) { summary.printFailuresTo(out); + boolean[] diffFlag = { true }; + //adding diff code here + summary.getFailures().forEach(failure -> { + if (diffFlag[0]) { + out.printf("%nDiffs (Markdown):%n"); + diffFlag[0] = false; + } + //get AssertionFailedError + if (failure.getException() instanceof AssertionFailedError) { + AssertionFailedError assertionFailedError = (AssertionFailedError) failure.getException(); + ValueWrapper expected = assertionFailedError.getExpected(); + ValueWrapper actual = assertionFailedError.getActual(); + //apply diff function + if (isCharSequence(expected) && isCharSequence(actual)) { + new DiffPrinter(testPlanListeners).printDiff(out, expected.getStringRepresentation(), + actual.getStringRepresentation(), failure.getTestIdentifier()); + } + } + }); } summary.printTo(out); } + private boolean isCharSequence(ValueWrapper value) { + return value != null && CharSequence.class.isAssignableFrom(value.getType()); + } + @FunctionalInterface public interface Factory { ConsoleTestExecutor create(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiffPrinter.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiffPrinter.java new file mode 100644 index 000000000000..9537bda92e4a --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiffPrinter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.console.tasks; + +import static java.lang.String.join; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; + +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; + +/** + * class provide access to printDiff function + */ +class DiffPrinter { + private final TestPlan testPlan; + + public DiffPrinter(TestPlan testPlan) { + this.testPlan = testPlan; + } + + //print the difference of two print to out + void printDiff(PrintWriter out, String expected, String actual, TestIdentifier testIdentifier) { + char id = testIdentifier.getUniqueId().charAt(testIdentifier.getUniqueId().length() - 4); + out.printf(" (%c) %s:", id == 's' ? '1' : id, describeTest(testIdentifier)); + boolean inlineDiffByWordFlag = false; + if (expected.contains(" ") || actual.contains(" ")) { + inlineDiffByWordFlag = true; + } + DiffRowGenerator generator = DiffRowGenerator.create().showInlineDiffs(true).inlineDiffByWord( + inlineDiffByWordFlag).oldTag(f -> "~~").newTag(f -> "**").build(); + List rows = generator.generateDiffRows(Arrays.asList(expected), Arrays.asList(actual)); + out.println(); + out.println(" | expected | actual |"); + out.println(" | -------- | ------ |"); + for (DiffRow row : rows) { + out.printf(" | %s | %s |", row.getOldLine(), row.getNewLine()); + out.println(); + } + } + + private String describeTest(TestIdentifier testIdentifier) { + List descriptionParts = new ArrayList<>(); + collectTestDescription(testIdentifier, descriptionParts); + return join(":", descriptionParts); + } + + private void collectTestDescription(TestIdentifier identifier, List descriptionParts) { + descriptionParts.add(0, identifier.getDisplayName()); + testPlan.getParent(identifier).ifPresent(parent -> collectTestDescription(parent, descriptionParts)); + } +} 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 0cbe2abd3352..f56de0718045 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 @@ -79,6 +79,7 @@ private void printlnTestDescriptor(Style style, String message, TestIdentifier t private void printlnException(Style style, Throwable throwable) { printlnMessage(style, "Exception", ExceptionUtils.readStackTrace(throwable)); + //add diff here } private void printlnMessage(Style style, String message, String detail) { diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java index a8df758dc114..000b329ffe33 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java @@ -97,6 +97,7 @@ public void executionStarted(TestIdentifier testIdentifier) { @Override public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { testExecutionResult.getThrowable().ifPresent(t -> printDetail(Style.FAILED, "caught", readStackTrace(t))); + //add diff here if (testIdentifier.isContainer()) { Long creationMillis = frames.pop(); printVerticals(theme.end()); 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..5fa912d2eed4 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 @@ -21,5 +21,6 @@ requires org.junit.platform.launcher; requires org.junit.platform.reporting; + provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider; } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java index da39a9a2b6d4..549b7ca95544 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java @@ -203,7 +203,8 @@ public void printFailuresTo(PrintWriter writer, int maxStackTraceLines) { if (getTotalFailureCount() > 0) { writer.printf("%nFailures (%d):%n", getTotalFailureCount()); this.failures.forEach(failure -> { - writer.printf("%s%s%n", TAB, describeTest(failure.getTestIdentifier())); + writer.printf("%s(%c) %s%n", TAB, getTestId(failure.getTestIdentifier()), + describeTest(failure.getTestIdentifier())); printSource(writer, failure.getTestIdentifier()); writer.printf("%s=> %s%n", DOUBLE_TAB, failure.getException()); printStackTrace(writer, failure.getException(), maxStackTraceLines); @@ -223,6 +224,12 @@ private String describeTest(TestIdentifier testIdentifier) { return join(":", descriptionParts); } + //return the unique id of the test + private char getTestId(TestIdentifier testIdentifier) { + char id = testIdentifier.getUniqueId().charAt(testIdentifier.getUniqueId().length() - 4); + return id == 's' ? '1' : id; + } + private void collectTestDescription(TestIdentifier identifier, List descriptionParts) { descriptionParts.add(0, identifier.getDisplayName()); this.testPlan.getParent(identifier).ifPresent(parent -> collectTestDescription(parent, descriptionParts)); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/SummaryGeneratingListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/SummaryGeneratingListener.java index c01c09af2bf9..f881769fc07b 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/SummaryGeneratingListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/SummaryGeneratingListener.java @@ -46,6 +46,13 @@ public TestExecutionSummary getSummary() { return this.summary; } + /** + * Get the testPlan of this listener. + */ + public TestPlan getTestPlan() { + return testPlan; + } + @Override public void testPlanExecutionStarted(TestPlan testPlan) { this.testPlan = testPlan;