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

Remove common prefixes and suffixes from actual and expected single-line strings #117

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions src/ScalarComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@
use function mb_strtolower;
use function method_exists;
use function sprintf;
use function strlen;
use function substr;
use SebastianBergmann\Exporter\Exporter;

/**
* Compares scalar or NULL values for equality.
*/
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) &&
Expand Down Expand Up @@ -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.',
);
}
Expand All @@ -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);
}
}
56 changes: 56 additions & 0 deletions tests/unit/ArrayComparatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,41 @@ public static function assertEqualsFailsProvider(): array
];
}

/**
* @return non-empty-list<array{0: string, 1: array, 2: array, 3?: float, 4?: bool}>
*/
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;
Expand Down Expand Up @@ -178,4 +213,25 @@ public function testAssertEqualsFails(array $expected, array $actual, float $del

$this->comparator->assertEquals($expected, $actual, $delta, $canonicalize);
}

/**
* @param array<mixed> $expected
* @param array<mixed> $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());
}
}
}
65 changes: 65 additions & 0 deletions tests/unit/ScalarComparatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,59 @@ public static function assertEqualsFailsProvider(): array
];
}

/**
* @return non-empty-list<array{0: string, 1: string, 2: string}>
*/
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
{
Expand Down Expand Up @@ -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());
}
}
}