Skip to content

Commit

Permalink
Assert: match() replaces patterns with their matching values
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik committed Aug 5, 2016
1 parent a28ff10 commit 7c21d99
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 37 deletions.
94 changes: 87 additions & 7 deletions src/Framework/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,12 @@ public static function match($pattern, $actual, $description = NULL)
if (!is_string($pattern)) {
throw new \Exception('Pattern must be a string.');

} elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
self::fail(self::describe('%1 should match %2', $description), $actual, rtrim($pattern));
} elseif (!is_scalar($actual)) {
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);

} elseif (!self::isMatching($pattern, $actual)) {
list($pattern, $actual) = self::expandMatchingPatterns($pattern, $actual);
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
}
}

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

} elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
self::fail(self::describe('%1 should match %2', $description), $actual, rtrim($pattern));
} elseif (!is_scalar($actual)) {
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);

} elseif (!self::isMatching($pattern, $actual)) {
list($pattern, $actual) = self::expandMatchingPatterns($pattern, $actual);
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
}
}

Expand Down Expand Up @@ -475,16 +483,17 @@ public static function with($obj, \Closure $closure)
* @return bool
* @internal
*/
public static function isMatching($pattern, $actual)
public static function isMatching($pattern, $actual, $strict = FALSE)
{
if (!is_string($pattern) && !is_scalar($actual)) {
throw new \Exception('Value and pattern must be strings.');
}

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

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

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


/**
* @return array
* @internal
*/
public static function expandMatchingPatterns($pattern, $actual)
{
if (self::isPcre($pattern)) {
return [$pattern, $actual];
}

$parts = preg_split('#(%)#', $pattern, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = count($parts); $i >= 0; $i--) {
$patternX = implode(array_slice($parts, 0, $i));
$patternY = "$patternX%A?%";
if (self::isMatching($patternY, $actual)) {
$patternZ = implode(array_slice($parts, $i));
break;
}
}

foreach (['%A%', '%A?%'] as $greedyPattern) {
if (substr($patternX, -strlen($greedyPattern)) === $greedyPattern) {
$patternX = substr($patternX, 0, -strlen($greedyPattern));
$patternY = "$patternX%A?%";
$patternZ = $greedyPattern . $patternZ;
break;
}
}

$low = 0;
$high = strlen($actual);
while ($low <= $high) {
$mid = ($low + $high) >> 1;
if (self::isMatching($patternY, substr($actual, 0, $mid))) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}

$low = $high + 2;
$high = strlen($actual);
while ($low <= $high) {
$mid = ($low + $high) >> 1;
if (!self::isMatching($patternX, substr($actual, 0, $mid), TRUE)) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}

$actualX = substr($actual, 0, $high);
$actualZ = substr($actual, $high);

return [
$actualX . rtrim(preg_replace('#[\t ]*\r?\n#', "\n", $patternZ)),
$actualX . rtrim(preg_replace('#[\t ]*\r?\n#', "\n", $actualZ)),
];
}


/**
* Compares two structures. Ignores the identity of objects and the order of keys in the arrays.
* @return bool
Expand Down Expand Up @@ -555,4 +625,14 @@ private static function isEqual($expected, $actual, $level = 0, $objects = NULL)
return $expected === $actual;
}


/**
* @param string
* @return bool
*/
private static function isPcre($pattern)
{
return (bool) preg_match('/^([~#]).+(\1)[imsxUu]*\z/s', $pattern);
}

}
8 changes: 5 additions & 3 deletions src/Runner/TestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,12 @@ private function assessOutputMatchFile(Job $job, $file)

