Skip to content

Commit 7c21d99

Browse files
committed
Assert: match() replaces patterns with their matching values
1 parent a28ff10 commit 7c21d99

File tree

5 files changed

+152
-37
lines changed

5 files changed

+152
-37
lines changed

src/Framework/Assert.php

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,12 @@ public static function match($pattern, $actual, $description = NULL)
417417
if (!is_string($pattern)) {
418418
throw new \Exception('Pattern must be a string.');
419419

420-
} elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
421-
self::fail(self::describe('%1 should match %2', $description), $actual, rtrim($pattern));
420+
} elseif (!is_scalar($actual)) {
421+
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
422+
423+
} elseif (!self::isMatching($pattern, $actual)) {
424+
list($pattern, $actual) = self::expandMatchingPatterns($pattern, $actual);
425+
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
422426
}
423427
}
424428

@@ -434,8 +438,12 @@ public static function matchFile($file, $actual, $description = NULL)
434438
if ($pattern === FALSE) {
435439
throw new \Exception("Unable to read file '$file'.");
436440

437-
} elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
438-
self::fail(self::describe('%1 should match %2', $description), $actual, rtrim($pattern));
441+
} elseif (!is_scalar($actual)) {
442+
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
443+
444+
} elseif (!self::isMatching($pattern, $actual)) {
445+
list($pattern, $actual) = self::expandMatchingPatterns($pattern, $actual);
446+
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
439447
}
440448
}
441449

@@ -475,16 +483,17 @@ public static function with($obj, \Closure $closure)
475483
* @return bool
476484
* @internal
477485
*/
478-
public static function isMatching($pattern, $actual)
486+
public static function isMatching($pattern, $actual, $strict = FALSE)
479487
{
480488
if (!is_string($pattern) && !is_scalar($actual)) {
481489
throw new \Exception('Value and pattern must be strings.');
482490
}
483491

484492
$old = ini_set('pcre.backtrack_limit', '10000000');
485493

486-
if (!preg_match('/^([~#]).+(\1)[imsxUu]*\z/s', $pattern)) {
494+
if (!self::isPcre($pattern)) {
487495
$utf8 = preg_match('#\x80-\x{10FFFF}]#u', $pattern) ? 'u' : '';
496+
$suffix = ($strict ? '\z#sU' : '\s*$#sU') . $utf8;
488497
$patterns = static::$patterns + [
489498
'[.\\\\+*?[^$(){|\x00\#]' => '\$0', // preg quoting
490499
'[\t ]*\r?\n' => '[\t ]*\r?\n', // right trim
@@ -496,7 +505,7 @@ public static function isMatching($pattern, $actual)
496505
return $s;
497506
}
498507
}
499-
}, rtrim($pattern)) . '\s*$#sU' . $utf8;
508+
}, rtrim($pattern)) . $suffix;
500509
}
501510

502511
$res = preg_match($pattern, $actual);
@@ -508,6 +517,67 @@ public static function isMatching($pattern, $actual)
508517
}
509518

510519

520+
/**
521+
* @return array
522+
* @internal
523+
*/
524+
public static function expandMatchingPatterns($pattern, $actual)
525+
{
526+
if (self::isPcre($pattern)) {
527+
return [$pattern, $actual];
528+
}
529+
530+
$parts = preg_split('#(%)#', $pattern, -1, PREG_SPLIT_DELIM_CAPTURE);
531+
for ($i = count($parts); $i >= 0; $i--) {
532+
$patternX = implode(array_slice($parts, 0, $i));
533+
$patternY = "$patternX%A?%";
534+
if (self::isMatching($patternY, $actual)) {
535+
$patternZ = implode(array_slice($parts, $i));
536+
break;
537+
}
538+
}
539+
540+
foreach (['%A%', '%A?%'] as $greedyPattern) {
541+
if (substr($patternX, -strlen($greedyPattern)) === $greedyPattern) {
542+
$patternX = substr($patternX, 0, -strlen($greedyPattern));
543+
$patternY = "$patternX%A?%";
544+
$patternZ = $greedyPattern . $patternZ;
545+
break;
546+
}
547+
}
548+
549+
$low = 0;
550+
$high = strlen($actual);
551+
while ($low <= $high) {
552+
$mid = ($low + $high) >> 1;
553+
if (self::isMatching($patternY, substr($actual, 0, $mid))) {
554+
$high = $mid - 1;
555+
} else {
556+
$low = $mid + 1;
557+
}
558+
}
559+
560+
$low = $high + 2;
561+
$high = strlen($actual);
562+
while ($low <= $high) {
563+
$mid = ($low + $high) >> 1;
564+
if (!self::isMatching($patternX, substr($actual, 0, $mid), TRUE)) {
565+
$high = $mid - 1;
566+
} else {
567+
$low = $mid + 1;
568+
}
569+
}
570+
571+
$actualX = substr($actual, 0, $high);
572+
$actualZ = substr($actual, $high);
573+
574+
return [
575+
$actualX . rtrim(preg_replace('#[\t ]*\r?\n#', "\n", $patternZ)),
576+
$actualX . rtrim(preg_replace('#[\t ]*\r?\n#', "\n", $actualZ)),
577+
];
578+
}
579+
580+
511581
/**
512582
* Compares two structures. Ignores the identity of objects and the order of keys in the arrays.
513583
* @return bool
@@ -555,4 +625,14 @@ private static function isEqual($expected, $actual, $level = 0, $objects = NULL)
555625
return $expected === $actual;
556626
}
557627

628+
629+
/**
630+
* @param string
631+
* @return bool
632+
*/
633+
private static function isPcre($pattern)
634+
{
635+
return (bool) preg_match('/^([~#]).+(\1)[imsxUu]*\z/s', $pattern);
636+
}
637+
558638
}

