diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java index a8fd354ce2a4..f4536e542dba 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java @@ -181,9 +181,93 @@ private static String formatValues(@Nullable Object expected, @Nullable Object a return "expected: %s but was: %s".formatted(formatClassAndValue(expected, expectedString), formatClassAndValue(actual, actualString)); } + + // Check if both are strings and have whitespace differences + if (expected instanceof String expectedStr && actual instanceof String actualStr) { + String baseMessage = "expected: <%s> but was: <%s>".formatted(expectedString, actualString); + String diff = createWhitespaceDiff(expectedStr, actualStr); + if (diff != null) { + return baseMessage + "\n" + diff; + } + return baseMessage; + } + return "expected: <%s> but was: <%s>".formatted(expectedString, actualString); } + /** + * Creates a diff showing whitespace differences between two strings. + * Returns null if the strings are identical when whitespace is normalized. + */ + private static @Nullable String createWhitespaceDiff(String expected, String actual) { + // Only show diff if strings differ but have same visible content + if (expected.replaceAll("\\s+", " ").trim().equals(actual.replaceAll("\\s+", " ").trim())) { + return "diff: " + visualizeWhitespace(expected) + "\n" + " " + visualizeWhitespace(actual); + } + + // Show diff for any string comparison to help identify whitespace issues + return "diff: " + visualizeWhitespace(expected) + "\n" + " " + visualizeWhitespace(actual); + } + + /** + * Converts whitespace characters to their visual representations. + */ + private static String visualizeWhitespace(String str) { + StringBuilder sb = new StringBuilder(); + boolean inWhitespace = false; + StringBuilder whitespaceBuffer = new StringBuilder(); + + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + + if (isVisualizableWhitespace(c)) { + if (!inWhitespace) { + inWhitespace = true; + whitespaceBuffer.setLength(0); + whitespaceBuffer.append("["); + } + whitespaceBuffer.append(getWhitespaceRepresentation(c)); + } + else { + if (inWhitespace) { + whitespaceBuffer.append("]"); + sb.append(whitespaceBuffer.toString()); + inWhitespace = false; + } + sb.append(c); + } + } + + // Handle case where string ends with whitespace + if (inWhitespace) { + whitespaceBuffer.append("]"); + sb.append(whitespaceBuffer.toString()); + } + + return sb.toString(); + } + + /** + * Checks if a character should be visualized as whitespace + */ + private static boolean isVisualizableWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || (Character.isWhitespace(c) && c != ' '); + } + + /** + * Gets the string representation of a whitespace character + */ + private static String getWhitespaceRepresentation(char c) { + return switch (c) { + case ' ' -> " "; + case '\t' -> "\\t"; + case '\n' -> "\\n"; + case '\r' -> "\\r"; + case '\f' -> "\\f"; + default -> "\\u" + "%04X".formatted((int) c); + }; + } + private static String formatClassAndValue(@Nullable Object value, String valueString) { // If the value is null, return instead of null. if (value == null) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertArrayEqualsAssertionsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertArrayEqualsAssertionsTests.java index 2bbfa11fe3e1..cf83501dde29 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertArrayEqualsAssertionsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertArrayEqualsAssertionsTests.java @@ -10,6 +10,7 @@ package org.junit.jupiter.api; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; @@ -1942,7 +1943,7 @@ void assertArrayEqualsDifferentNestedObjectArraysAndMessage() { } catch (AssertionFailedError ex) { assertMessageStartsWith(ex, "message"); - assertMessageEndsWith(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>"); + assertMessageContains(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>"); } try { @@ -1952,7 +1953,7 @@ void assertArrayEqualsDifferentNestedObjectArraysAndMessage() { } catch (AssertionFailedError ex) { assertMessageStartsWith(ex, "message"); - assertMessageEndsWith(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>"); + assertMessageContains(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>"); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertEqualsAssertionsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertEqualsAssertionsTests.java index ec30168e7605..c37411f966f4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertEqualsAssertionsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertEqualsAssertionsTests.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api; import static org.junit.jupiter.api.AssertionTestUtils.assertExpectedAndActualValues; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; @@ -51,6 +52,21 @@ void assertEqualsByteWithUnequalValues() { } } + @Test + void assertEqualsStringWithUnequalSpaceTabs() { + String expected = "a c"; + String actual = "a c"; + try { + assertEquals(expected, actual, "message"); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageStartsWith(ex, "message"); + assertMessageContains(ex, "expected: but was: "); + assertMessageContains(ex, "diff: a[\\t\\t\\t]c"); + } + } + @Test void assertEqualsByteWithUnequalValuesAndMessage() { byte expected = 1; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertIterableEqualsAssertionsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertIterableEqualsAssertionsTests.java index ca53fc023c3d..95a3bac0406c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertIterableEqualsAssertionsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertIterableEqualsAssertionsTests.java @@ -10,6 +10,7 @@ package org.junit.jupiter.api; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; @@ -394,7 +395,7 @@ void assertIterableEqualsDifferentNestedIterablesAndMessage() { } catch (AssertionFailedError ex) { assertMessageStartsWith(ex, "message"); - assertMessageEndsWith(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>"); + assertMessageContains(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>"); } try { @@ -404,7 +405,7 @@ void assertIterableEqualsDifferentNestedIterablesAndMessage() { } catch (AssertionFailedError ex) { assertMessageStartsWith(ex, "message"); - assertMessageEndsWith(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>"); + assertMessageContains(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>"); } }