private function assessOutputMatch(Job $job, $content)
{
if (!Tester\Assert::isMatching($content, $job->getOutput())) {
Dumper::saveOutput($job->getFile(), $job->getOutput(), '.actual');
$actual = $job->getOutput();
if (!Tester\Assert::isMatching($content, $actual)) {
list($content, $actual) = Tester\Assert::expandMatchingPatterns($content, $actual);
Dumper::saveOutput($job->getFile(), $actual, '.actual');
Dumper::saveOutput($job->getFile(), $content, '.expected');
return [Runner::FAILED, 'Failed: output should match ' . Dumper::toLine(rtrim($content))];
return [Runner::FAILED, 'Failed: output should match ' . Dumper::toLine($content)];
}
}

Expand Down
81 changes: 57 additions & 24 deletions tests/Framework/Assert.match.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -41,41 +41,74 @@ $matches = [
['%[a-c]+%', 'abc'],
['%[]%', '%[]%'],
['.\\+*?[^]$(){}=!<>|:-#', '.\\+*?[^]$(){}=!<>|:-#'],
['~\d+~', '123'],
['#\d+#', '123'],
];

$notMatches = [
['a', ' a '],
['%a%', "a\nb"],
['%a%', ''],
['%A%', ''],
['a%s%b', "a\nb"],
['%s?%', 'a'],
['a%c%c', 'abbc'],
['a%c%c', 'ac'],
['a%c%c', "a\nc"],
['%d%', ''],
['%i%', '-123.5'],
['%i%', ''],
['%f%', ''],
['%h%', 'gh'],
['%h%', ''],
['%w%', ','],
['%w%', ''],
['%[a-c]+%', 'Abc'],
['', 'a', '', 'a'],
['a', ' a ', 'a', ' a'],
["a\nb", "a\r\nx", "a\nb", "a\nx"],
["a\r\nb", "a\nx", "a\nb", "a\nx"],
["a\t \nb", "a\nx", "a\nb", "a\nx"],
["a\nb", "a\t \nx", "a\nb", "a\nx"],
["a\t\r\n\t ", 'x', 'a', 'x'],
['a', "x\t\r\n\t ", 'a', 'x'],
['%a%', "a\nb", 'a', "a\nb"],
['%a%', '', '%a%', ''],
['%A%', '', '%A%', ''],
['a%s%b', "a\nb", 'a%s%b', "a\nb"],
['%s?%', 'a', '', 'a'],
['a%c%c', 'abbc', 'abc', 'abbc'],
['a%c%c', 'ac', 'acc', 'ac'],
['a%c%c', "a\nc", 'a%c%c', "a\nc"],
['%d%', '', '%d%', ''],
['%i%', '-123.5', '-123', '-123.5'],
['%i%', '', '%i%', ''],
['%f%', '', '%f%', ''],
['%h%', 'gh', '%h%', 'gh'],
['%h%', '', '%h%', ''],
['%w%', ',', '%w%', ','],
['%w%', '', '%w%', ''],
['%[a-c]+%', 'Abc', '%[a-c]+%', 'Abc'],
['foo%d%foo', 'foo123baz', 'foo123foo', 'foo123baz'],
['foo%d%bar', 'foo123baz', 'foo123bar', 'foo123baz'],
['foo%d?%foo', 'foo123baz', 'foo123foo', 'foo123baz'],
['foo%d?%bar', 'foo123baz', 'foo123bar', 'foo123baz'],
['%a%x', 'abc', 'abcx', 'abc'],
['~%d%~', '~123~', '~%d%~', '~123~'],
];

foreach ($matches as $case) {
list($expected, $value) = $case;
Assert::match($expected, $value);
list($expected, $actual) = $case;
Assert::match($expected, $actual);
}

foreach ($notMatches as $case) {
list($expected, $value) = $case;
Assert::exception(function () use ($expected, $value) {
Assert::match($expected, $value);
}, 'Tester\AssertException', '%A% should match %A%');
list($expected, $actual, $expected2, $actual2) = $case;
$expected3 = str_replace('%', '%%', $expected2);
$actual3 = str_replace('%', '%%', $actual2);

$ex = Assert::exception(function () use ($expected, $actual) {
Assert::match($expected, $actual);
}, 'Tester\AssertException', "'$actual3' should match '$expected3'");

Assert::same($expected2, $ex->expected);
Assert::same($actual2, $ex->actual);
}


Assert::same('', Assert::expandMatchingPatterns('', '')[0]);
Assert::same('abc', Assert::expandMatchingPatterns('abc', 'a')[0]);
Assert::same('a', Assert::expandMatchingPatterns('%a?%', 'a')[0]);
Assert::same('123a', Assert::expandMatchingPatterns('%d?%a', '123b')[0]);
Assert::same('a', Assert::expandMatchingPatterns('a', 'a')[0]);
Assert::same('ab', Assert::expandMatchingPatterns('ab', 'abc')[0]);
Assert::same('abcx', Assert::expandMatchingPatterns('%a%x', 'abc')[0]);
Assert::same('a123c', Assert::expandMatchingPatterns('a%d%c', 'a123x')[0]);
Assert::same('a%A%b', Assert::expandMatchingPatterns('a%A%b', 'axc')[0]);


Assert::exception(function () {
Assert::match(NULL, '');
}, 'Exception', 'Pattern must be a string.');
Expand Down
2 changes: 1 addition & 1 deletion tests/Framework/Dumper.dumpException.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $cases = [
'Failed: NULL should not be NULL' => function () { Assert::notSame(NULL, NULL); },
'Failed: boolean should be instance of x' => function () { Assert::type('x', TRUE); },
'Failed: resource should be int' => function () { Assert::type('int', fopen(__FILE__, 'r')); },
"Failed: 'Hello\nWorld' should match\n ... '%a%'" => function () { Assert::match('%a%', "Hello\nWorld"); },
"Failed: 'Hello\nWorld' should match\n ... 'Hello'" => function () { Assert::match('%a%', "Hello\nWorld"); },
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 100), str_repeat('x', 120)); },
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxx****************************************' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 30), str_repeat('x', 30) . str_repeat('*', 40)); },
"Failed: 'xxxxx*****************************************************************...' should be \n ... 'xxxxx'" => function () { Assert::same(str_repeat('x', 5), str_repeat('x', 5) . str_repeat('*', 90)); },
Expand Down
4 changes: 2 additions & 2 deletions tests/Runner/Runner.annotations.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Assert::same([
['httpCode.error1.phptx', $cli ? Runner::PASSED : Runner::FAILED, $cli ? NULL : 'Exited with HTTP code 200 (expected 500)'], // @httpCode is ignored in CLI
['httpCode.error2.phptx', $cli ? Runner::PASSED : Runner::FAILED, $cli ? NULL : 'Exited with HTTP code 500 (expected 200)'], // @httpCode is ignored in CLI
['outputMatch.match.phptx', Runner::PASSED, NULL],
['outputMatch.notmatch.phptx', Runner::FAILED, "Failed: output should match '%a%Hello%a%'"],
['outputMatch.notmatch.phptx', Runner::FAILED, "Failed: output should match '! World !Hello%a%'"],
['outputMatchFile.error.phptx', Runner::FAILED, "Missing matching file '{$path}missing.txt'."],
['outputMatchFile.match.phptx', Runner::PASSED, NULL],
['outputMatchFile.notmatch.phptx', Runner::FAILED, "Failed: output should match '%a%Hello%a%'"],
['outputMatchFile.notmatch.phptx', Runner::FAILED, "Failed: output should match '! World !Hello%a%'"],
['phpIni.phptx', Runner::PASSED, NULL],
['phpversion.match.phptx', Runner::PASSED, NULL],
['phpversion.notmatch.phptx', Runner::SKIPPED, 'Requires PHP < 5.'],
Expand Down

0 comments on commit 7c21d99

Please sign in to comment.