src/Runner/TestHandler.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,12 @@ private function assessOutputMatchFile(Job $job, $file)
202202

203203
private function assessOutputMatch(Job $job, $content)
204204
{
205-
if (!Tester\Assert::isMatching($content, $job->getOutput())) {
206-
Dumper::saveOutput($job->getFile(), $job->getOutput(), '.actual');
205+
$actual = $job->getOutput();
206+
if (!Tester\Assert::isMatching($content, $actual)) {
207+
list($content, $actual) = Tester\Assert::expandMatchingPatterns($content, $actual);
208+
Dumper::saveOutput($job->getFile(), $actual, '.actual');
207209
Dumper::saveOutput($job->getFile(), $content, '.expected');
208-
return [Runner::FAILED, 'Failed: output should match ' . Dumper::toLine(rtrim($content))];
210+
return [Runner::FAILED, 'Failed: output should match ' . Dumper::toLine($content)];
209211
}
210212
}
211213

tests/Framework/Assert.match.phpt

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,41 +41,74 @@ $matches = [
4141
['%[a-c]+%', 'abc'],
4242
['%[]%', '%[]%'],
4343
['.\\+*?[^]$(){}=!<>|:-#', '.\\+*?[^]$(){}=!<>|:-#'],
44+
['~\d+~', '123'],
45+
['#\d+#', '123'],
4446
];
4547

4648
$notMatches = [
47-
['a', ' a '],
48-
['%a%', "a\nb"],
49-
['%a%', ''],
50-
['%A%', ''],
51-
['a%s%b', "a\nb"],
52-
['%s?%', 'a'],
53-
['a%c%c', 'abbc'],
54-
['a%c%c', 'ac'],
55-
['a%c%c', "a\nc"],
56-
['%d%', ''],
57-
['%i%', '-123.5'],
58-
['%i%', ''],
59-
['%f%', ''],
60-
['%h%', 'gh'],
61-
['%h%', ''],
62-
['%w%', ','],
63-
['%w%', ''],
64-
['%[a-c]+%', 'Abc'],
49+
['', 'a', '', 'a'],
50+
['a', ' a ', 'a', ' a'],
51+
["a\nb", "a\r\nx", "a\nb", "a\nx"],
52+
["a\r\nb", "a\nx", "a\nb", "a\nx"],
53+
["a\t \nb", "a\nx", "a\nb", "a\nx"],
54+
["a\nb", "a\t \nx", "a\nb", "a\nx"],
55+
["a\t\r\n\t ", 'x', 'a', 'x'],
56+
['a', "x\t\r\n\t ", 'a', 'x'],
57+
['%a%', "a\nb", 'a', "a\nb"],
58+
['%a%', '', '%a%', ''],
59+
['%A%', '', '%A%', ''],
60+
['a%s%b', "a\nb", 'a%s%b', "a\nb"],
61+
['%s?%', 'a', '', 'a'],
62+
['a%c%c', 'abbc', 'abc', 'abbc'],
63+
['a%c%c', 'ac', 'acc', 'ac'],
64+
['a%c%c', "a\nc", 'a%c%c', "a\nc"],
65+
['%d%', '', '%d%', ''],
66+
['%i%', '-123.5', '-123', '-123.5'],
67+
['%i%', '', '%i%', ''],
68+
['%f%', '', '%f%', ''],
69+
['%h%', 'gh', '%h%', 'gh'],
70+
['%h%', '', '%h%', ''],
71+
['%w%', ',', '%w%', ','],
72+
['%w%', '', '%w%', ''],
73+
['%[a-c]+%', 'Abc', '%[a-c]+%', 'Abc'],
74+
['foo%d%foo', 'foo123baz', 'foo123foo', 'foo123baz'],
75+
['foo%d%bar', 'foo123baz', 'foo123bar', 'foo123baz'],
76+
['foo%d?%foo', 'foo123baz', 'foo123foo', 'foo123baz'],
77+
['foo%d?%bar', 'foo123baz', 'foo123bar', 'foo123baz'],
78+
['%a%x', 'abc', 'abcx', 'abc'],
79+
['~%d%~', '~123~', '~%d%~', '~123~'],
6580
];
6681

6782
foreach ($matches as $case) {
68-
list($expected, $value) = $case;
69-
Assert::match($expected, $value);
83+
list($expected, $actual) = $case;
84+
Assert::match($expected, $actual);
7085
}
7186

7287
foreach ($notMatches as $case) {
73-
list($expected, $value) = $case;
74-
Assert::exception(function () use ($expected, $value) {
75-
Assert::match($expected, $value);
76-
}, 'Tester\AssertException', '%A% should match %A%');
88+
list($expected, $actual, $expected2, $actual2) = $case;
89+
$expected3 = str_replace('%', '%%', $expected2);
90+
$actual3 = str_replace('%', '%%', $actual2);
91+
92+
$ex = Assert::exception(function () use ($expected, $actual) {
93+
Assert::match($expected, $actual);
94+
}, 'Tester\AssertException', "'$actual3' should match '$expected3'");
95+
96+
Assert::same($expected2, $ex->expected);
97+
Assert::same($actual2, $ex->actual);
7798
}
7899

100+
101+
Assert::same('', Assert::expandMatchingPatterns('', '')[0]);
102+
Assert::same('abc', Assert::expandMatchingPatterns('abc', 'a')[0]);
103+
Assert::same('a', Assert::expandMatchingPatterns('%a?%', 'a')[0]);
104+
Assert::same('123a', Assert::expandMatchingPatterns('%d?%a', '123b')[0]);
105+
Assert::same('a', Assert::expandMatchingPatterns('a', 'a')[0]);
106+
Assert::same('ab', Assert::expandMatchingPatterns('ab', 'abc')[0]);
107+
Assert::same('abcx', Assert::expandMatchingPatterns('%a%x', 'abc')[0]);
108+
Assert::same('a123c', Assert::expandMatchingPatterns('a%d%c', 'a123x')[0]);
109+
Assert::same('a%A%b', Assert::expandMatchingPatterns('a%A%b', 'axc')[0]);
110+
111+
79112
Assert::exception(function () {
80113
Assert::match(NULL, '');
81114
}, 'Exception', 'Pattern must be a string.');

tests/Framework/Dumper.dumpException.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ $cases = [
1919
'Failed: NULL should not be NULL' => function () { Assert::notSame(NULL, NULL); },
2020
'Failed: boolean should be instance of x' => function () { Assert::type('x', TRUE); },
2121
'Failed: resource should be int' => function () { Assert::type('int', fopen(__FILE__, 'r')); },
22-
"Failed: 'Hello\nWorld' should match\n ... '%a%'" => function () { Assert::match('%a%', "Hello\nWorld"); },
22+
"Failed: 'Hello\nWorld' should match\n ... 'Hello'" => function () { Assert::match('%a%', "Hello\nWorld"); },
2323
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 100), str_repeat('x', 120)); },
2424
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxx****************************************' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 30), str_repeat('x', 30) . str_repeat('*', 40)); },
2525
"Failed: 'xxxxx*****************************************************************...' should be \n ... 'xxxxx'" => function () { Assert::same(str_repeat('x', 5), str_repeat('x', 5) . str_repeat('*', 90)); },

