diff --git a/src/ScalarComparator.php b/src/ScalarComparator.php index 9b21fb6..b8121d7 100644 --- a/src/ScalarComparator.php +++ b/src/ScalarComparator.php @@ -16,6 +16,8 @@ use function mb_strtolower; use function method_exists; use function sprintf; +use function strlen; +use function substr; use SebastianBergmann\Exporter\Exporter; /** @@ -23,6 +25,9 @@ */ class ScalarComparator extends Comparator { + private const OVERLONG_THRESHOLD = 40; + private const KEEP_CONTEXT_CHARS = 25; + public function accepts(mixed $expected, mixed $actual): bool { return ((is_scalar($expected) xor null === $expected) && @@ -57,11 +62,14 @@ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, } if ($expectedToCompare !== $actualToCompare && is_string($expected) && is_string($actual)) { + [$cutExpected, $cutActual] = self::removeOverlongCommonPrefix($expected, $actual); + [$cutExpected, $cutActual] = self::removeOverlongCommonSuffix($cutExpected, $cutActual); + throw new ComparisonFailure( $expected, $actual, - $exporter->export($expected), - $exporter->export($actual), + $exporter->export($cutExpected), + $exporter->export($cutActual), 'Failed asserting that two strings are equal.', ); } @@ -81,4 +89,66 @@ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, ); } } + + /** + * @return array{string, string} + */ + private static function removeOverlongCommonPrefix(string $string1, string $string2): array + { + $commonPrefix = self::findCommonPrefix($string1, $string2); + + if (strlen($commonPrefix) > self::OVERLONG_THRESHOLD) { + $string1 = '...' . substr($string1, strlen($commonPrefix) - self::KEEP_CONTEXT_CHARS); + $string2 = '...' . substr($string2, strlen($commonPrefix) - self::KEEP_CONTEXT_CHARS); + } + + return [$string1, $string2]; + } + + private static function findCommonPrefix(string $string1, string $string2): string + { + for ($i = 0; $i < strlen($string1); $i++) { + if (!isset($string2[$i]) || $string1[$i] != $string2[$i]) { + break; + } + } + + return substr($string1, 0, $i); + } + + /** + * @return array{string, string} + */ + private static function removeOverlongCommonSuffix(string $string1, string $string2): array + { + $commonSuffix = self::findCommonSuffix($string1, $string2); + + if (strlen($commonSuffix) > self::OVERLONG_THRESHOLD) { + $string1 = substr($string1, 0, -(strlen($commonSuffix) - self::KEEP_CONTEXT_CHARS)) . '...'; + $string2 = substr($string2, 0, -(strlen($commonSuffix) - self::KEEP_CONTEXT_CHARS)) . '...'; + } + + return [$string1, $string2]; + } + + private static function findCommonSuffix(string $string1, string $string2): string + { + $lastCharIndex1 = strlen($string1) - 1; + $lastCharIndex2 = strlen($string2) - 1; + + if ($string1[$lastCharIndex1] != $string2[$lastCharIndex2]) { + return ''; + } + + while ( + $lastCharIndex1 > 0 && + $lastCharIndex2 > 0 && + $string1[$lastCharIndex1] == $string2[$lastCharIndex2] + ) { + $lastCharIndex1--; + $lastCharIndex2--; + } + + return substr($string1, $lastCharIndex1 - strlen($string1) + 1); + } } diff --git a/tests/unit/ArrayComparatorTest.php b/tests/unit/ArrayComparatorTest.php index 96ceadd..6cd5a2d 100644 --- a/tests/unit/ArrayComparatorTest.php +++ b/tests/unit/ArrayComparatorTest.php @@ -124,6 +124,41 @@ public static function assertEqualsFailsProvider(): array ]; } + /** + * @return non-empty-list + */ + public static function assertEqualsFailsWithDiffProvider(): array + { + return [ + [ + " +--- Expected ++++ Actual +@@ @@ + Array ( +- 0 => 'Too short to cut XYZ' ++ 0 => 'Too short to cut HERE' + ) +", + ['Too short to cut XYZ'], + ['Too short to cut HERE'], + ], + [ + " +--- Expected ++++ Actual +@@ @@ + Array ( +- 0 => '... contains important clue XYZ and more behind' ++ 0 => '... contains important clue HERE and more behind' + ) +", + ['Some really long string that just keeps going and going and going but contains important clue XYZ and more behind'], + ['Some really long string that just keeps going and going and going but contains important clue HERE and more behind'], + ], + ]; + } + protected function setUp(): void { $this->comparator = new ArrayComparator; @@ -178,4 +213,25 @@ public function testAssertEqualsFails(array $expected, array $actual, float $del $this->comparator->assertEquals($expected, $actual, $delta, $canonicalize); } + + /** + * @param array $expected + * @param array $actual + */ + #[DataProvider('assertEqualsFailsWithDiffProvider')] + public function testAssertEqualsFailsWithDiff( + string $expectedDiff, + array $expected, + array $actual, + float $delta = 0.0, + bool $canonicalize = false + ): void { + try { + $this->comparator->assertEquals($expected, $actual, $delta, $canonicalize); + $this->fail('Expected ComparisonFailure not thrown'); + } catch (ComparisonFailure $e) { + $this->assertEquals('Failed asserting that two arrays are equal.', $e->getMessage()); + $this->assertEquals($expectedDiff, $e->getDiff()); + } + } } diff --git a/tests/unit/ScalarComparatorTest.php b/tests/unit/ScalarComparatorTest.php index 275855a..bd2a820 100644 --- a/tests/unit/ScalarComparatorTest.php +++ b/tests/unit/ScalarComparatorTest.php @@ -123,6 +123,59 @@ public static function assertEqualsFailsProvider(): array ]; } + /** + * @return non-empty-list + */ + public static function assertEqualsFailsWithDiffProvider(): array + { + return [ + [ + " +--- Expected ++++ Actual +@@ @@ +-'string' ++'other string' +", + 'string', + 'other string', + ], + [ + " +--- Expected ++++ Actual +@@ @@ +-'...string which will be cut HERE some trailer' ++'...string which will be cut XYZ some trailer' +", + 'too too too long string which will be cut HERE some trailer', + 'too too too long string which will be cut XYZ some trailer', + ], + [ + " +--- Expected ++++ Actual +@@ @@ +-'short start until HERE some llooooooooonnng llo...' ++'short start until XYZ some llooooooooonnng llo...' +", + 'short start until HERE some llooooooooonnng llooooooooonnng llooooooooonnng llooooooooonnng trailer', + 'short start until XYZ some llooooooooonnng llooooooooonnng llooooooooonnng llooooooooonnng trailer', + ], + [ + " +--- Expected ++++ Actual +@@ @@ +-'...string which will be cut HERE some llooooooooonnng llo...' ++'...string which will be cut XYZ some llooooooooonnng llo...' +", + 'too too too long string which will be cut HERE some llooooooooonnng llooooooooonnng llooooooooonnng llooooooooonnng trailer', + 'too too too long string which will be cut XYZ some llooooooooonnng llooooooooonnng llooooooooonnng llooooooooonnng trailer', + ], + ]; + } + #[DataProvider('acceptsSucceedsProvider')] public function testAcceptsSucceeds(mixed $expected, mixed $actual): void { @@ -160,4 +213,16 @@ public function testAssertEqualsFails(mixed $expected, mixed $actual, string $me (new ScalarComparator)->assertEquals($expected, $actual); } + + #[DataProvider('assertEqualsFailsWithDiffProvider')] + public function testAssertEqualsFailsWithDiff(string $expectedDiff, string $expected, string $actual): void + { + try { + (new ScalarComparator)->assertEquals($expected, $actual); + $this->fail('Expected ComparisonFailure not thrown'); + } catch (ComparisonFailure $e) { + $this->assertEquals('Failed asserting that two strings are equal.', $e->getMessage()); + $this->assertEquals($expectedDiff, $e->getDiff()); + } + } }