tests/Runner/Runner.annotations.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ Assert::same([
4949
['httpCode.error1.phptx', $cli ? Runner::PASSED : Runner::FAILED, $cli ? NULL : 'Exited with HTTP code 200 (expected 500)'], // @httpCode is ignored in CLI
5050
['httpCode.error2.phptx', $cli ? Runner::PASSED : Runner::FAILED, $cli ? NULL : 'Exited with HTTP code 500 (expected 200)'], // @httpCode is ignored in CLI
5151
['outputMatch.match.phptx', Runner::PASSED, NULL],
52-
['outputMatch.notmatch.phptx', Runner::FAILED, "Failed: output should match '%a%Hello%a%'"],
52+
['outputMatch.notmatch.phptx', Runner::FAILED, "Failed: output should match '! World !Hello%a%'"],
5353
['outputMatchFile.error.phptx', Runner::FAILED, "Missing matching file '{$path}missing.txt'."],
5454
['outputMatchFile.match.phptx', Runner::PASSED, NULL],
55-
['outputMatchFile.notmatch.phptx', Runner::FAILED, "Failed: output should match '%a%Hello%a%'"],
55+
['outputMatchFile.notmatch.phptx', Runner::FAILED, "Failed: output should match '! World !Hello%a%'"],
5656
['phpIni.phptx', Runner::PASSED, NULL],
5757
['phpversion.match.phptx', Runner::PASSED, NULL],
5858
['phpversion.notmatch.phptx', Runner::SKIPPED, 'Requires PHP < 5.'],

0 commit comments

Comments
 (0)