diff --git a/src/Matcher/ArrayMatcher.php b/src/Matcher/ArrayMatcher.php index 2168f336..418749d1 100644 --- a/src/Matcher/ArrayMatcher.php +++ b/src/Matcher/ArrayMatcher.php @@ -6,6 +6,10 @@ use Coduo\PHPMatcher\Backtrace; use Coduo\PHPMatcher\Exception\Exception; +use Coduo\PHPMatcher\Matcher\ArrayMatcher\Diff; +use Coduo\PHPMatcher\Matcher\ArrayMatcher\Difference; +use Coduo\PHPMatcher\Matcher\ArrayMatcher\StringDifference; +use Coduo\PHPMatcher\Matcher\ArrayMatcher\ValuePatternDifference; use Coduo\PHPMatcher\Parser; use Coduo\ToString\StringConverter; @@ -36,8 +40,11 @@ final class ArrayMatcher extends Matcher private Backtrace $backtrace; + private Diff $diff; + public function __construct(ValueMatcher $propertyMatcher, Backtrace $backtrace, Parser $parser) { + $this->diff = new Diff(); $this->propertyMatcher = $propertyMatcher; $this->parser = $parser; $this->backtrace = $backtrace; @@ -54,8 +61,9 @@ public function match($value, $pattern) : bool } if (!\is_array($value)) { - $this->error = \sprintf('%s "%s" is not a valid array.', \gettype($value), new StringConverter($value)); - $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error); + $this->addValuePatternDifference($value, $pattern); + + $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError()); return false; } @@ -65,7 +73,7 @@ public function match($value, $pattern) : bool } if (!$this->iterateMatch($value, $pattern)) { - $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error); + $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError()); return false; } @@ -80,6 +88,20 @@ public function canMatch($pattern) : bool return \is_array($pattern) || $this->isArrayPattern($pattern); } + public function getError() : ?string + { + if (!$this->diff->count()) { + return null; + } + + return \implode("\n", \array_map(fn (Difference $difference) : string => $difference->format(), $this->diff->all())); + } + + public function clearError() : void + { + $this->diff = new Diff(); + } + private function isArrayPattern($pattern) : bool { if (!\is_string($pattern)) { @@ -126,7 +148,7 @@ private function iterateMatch(array $values, array $patterns, string $parentPath continue; } - if ($this->valueMatchPattern($value, $pattern)) { + if ($this->valueMatchPattern($value, $pattern, $this->formatFullPath($parentPath, $path))) { continue; } @@ -135,7 +157,9 @@ private function iterateMatch(array $values, array $patterns, string $parentPath } if ($this->isArrayPattern($pattern)) { - if (!$this->allExpandersMatch($value, $pattern)) { + if (!$this->allExpandersMatch($value, $pattern, $parentPath)) { + $this->addValuePatternDifference($value, $parentPath, $this->formatFullPath($parentPath, $path)); + return false; } @@ -200,13 +224,15 @@ private function findNotExistingKeys(array $patterns, array $values) : array }, ARRAY_FILTER_USE_BOTH); } - private function valueMatchPattern($value, $pattern) : bool + private function valueMatchPattern($value, $pattern, $parentPath) : bool { $match = $this->propertyMatcher->canMatch($pattern) && $this->propertyMatcher->match($value, $pattern); if (!$match) { - $this->error = $this->propertyMatcher->getError(); + if (!\is_array($value)) { + $this->addValuePatternDifference($value, $pattern, $parentPath); + } } return $match; @@ -230,7 +256,7 @@ private function getValueByPath(array $array, string $path) private function setMissingElementInError(string $place, string $path) : void { - $this->error = \sprintf('There is no element under path %s in %s.', $path, $place); + $this->diff = $this->diff->add(new StringDifference(\sprintf('There is no element under path %s in %s.', $path, $place))); } private function formatAccessPath($key) : string @@ -253,13 +279,14 @@ private function shouldSkipValueMatchingFor($lastPattern) : bool return $lastPattern === self::UNBOUNDED_PATTERN; } - private function allExpandersMatch($value, $pattern) : bool + private function allExpandersMatch($value, $pattern, $parentPath = '') : bool { $typePattern = $this->parser->parse($pattern); if (!$typePattern->matchExpanders($value)) { - $this->error = $typePattern->getError(); - $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error); + $this->addValuePatternDifference($value, $pattern, $parentPath); + + $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError()); return false; } @@ -268,4 +295,13 @@ private function allExpandersMatch($value, $pattern) : bool return true; } + + private function addValuePatternDifference($value, $pattern, string $path = '') : void + { + $this->diff = $this->diff->add(new ValuePatternDifference( + (string) new StringConverter($value), + (string) new StringConverter($pattern), + $path ? $path : 'root' + )); + } } diff --git a/src/Matcher/ArrayMatcher/Diff.php b/src/Matcher/ArrayMatcher/Diff.php new file mode 100644 index 00000000..b0264d49 --- /dev/null +++ b/src/Matcher/ArrayMatcher/Diff.php @@ -0,0 +1,36 @@ +differences = $difference; + } + + public function add(Difference $difference) : self + { + return new self(...\array_merge($this->differences, [$difference])); + } + + /** + * @return Difference[] + */ + public function all() : array + { + return $this->differences; + } + + public function count() : int + { + return \count($this->differences); + } +} diff --git a/src/Matcher/ArrayMatcher/Difference.php b/src/Matcher/ArrayMatcher/Difference.php new file mode 100644 index 00000000..d901f31c --- /dev/null +++ b/src/Matcher/ArrayMatcher/Difference.php @@ -0,0 +1,8 @@ +description = $description; + } + + public function format() : string + { + return $this->description; + } +} diff --git a/src/Matcher/ArrayMatcher/ValuePatternDifference.php b/src/Matcher/ArrayMatcher/ValuePatternDifference.php new file mode 100644 index 00000000..1d9e4ce9 --- /dev/null +++ b/src/Matcher/ArrayMatcher/ValuePatternDifference.php @@ -0,0 +1,26 @@ +value = $value; + $this->pattern = $pattern; + $this->path = $path; + } + + public function format() : string + { + return "Value \"{$this->value}\" does not match pattern \"{$this->pattern}\" at path: \"{$this->path}\""; + } +} diff --git a/src/Matcher/ChainMatcher.php b/src/Matcher/ChainMatcher.php index b5a1bdb3..714ffb18 100644 --- a/src/Matcher/ChainMatcher.php +++ b/src/Matcher/ChainMatcher.php @@ -5,6 +5,7 @@ namespace Coduo\PHPMatcher\Matcher; use Coduo\PHPMatcher\Backtrace; +use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json; use Coduo\PHPMatcher\Value\SingleLineString; use Coduo\ToString\StringConverter; @@ -19,6 +20,11 @@ final class ChainMatcher extends Matcher */ private array $matchers = []; + /** + * @var array + */ + private array $matcherErrors; + /** * @param Backtrace $backtrace * @param ValueMatcher[] $matchers @@ -42,16 +48,23 @@ public function match($value, $pattern) : bool return true; } + $this->matcherErrors[\get_class($propertyMatcher)] = (string) $propertyMatcher->getError(); $this->error = $propertyMatcher->getError(); } } if (!isset($this->error)) { - $this->error = \sprintf( - 'Any matcher from chain can\'t match value "%s" to pattern "%s"', - new SingleLineString((string) new StringConverter($value)), - new SingleLineString((string) new StringConverter($pattern)) - ); + if (\is_array($value) && isset($this->matcherErrors[ArrayMatcher::class])) { + $this->error = $this->matcherErrors[ArrayMatcher::class]; + } elseif (Json::isValidPattern($pattern) && isset($this->matcherErrors[JsonMatcher::class])) { + $this->error = $this->matcherErrors[JsonMatcher::class]; + } else { + $this->error = \sprintf( + 'Any matcher from chain can\'t match value "%s" to pattern "%s"', + new SingleLineString((string) new StringConverter($value)), + new SingleLineString((string) new StringConverter($pattern)) + ); + } } $this->backtrace->matcherFailed($this->matcherName(), $value, $pattern, $this->error); diff --git a/src/Matcher/JsonMatcher.php b/src/Matcher/JsonMatcher.php index bd790fcb..fffd8cdc 100644 --- a/src/Matcher/JsonMatcher.php +++ b/src/Matcher/JsonMatcher.php @@ -6,8 +6,6 @@ use Coduo\PHPMatcher\Backtrace; use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json; -use Coduo\PHPMatcher\Value\SingleLineString; -use Coduo\ToString\StringConverter; final class JsonMatcher extends Matcher { @@ -50,11 +48,7 @@ public function match($value, $pattern) : bool $match = $this->arrayMatcher->match(\json_decode($value, true), \json_decode($transformedPattern, true)); if (!$match) { - $this->error = \sprintf( - 'Value %s does not match pattern %s', - new SingleLineString((string) new StringConverter($value)), - new SingleLineString((string) new StringConverter($transformedPattern)) - ); + $this->error = $this->arrayMatcher->getError(); $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error); diff --git a/tests/BacktraceTest/failed_complex_matching_expected_trace.txt b/tests/BacktraceTest/failed_complex_matching_expected_trace.txt index 6448b4be..332ea941 100644 --- a/tests/BacktraceTest/failed_complex_matching_expected_trace.txt +++ b/tests/BacktraceTest/failed_complex_matching_expected_trace.txt @@ -306,13 +306,13 @@ #306 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) failed to match value "false" with "expr(value == true)" pattern #307 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) error: boolean "false" is not a valid string. #308 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher failed to match value "Array(3)" with "Array(3)" pattern -#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: boolean "false" is not a valid string. +#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]" #310 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern -#311 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"} +#311 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]" #312 Matcher Coduo\PHPMatcher\Matcher\XmlMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" #313 Matcher Coduo\PHPMatcher\Matcher\OrMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" #314 Matcher Coduo\PHPMatcher\Matcher\TextMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" #315 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern -#316 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"} +#316 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]" #317 Matcher Coduo\PHPMatcher\Matcher failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern -#318 Matcher Coduo\PHPMatcher\Matcher error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@array@.isEmpty()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"} \ No newline at end of file +#318 Matcher Coduo\PHPMatcher\Matcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]" \ No newline at end of file diff --git a/tests/Matcher/ArrayMatcherTest.php b/tests/Matcher/ArrayMatcherTest.php index 0a72bccd..bd987fbc 100644 --- a/tests/Matcher/ArrayMatcherTest.php +++ b/tests/Matcher/ArrayMatcherTest.php @@ -16,7 +16,44 @@ class ArrayMatcherTest extends TestCase private Backtrace $backtrace; - public static function positiveMatchData() + public function setUp() : void + { + $this->backtrace = new Backtrace\InMemoryBacktrace(); + $parser = new Parser(new Lexer(), new Parser\ExpanderInitializer($this->backtrace)); + + $matchers = [ + new Matcher\CallbackMatcher($this->backtrace), + new Matcher\NullMatcher($this->backtrace), + new Matcher\StringMatcher($this->backtrace, $parser), + new Matcher\IntegerMatcher($this->backtrace, $parser), + new Matcher\BooleanMatcher($this->backtrace, $parser), + new Matcher\DoubleMatcher($this->backtrace, $parser), + new Matcher\NumberMatcher($this->backtrace, $parser), + new Matcher\DateMatcher($this->backtrace, $parser), + new Matcher\ScalarMatcher($this->backtrace), + new Matcher\WildcardMatcher($this->backtrace), + ]; + + if (\class_exists('Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage')) { + $matchers[] = new Matcher\ExpressionMatcher($this->backtrace); + } + + $this->matcher = new Matcher\ArrayMatcher( + new Matcher\ChainMatcher(self::class, $this->backtrace, $matchers), + $this->backtrace, + $parser + ); + } + + /** + * @dataProvider positiveMatchData + */ + public function test_positive_match_arrays($value, $pattern) : void + { + $this->assertTrue($this->matcher->match($value, $pattern)); + } + + public function positiveMatchData() { $simpleArr = [ 'users' => [ @@ -111,7 +148,16 @@ public static function positiveMatchData() ]; } - public static function negativeMatchData() + /** + * @dataProvider negativeMatchData + */ + public function test_negative_match_arrays($value, $pattern, string $error) : void + { + $this->assertFalse($this->matcher->match($value, $pattern)); + $this->assertSame($error, $this->matcher->getError()); + } + + public function negativeMatchData() { $simpleArr = [ 'users' => [ @@ -162,18 +208,22 @@ public static function negativeMatchData() ]; return [ - [$simpleArr, $simpleDiff], - [$simpleArr, $simpleArrPatternWithUniversalKeyAndIntegerValue], - [['status' => 'ok', 'data' => [['foo']]], ['status' => 'ok', 'data' => []]], - [[1], []], - [[], ['key' => []]], - [['key' => 'val'], ['key' => 'val2']], - [[1], [2]], - [['foo', 1, 3], ['foo', 2, 3]], - [[], ['key' => []]], - [[], ['foo' => 'bar']], - [[], ['foo' => ['bar' => []]]], - [['key' => 'val', 'key2' => 'val2'], ['not key' => 'val', '@*@' => '@*@']], + [$simpleArr, $simpleDiff, 'Value "Michał" does not match pattern "Pablo" at path: "[users][1][firstName]"'], + [$simpleArr, $simpleArrPatternWithUniversalKeyAndIntegerValue, 'Value "Orzechowicz" does not match pattern "@integer@" at path: "[users][0][lastName]"'], + [['status' => 'ok', 'data' => [['foo']]], ['status' => 'ok', 'data' => []], 'There is no element under path [data][0] in pattern.'], + [[1], [], 'There is no element under path [0] in pattern.'], + [[], ['key' => []], 'There is no element under path [key] in value.'], + [['key' => 'val'], ['key' => 'val2'], 'Value "val" does not match pattern "val2" at path: "[key]"'], + [[1], [2], 'Value "1" does not match pattern "2" at path: "[0]"'], + [['foo', 1, 3], ['foo', 2, 3], 'Value "1" does not match pattern "2" at path: "[1]"'], + [[], ['key' => []], 'There is no element under path [key] in value.'], + [[], ['foo' => 'bar'], 'There is no element under path [foo] in value.'], + [ + [], + ['foo' => ['bar' => []]], + "There is no element under path [bar] in value.\nThere is no element under path [foo] in value.", + ], + [['key' => 'val', 'key2' => 'val2'], ['not key' => 'val', '@*@' => '@*@'], 'There is no element under path [not key] in value.'], 'unbound array should match one or none elements' => [ [ 'users' => [ @@ -188,55 +238,11 @@ public static function negativeMatchData() 6.66, ], $simpleDiff, + 'Value "Foobar" does not match pattern "Orzechowicz" at path: "[users][0][lastName]"', ], ]; } - public function setUp() : void - { - $this->backtrace = new Backtrace\InMemoryBacktrace(); - $parser = new Parser(new Lexer(), new Parser\ExpanderInitializer($this->backtrace)); - - $matchers = [ - new Matcher\CallbackMatcher($this->backtrace), - new Matcher\NullMatcher($this->backtrace), - new Matcher\StringMatcher($this->backtrace, $parser), - new Matcher\IntegerMatcher($this->backtrace, $parser), - new Matcher\BooleanMatcher($this->backtrace, $parser), - new Matcher\DoubleMatcher($this->backtrace, $parser), - new Matcher\NumberMatcher($this->backtrace, $parser), - new Matcher\DateMatcher($this->backtrace, $parser), - new Matcher\ScalarMatcher($this->backtrace), - new Matcher\WildcardMatcher($this->backtrace), - ]; - - if (\class_exists('Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage')) { - $matchers[] = new Matcher\ExpressionMatcher($this->backtrace); - } - - $this->matcher = new Matcher\ArrayMatcher( - new Matcher\ChainMatcher(self::class, $this->backtrace, $matchers), - $this->backtrace, - $parser - ); - } - - /** - * @dataProvider positiveMatchData - */ - public function test_positive_match_arrays($value, $pattern) : void - { - $this->assertTrue($this->matcher->match($value, $pattern)); - } - - /** - * @dataProvider negativeMatchData - */ - public function test_negative_match_arrays($value, $pattern) : void - { - $this->assertFalse($this->matcher->match($value, $pattern)); - } - public function test_negative_match_when_cant_find_matcher_that_can_match_array_element() : void { $matcher = new Matcher\ArrayMatcher( @@ -298,14 +304,14 @@ public function test_error_when_path_in_nested_value_does_not_exist() : void public function test_error_when_matching_fail() : void { $this->assertFalse($this->matcher->match(['foo' => 'foo value'], ['foo' => 'bar value'])); - $this->assertEquals($this->matcher->getError(), '"foo value" does not match "bar value".'); + $this->assertEquals($this->matcher->getError(), 'Value "foo value" does not match pattern "bar value" at path: "[foo]"'); $this->assertFalse($this->backtrace->isEmpty()); } public function test_error_message_when_matching_non_array_value() : void { $this->assertFalse($this->matcher->match(new \DateTime(), '@array@')); - $this->assertEquals($this->matcher->getError(), 'object "\\DateTime" is not a valid array.'); + $this->assertEquals($this->matcher->getError(), 'Value "\DateTime" does not match pattern "@array@" at path: "root"'); $this->assertFalse($this->backtrace->isEmpty()); } @@ -346,7 +352,7 @@ public function test_array_previous_pattern() : void '@array_previous@', ] ), - $this->matcher->getError() + (string) $this->matcher->getError() ); } @@ -371,7 +377,7 @@ public function test_array_previous_repeat_pattern() : void '@array_previous_repeat@', ] ), - $this->matcher->getError() + (string) $this->matcher->getError() ); } @@ -396,7 +402,7 @@ public function test_invalid_array_previous_repeat_pattern() : void ); $this->assertSame( - '"4" does not match "@string@".', + 'Value "4" does not match pattern "@string@" at path: "[3][id]"', $this->matcher->getError() ); } diff --git a/tests/Matcher/ChainMatcherTest.php b/tests/Matcher/ChainMatcherTest.php index dbde9b31..f2a006da 100644 --- a/tests/Matcher/ChainMatcherTest.php +++ b/tests/Matcher/ChainMatcherTest.php @@ -71,14 +71,14 @@ public function test_first_matcher_match() : void public function test_if_there_is_error_description_only_from_last_matcher_that_fails() : void { - $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); - $this->firstMatcher->expects($this->once())->method('match')->will($this->returnValue(false)); - $this->firstMatcher->expects($this->once())->method('getError') + $this->firstMatcher->expects($this->any())->method('canMatch')->will($this->returnValue(true)); + $this->firstMatcher->expects($this->any())->method('match')->will($this->returnValue(false)); + $this->firstMatcher->expects($this->any())->method('getError') ->will($this->returnValue('First matcher error')); - $this->secondMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); - $this->secondMatcher->expects($this->once())->method('match')->will($this->returnValue(false)); - $this->secondMatcher->expects($this->once())->method('getError') + $this->secondMatcher->expects($this->any())->method('canMatch')->will($this->returnValue(true)); + $this->secondMatcher->expects($this->any())->method('match')->will($this->returnValue(false)); + $this->secondMatcher->expects($this->any())->method('getError') ->will($this->returnValue('Second matcher error')); $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), false); diff --git a/tests/Matcher/JsonMatcherTest.php b/tests/Matcher/JsonMatcherTest.php index 334be604..b9f015ff 100644 --- a/tests/Matcher/JsonMatcherTest.php +++ b/tests/Matcher/JsonMatcherTest.php @@ -7,14 +7,49 @@ use Coduo\PHPMatcher\Backtrace; use Coduo\PHPMatcher\Lexer; use Coduo\PHPMatcher\Matcher; +use Coduo\PHPMatcher\Matcher\JsonMatcher; use Coduo\PHPMatcher\Parser; use PHPUnit\Framework\TestCase; class JsonMatcherTest extends TestCase { - private ?\Coduo\PHPMatcher\Matcher\JsonMatcher $matcher = null; + private ?JsonMatcher $matcher = null; - public static function positivePatterns() + public function setUp() : void + { + $backtrace = new Backtrace\InMemoryBacktrace(); + $parser = new Parser(new Lexer(), new Parser\ExpanderInitializer($backtrace)); + $scalarMatchers = new Matcher\ChainMatcher( + self::class, + $backtrace, + [ + new Matcher\CallbackMatcher($backtrace), + new Matcher\ExpressionMatcher($backtrace), + new Matcher\NullMatcher($backtrace), + new Matcher\StringMatcher($backtrace, $parser), + new Matcher\IntegerMatcher($backtrace, $parser), + new Matcher\BooleanMatcher($backtrace, $parser), + new Matcher\DoubleMatcher($backtrace, $parser), + new Matcher\NumberMatcher($backtrace, $parser), + new Matcher\ScalarMatcher($backtrace), + new Matcher\WildcardMatcher($backtrace), + ] + ); + $this->matcher = new JsonMatcher( + new Matcher\ArrayMatcher($scalarMatchers, $backtrace, $parser), + $backtrace + ); + } + + /** + * @dataProvider positivePatterns + */ + public function test_positive_can_match($pattern) : void + { + $this->assertTrue($this->matcher->canMatch($pattern)); + } + + public function positivePatterns() { return [ [\json_encode(['Norbert', 'Michał'])], @@ -23,7 +58,15 @@ public static function positivePatterns() ]; } - public static function negativePatterns() + /** + * @dataProvider negativePatterns + */ + public function test_negative_can_match($pattern) : void + { + $this->assertFalse($this->matcher->canMatch($pattern)); + } + + public function negativePatterns() { return [ ['@string@'], @@ -32,7 +75,15 @@ public static function negativePatterns() ]; } - public static function positiveMatches() + /** + * @dataProvider positiveMatches + */ + public function test_positive_matches($value, $pattern) : void + { + $this->assertTrue($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); + } + + public function positiveMatches() { return [ [ @@ -115,41 +166,15 @@ public static function positiveMatches() ]; } - public static function negativeMatches() + /** + * @dataProvider normalizationRequiredDataProvider + */ + public function test_positive_matches_after_normalization($value, $pattern) : void { - return [ - [ - '{"users":["Norbert","Michał"]}', - '{"users":["Michał","@string@"]}', - ], - [ - '{"users":["Norbert","Michał", "John"], "stuff": [1, 2, 3]}', - '{"users":["@string@", @...@], "stuff": [1, 2]}', - ], - [ - '{this_is_not_valid_json', - '{"users":["Michał","@string@"]}', - ], - [ - '{"status":"ok","data":[]}', - '{"status":"ok","data":[{"id": 4,"code":"123987","name":"Anvill","short_description":"ACME Anvill","url":"http://test-store.example.com/p/123987","image":{"url":"http://test-store.example.com/i/123987-0.jpg","description":"ACME Anvill"},"price":95,"promotion_description":"Anvills sale"},{"id": 5,"code":"123988","name":"Red Anvill","short_description":"Red ACME Anvill","url":"http://test-store.example.com/p/123988","image":{"url":"http://test-store.example.com/i/123988-0.jpg","description":"ACME Anvill"},"price":44.99,"promotion_description":"Red is cheap"}]}', - ], - [ - '{"foo":"foo val","bar":"bar val"}', - '{"foo":"foo val"}', - ], - [ - '[{"name": "Norbert","lastName":"Orzechowicz"},{"name":"Michał"},{"name":"Bob"},{"name":"Martin"}]', - '"@array@.repeat({\"name\": \"@string@\"})"', - ], - [ - [], - '[]', - ], - ]; + $this->assertTrue($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); } - public static function normalizationRequiredDataProvider() + public function normalizationRequiredDataProvider() { return [ [ @@ -179,70 +204,54 @@ public static function normalizationRequiredDataProvider() ]; } - public function setUp() : void - { - $backtrace = new Backtrace\InMemoryBacktrace(); - $parser = new Parser(new Lexer(), new Parser\ExpanderInitializer($backtrace)); - $scalarMatchers = new Matcher\ChainMatcher( - self::class, - $backtrace, - [ - new Matcher\CallbackMatcher($backtrace), - new Matcher\ExpressionMatcher($backtrace), - new Matcher\NullMatcher($backtrace), - new Matcher\StringMatcher($backtrace, $parser), - new Matcher\IntegerMatcher($backtrace, $parser), - new Matcher\BooleanMatcher($backtrace, $parser), - new Matcher\DoubleMatcher($backtrace, $parser), - new Matcher\NumberMatcher($backtrace, $parser), - new Matcher\ScalarMatcher($backtrace), - new Matcher\WildcardMatcher($backtrace), - ] - ); - $this->matcher = new Matcher\JsonMatcher( - new Matcher\ArrayMatcher($scalarMatchers, $backtrace, $parser), - $backtrace - ); - } - - /** - * @dataProvider positivePatterns - */ - public function test_positive_can_match($pattern) : void - { - $this->assertTrue($this->matcher->canMatch($pattern)); - } - - /** - * @dataProvider negativePatterns - */ - public function test_negative_can_match($pattern) : void - { - $this->assertFalse($this->matcher->canMatch($pattern)); - } - - /** - * @dataProvider positiveMatches - */ - public function test_positive_matches($value, $pattern) : void - { - $this->assertTrue($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); - } - /** - * @dataProvider normalizationRequiredDataProvider + * @dataProvider negativeMatches */ - public function test_positive_matches_after_normalization($value, $pattern) : void + public function test_negative_matches($value, $pattern, string $error) : void { - $this->assertTrue($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); + $this->assertFalse($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); + $this->assertSame($error, $this->matcher->getError()); } - /** - * @dataProvider negativeMatches - */ - public function test_negative_matches($value, $pattern) : void + public function negativeMatches() { - $this->assertFalse($this->matcher->match($value, $pattern), (string) $this->matcher->getError()); + return [ + [ + '{"users":["Norbert","Michał"]}', + '{"users":["Michał","@string@"]}', + 'Value "Norbert" does not match pattern "Michał" at path: "[users][0]"', + ], + [ + '{"users":["Norbert","Michał", "John"], "stuff": [1, 2, 3]}', + '{"users":["@string@", @...@], "stuff": [1, 2]}', + 'There is no element under path [stuff][2] in pattern.', + ], + [ + '{this_is_not_valid_json', + '{"users":["Michał","@string@"]}', + 'Invalid given JSON of value. Syntax error, malformed JSON', + ], + [ + '{"status":"ok","data":[]}', + '{"status":"ok","data":[{"id": 4,"code":"123987","name":"Anvill","short_description":"ACME Anvill","url":"http://test-store.example.com/p/123987","image":{"url":"http://test-store.example.com/i/123987-0.jpg","description":"ACME Anvill"},"price":95,"promotion_description":"Anvills sale"},{"id": 5,"code":"123988","name":"Red Anvill","short_description":"Red ACME Anvill","url":"http://test-store.example.com/p/123988","image":{"url":"http://test-store.example.com/i/123988-0.jpg","description":"ACME Anvill"},"price":44.99,"promotion_description":"Red is cheap"}]}', + "There is no element under path [url] in value.\nThere is no element under path [id] in value.\nThere is no element under path [url] in value.\nThere is no element under path [id] in value.\nThere is no element under path [data][0] in value.", + ], + [ + '{"foo":"foo val","bar":"bar val"}', + '{"foo":"foo val"}', + 'There is no element under path [bar] in pattern.', + ], + [ + '[{"name": "Norbert","lastName":"Orzechowicz"},{"name":"Michał"},{"name":"Bob"},{"name":"Martin"}]', + '"@array@.repeat({\"name\": \"@string@\"})"', + 'Value "Array(4)" does not match pattern "@array@.repeat({"name": "@string@"})" at path: "root"', + ], + [ + [], + '[]', + 'Invalid given JSON of value. Unknown error', + ], + ]; } public function test_error_when_matching_fail() : void @@ -261,7 +270,7 @@ public function test_error_when_matching_fail() : void ]); $this->assertFalse($this->matcher->match($value, $pattern)); - $this->assertEquals($this->matcher->getError(), 'Value {"users":[{"name":"Norbert"},{"name":"Micha\u0142"}]} does not match pattern {"users":[{"name":"@string@"},{"name":"@boolean@"}]}'); + $this->assertEquals('Value "Michał" does not match pattern "@boolean@" at path: "[users][1][name]"', $this->matcher->getError()); } public function test_error_when_path_in_nested_pattern_does_not_exist() : void @@ -271,7 +280,7 @@ public function test_error_when_path_in_nested_pattern_does_not_exist() : void $this->assertFalse($this->matcher->match($value, $pattern)); - $this->assertEquals($this->matcher->getError(), 'Value {"foo":{"bar":{"baz":"bar value"}}} does not match pattern {"foo":{"bar":{"faz":"faz value"}}}'); + $this->assertEquals('There is no element under path [foo][bar][baz] in pattern.', $this->matcher->getError()); } public function test_error_when_path_in_nested_value_does_not_exist() : void @@ -281,7 +290,7 @@ public function test_error_when_path_in_nested_value_does_not_exist() : void $this->assertFalse($this->matcher->match($value, $pattern)); - $this->assertEquals($this->matcher->getError(), 'Value {"foo":{"bar":[]}} does not match pattern {"foo":{"bar":{"faz":"faz value"}}}'); + $this->assertEquals('There is no element under path [foo][bar][faz] in value.', $this->matcher->getError()); } public function test_error_when_json_pattern_is_invalid() : void @@ -291,7 +300,7 @@ public function test_error_when_json_pattern_is_invalid() : void $this->assertFalse($this->matcher->match($value, $pattern)); - $this->assertEquals($this->matcher->getError(), 'Invalid given JSON of pattern. Syntax error, malformed JSON'); + $this->assertEquals('Invalid given JSON of pattern. Syntax error, malformed JSON', $this->matcher->getError()); } /** @@ -310,6 +319,6 @@ public function test_error_when_json_value_is_invalid() : void $this->assertFalse($this->matcher->match($value, $pattern)); - $this->assertEquals($this->matcher->getError(), 'Invalid given JSON of value. Syntax error, malformed JSON'); + $this->assertEquals('Invalid given JSON of value. Syntax error, malformed JSON', $this->matcher->getError()); } } diff --git a/tests/MatcherTest.php b/tests/MatcherTest.php index 689a0f69..f893a096 100644 --- a/tests/MatcherTest.php +++ b/tests/MatcherTest.php @@ -4,21 +4,11 @@ namespace Coduo\PHPMatcher\Tests; +use Coduo\PHPMatcher\PHPMatcher; use Coduo\PHPMatcher\PHPUnit\PHPMatcherTestCase; class MatcherTest extends PHPMatcherTestCase { - public static function nullExamples() - { - return [ - [ - '{"proformaInvoiceLink":null}', '{"proformaInvoiceLink":null}', - '{"proformaInvoiceLink":null, "test":"test"}', '{"proformaInvoiceLink":null, "test":"@string@"}', - '{"proformaInvoiceLink":null, "test":"test"}', '{"proformaInvoiceLink":@null@, "test":"@string@"}', - ], - ]; - } - /** * @dataProvider scalarValueExamples */ @@ -27,6 +17,17 @@ public function test_matcher_with_scalar_values($value, $pattern) : void $this->assertMatchesPattern($pattern, $value); } + public function scalarValueExamples() + { + return [ + ['Norbert Orzechowicz', '@string@'], + [6.66, '@double@'], + [1, '@integer@'], + [['foo'], '@array@'], + ['9f4db639-0e87-4367-9beb-d64e3f42ae18', '@uuid@'], + ]; + } + public function test_matcher_with_array_value() : void { $value = [ @@ -78,119 +79,6 @@ public function test_matcher_with_json($value, $pattern) : void $this->assertMatchesPattern($pattern, $value); } - public function test_matcher_with_xml() : void - { - $value = <<<'XML' - - - - - - IBM - Any Value - - - - -XML; - $pattern = <<<'XML' - - - - - - @string@ - @string@ - - - - -XML; - - $this->assertMatchesPattern($pattern, $value); - } - - public function test_matcher_with_xml_including_optional_node() : void - { - $value = <<<'XML' - - - - - - IBM - Any Value - - - - -XML; - $pattern = <<<'XML' - - - - - - @string@.optional() - @string@.optional() - @integer@.optional() - - - - -XML; - - $this->assertMatchesPattern($pattern, $value); - } - - public function test_full_text_matcher() : void - { - $value = 'lorem ipsum 1234 random text'; - $pattern = "@string@.startsWith('lo') ipsum @number@.greaterThan(10) random text"; - $this->assertMatchesPattern($pattern, $value); - } - - public function test_matcher_with_callback() : void - { - $this->assertMatchesPattern( - fn ($value) => $value === 'test', - 'test' - ); - } - - public function test_matcher_with_wildcard() : void - { - $this->assertMatchesPattern('@*@', 'test'); - $this->assertMatchesPattern('@wildcard@', 'test'); - } - - /** - * @dataProvider nullExamples - */ - public function test_null_value_in_the_json(string $value, string $pattern) : void - { - $this->assertMatchesPattern($pattern, $value); - } - - public function scalarValueExamples() - { - return [ - ['Norbert Orzechowicz', '@string@'], - [6.66, '@double@'], - [1, '@integer@'], - [['foo'], '@array@'], - ['9f4db639-0e87-4367-9beb-d64e3f42ae18', '@uuid@'], - ]; - } - public function jsonDataProvider() { return [ @@ -375,6 +263,119 @@ public function jsonDataProvider() ]; } + public function test_matcher_with_xml() : void + { + $value = <<<'XML' + + + + + + IBM + Any Value + + + + +XML; + $pattern = <<<'XML' + + + + + + @string@ + @string@ + + + + +XML; + + $this->assertMatchesPattern($pattern, $value); + } + + public function test_matcher_with_xml_including_optional_node() : void + { + $value = <<<'XML' + + + + + + IBM + Any Value + + + + +XML; + $pattern = <<<'XML' + + + + + + @string@.optional() + @string@.optional() + @integer@.optional() + + + + +XML; + + $this->assertMatchesPattern($pattern, $value); + } + + public function test_full_text_matcher() : void + { + $value = 'lorem ipsum 1234 random text'; + $pattern = "@string@.startsWith('lo') ipsum @number@.greaterThan(10) random text"; + $this->assertMatchesPattern($pattern, $value); + } + + public function test_matcher_with_callback() : void + { + $this->assertMatchesPattern( + fn ($value) => $value === 'test', + 'test' + ); + } + + public function test_matcher_with_wildcard() : void + { + $this->assertMatchesPattern('@*@', 'test'); + $this->assertMatchesPattern('@wildcard@', 'test'); + } + + /** + * @dataProvider nullExamples + */ + public function test_null_value_in_the_json(string $value, string $pattern) : void + { + $this->assertMatchesPattern($pattern, $value); + } + + public function nullExamples() + { + return [ + [ + '{"proformaInvoiceLink":null}', '{"proformaInvoiceLink":null}', + '{"proformaInvoiceLink":null, "test":"test"}', '{"proformaInvoiceLink":null, "test":"@string@"}', + '{"proformaInvoiceLink":null, "test":"test"}', '{"proformaInvoiceLink":@null@, "test":"@string@"}', + ], + ]; + } + public function test_php_pattern_of_github_pull_requests_response() : void { $this->assertMatchesPattern( @@ -704,4 +705,341 @@ public function test_php_pattern_of_github_pull_requests_response() : void \file_get_contents(__DIR__ . '/fixtures/github_pulls.json'), ); } + + public function test_php_pattern_of_github_pull_requests_response_with_one_small_mistake() : void + { + $matcher = new PHPMatcher(); + + $matcher->match( + \file_get_contents(__DIR__ . '/fixtures/github_pulls.json'), + /* @lang JSON */ + <<<'PATTERN' +[ + { + "url": "https://api.github.com/repos/coduo/php-matcher/pulls/@number@", + "id": "@integer@", + "node_id": "@string@", + "html_url": "https://github.com/coduo/php-matcher/pull/@integer@", + "diff_url": "https://github.com/coduo/php-matcher/pull/@integer@.diff", + "patch_url": "https://github.com/coduo/php-matcher/pull/@integer@.patch", + "issue_url": "https://api.github.com/repos/coduo/php-matcher/issues/@integer@", + "number": "@integer@", + "state": "@string@", + "locked": "@boolean@", + "title": "@string@", + "user": { + "login": "@integer@", + "id": "@integer@", + "node_id": "@string@", + "avatar_url": "@string@", + "gravatar_id": "@string@", + "url": "https://api.github.com/users/@string@", + "html_url": "https://github.com/@string@", + "followers_url": "https://api.github.com/users/@string@/followers", + "following_url": "https://api.github.com/users/@string@/following{/other_user}", + "gists_url": "https://api.github.com/users/@string@/gists{/gist_id}", + "starred_url": "https://api.github.com/users/@string@/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/@string@/subscriptions", + "organizations_url": "https://api.github.com/users/@string@/orgs", + "repos_url": "https://api.github.com/users/@string@/repos", + "events_url": "https://api.github.com/users/@string@/events{/privacy}", + "received_events_url": "https://api.github.com/users/@string@/received_events", + "type": "@string@", + "site_admin": "@boolean@" + }, + "body": "@string@", + "created_at": "@datetime@", + "updated_at": "@datetime@", + "closed_at": "@datetime@||@null@", + "merged_at": "@datetime@||@null@", + "merge_commit_sha": "@string@", + "assignee": "@json@||@null@", + "assignees": "@array@", + "requested_reviewers": "@array@", + "requested_teams":"@array@", + "labels": "@array@", + "milestone": "@json@||@null@", + "draft": "@boolean@", + "commits_url": "https://api.github.com/repos/coduo/php-matcher/pulls/@number@/commits", + "review_comments_url": "https://api.github.com/repos/coduo/php-matcher/pulls/@number@/comments", + "review_comment_url": "https://api.github.com/repos/coduo/php-matcher/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/coduo/php-matcher/issues/@number@/comments", + "statuses_url": "https://api.github.com/repos/coduo/php-matcher/statuses/@string@", + "head": { + "label": "@string@", + "ref": "@string@", + "sha": "@string@", + "user": { + "login": "@string@", + "id": "@integer@", + "node_id": "@string@", + "avatar_url": "@string@", + "gravatar_id": "@string@", + "url": "https://api.github.com/users/@string@", + "html_url": "https://github.com/@string@", + "followers_url": "https://api.github.com/users/@string@/followers", + "following_url": "https://api.github.com/users/@string@/following{/other_user}", + "gists_url": "https://api.github.com/users/@string@gists{/gist_id}", + "starred_url": "https://api.github.com/users/@string@/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/@string@/subscriptions", + "organizations_url": "https://api.github.com/users/@string@/orgs", + "repos_url": "https://api.github.com/users/@string@/repos", + "events_url": "https://api.github.com/users/@string@/events{/privacy}", + "received_events_url": "https://api.github.com/users/@string@/received_events", + "type": "@string@", + "site_admin": "@boolean@" + }, + "repo": { + "id": "@integer@", + "node_id": "@string@", + "name": "php-matcher", + "full_name": "@string@", + "private": "@boolean@", + "owner": { + "login": "@string@", + "id": "@integer@", + "node_id": "@string@", + "avatar_url": "@string@", + "gravatar_id": "@string@", + "url": "https://api.github.com/users/@string@", + "html_url": "https://github.com/@string@", + "followers_url": "https://api.github.com/users/@string@/followers", + "following_url": "https://api.github.com/users/@string@/following{/other_user}", + "gists_url": "https://api.github.com/users/@string@/gists{/gist_id}", + "starred_url": "https://api.github.com/users/@string@/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/@string@/subscriptions", + "organizations_url": "https://api.github.com/users/@string@/orgs", + "repos_url": "https://api.github.com/users/@string@/repos", + "events_url": "https://api.github.com/users/@string@/events{/privacy}", + "received_events_url": "https://api.github.com/users/@string@/received_events", + "type": "@string@", + "site_admin": "@boolean@" + }, + "html_url": "https://github.com/@string@/php-matcher", + "description": "@string@", + "fork": "@boolean@", + "url": "https://api.github.com/repos/@string@/php-matcher", + "forks_url": "https://api.github.com/repos/@string@/php-matcher/forks", + "keys_url": "https://api.github.com/repos/@string@/php-matcher/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/@string@/php-matcher/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/@string@/php-matcher/teams", + "hooks_url": "https://api.github.com/repos/@string@/php-matcher/hooks", + "issue_events_url": "https://api.github.com/repos/@string@/php-matcher/issues/events{/number}", + "events_url": "https://api.github.com/repos/@string@/php-matcher/events", + "assignees_url": "https://api.github.com/repos/@string@/php-matcher/assignees{/user}", + "branches_url": "https://api.github.com/repos/@string@/php-matcher/branches{/branch}", + "tags_url": "https://api.github.com/repos/@string@/php-matcher/tags", + "blobs_url": "https://api.github.com/repos/@string@/php-matcher/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/@string@/php-matcher/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/@string@/php-matcher/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/@string@/php-matcher/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/@string@/php-matcher/statuses/{sha}", + "languages_url": "https://api.github.com/repos/@string@/php-matcher/languages", + "stargazers_url": "https://api.github.com/repos/@string@/php-matcher/stargazers", + "contributors_url": "https://api.github.com/repos/@string@/php-matcher/contributors", + "subscribers_url": "https://api.github.com/repos/@string@/php-matcher/subscribers", + "subscription_url": "https://api.github.com/repos/@string@/php-matcher/subscription", + "commits_url": "https://api.github.com/repos/@string@/php-matcher/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/@string@/php-matcher/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/@string@/php-matcher/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/@string@/php-matcher/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/@string@/php-matcher/contents/{+path}", + "compare_url": "https://api.github.com/repos/@string@/php-matcher/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/@string@/php-matcher/merges", + "archive_url": "https://api.github.com/repos/@string@/php-matcher/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/@string@/php-matcher/downloads", + "issues_url": "https://api.github.com/repos/@string@/php-matcher/issues{/number}", + "pulls_url": "https://api.github.com/repos/@string@/php-matcher/pulls{/number}", + "milestones_url": "https://api.github.com/repos/@string@/php-matcher/milestones{/number}", + "notifications_url": "https://api.github.com/repos/@string@/php-matcher/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/@string@/php-matcher/labels{/name}", + "releases_url": "https://api.github.com/repos/@string@/php-matcher/releases{/id}", + "deployments_url": "https://api.github.com/repos/@string@/php-matcher/deployments", + "created_at": "@datetime@", + "updated_at": "@datetime@", + "pushed_at": "@datetime@", + "git_url": "git://github.com/@string@/php-matcher.git", + "ssh_url": "git@github.com:@string@/php-matcher.git", + "clone_url": "https://github.com/@string@/php-matcher.git", + "svn_url": "https://github.com/@string@/php-matcher", + "homepage": "@string@", + "size": "@integer@", + "stargazers_count": "@integer@", + "watchers_count": "@integer@", + "language": "@string@||@null@", + "has_issues": "@boolean@", + "has_projects": "@boolean@", + "has_downloads": "@boolean@", + "has_wiki": "@boolean@", + "has_pages": "@boolean@", + "forks_count": "@integer@", + "mirror_url": "@string@.isUrl()||@null@", + "archived": "@boolean@", + "disabled": "@boolean@", + "open_issues_count": "@integer@", + "license": "@json@", + "forks": "@integer@", + "open_issues": "@integer@", + "watchers": "@integer@", + "default_branch": "@string@" + } + }, + "base": { + "label": "@string@", + "ref": "@string@", + "sha": "@string@", + "user": { + "login": "coduo", + "id": "@integer@", + "node_id": "@string@", + "avatar_url": "@string@", + "gravatar_id": "@string@||@null@", + "url": "https://api.github.com/users/coduo", + "html_url": "https://github.com/coduo", + "followers_url": "https://api.github.com/users/coduo/followers", + "following_url": "https://api.github.com/users/coduo/following{/other_user}", + "gists_url": "https://api.github.com/users/coduo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/coduo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/coduo/subscriptions", + "organizations_url": "https://api.github.com/users/coduo/orgs", + "repos_url": "https://api.github.com/users/coduo/repos", + "events_url": "https://api.github.com/users/coduo/events{/privacy}", + "received_events_url": "https://api.github.com/users/coduo/received_events", + "type": "Organization", + "site_admin": "@boolean@" + }, + "repo": { + "id": "@integer@", + "node_id": "@string@", + "name": "php-matcher", + "full_name": "coduo/php-matcher", + "private": "@boolean@", + "owner": { + "login": "coduo", + "id": "@integer@", + "node_id": "@string@", + "avatar_url": "@string@", + "gravatar_id": "@string@", + "url": "https://api.github.com/users/coduo", + "html_url": "https://github.com/coduo", + "followers_url": "https://api.github.com/users/coduo/followers", + "following_url": "https://api.github.com/users/coduo/following{/other_user}", + "gists_url": "https://api.github.com/users/coduo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/coduo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/coduo/subscriptions", + "organizations_url": "https://api.github.com/users/coduo/orgs", + "repos_url": "https://api.github.com/users/coduo/repos", + "events_url": "https://api.github.com/users/coduo/events{/privacy}", + "received_events_url": "https://api.github.com/users/coduo/received_events", + "type": "@string@", + "site_admin": "@boolean@" + }, + "html_url": "https://github.com/coduo/php-matcher", + "description": "@string@", + "fork": "@boolean@", + "url": "https://api.github.com/repos/coduo/php-matcher", + "forks_url": "https://api.github.com/repos/coduo/php-matcher/forks", + "keys_url": "https://api.github.com/repos/coduo/php-matcher/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/coduo/php-matcher/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/coduo/php-matcher/teams", + "hooks_url": "https://api.github.com/repos/coduo/php-matcher/hooks", + "issue_events_url": "https://api.github.com/repos/coduo/php-matcher/issues/events{/number}", + "events_url": "https://api.github.com/repos/coduo/php-matcher/events", + "assignees_url": "https://api.github.com/repos/coduo/php-matcher/assignees{/user}", + "branches_url": "https://api.github.com/repos/coduo/php-matcher/branches{/branch}", + "tags_url": "https://api.github.com/repos/coduo/php-matcher/tags", + "blobs_url": "https://api.github.com/repos/coduo/php-matcher/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/coduo/php-matcher/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/coduo/php-matcher/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/coduo/php-matcher/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/coduo/php-matcher/statuses/{sha}", + "languages_url": "https://api.github.com/repos/coduo/php-matcher/languages", + "stargazers_url": "https://api.github.com/repos/coduo/php-matcher/stargazers", + "contributors_url": "https://api.github.com/repos/coduo/php-matcher/contributors", + "subscribers_url": "https://api.github.com/repos/coduo/php-matcher/subscribers", + "subscription_url": "https://api.github.com/repos/coduo/php-matcher/subscription", + "commits_url": "https://api.github.com/repos/coduo/php-matcher/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/coduo/php-matcher/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/coduo/php-matcher/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/coduo/php-matcher/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/coduo/php-matcher/contents/{+path}", + "compare_url": "https://api.github.com/repos/coduo/php-matcher/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/coduo/php-matcher/merges", + "archive_url": "https://api.github.com/repos/coduo/php-matcher/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/coduo/php-matcher/downloads", + "issues_url": "https://api.github.com/repos/coduo/php-matcher/issues{/number}", + "pulls_url": "https://api.github.com/repos/coduo/php-matcher/pulls{/number}", + "milestones_url": "https://api.github.com/repos/coduo/php-matcher/milestones{/number}", + "notifications_url": "https://api.github.com/repos/coduo/php-matcher/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/coduo/php-matcher/labels{/name}", + "releases_url": "https://api.github.com/repos/coduo/php-matcher/releases{/id}", + "deployments_url": "https://api.github.com/repos/coduo/php-matcher/deployments", + "created_at": "@datetime@", + "updated_at": "@datetime@", + "pushed_at": "@datetime@", + "git_url": "@string@", + "ssh_url": "@string@", + "clone_url": "@string@.isUrl()", + "svn_url": "@string@.isUrl()", + "homepage": "@string@||@null@", + "size": "@integer@", + "stargazers_count": "@integer@", + "watchers_count": "@integer@", + "language": "@string@", + "has_issues": "@boolean@", + "has_projects": "@boolean@", + "has_downloads": "@boolean@", + "has_wiki": "@boolean@", + "has_pages": "@boolean@", + "forks_count": "@integer@", + "mirror_url": "@string@||@null@", + "archived": "@boolean@", + "disabled": "@boolean@", + "open_issues_count": "@integer@", + "license": "@json@", + "forks": "@integer@", + "open_issues": "@integer@", + "watchers": "@integer@", + "default_branch": "@string@" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/coduo/php-matcher/pulls/@integer@" + }, + "html": { + "href": "https://github.com/coduo/php-matcher/pull/@integer@" + }, + "issue": { + "href": "https://api.github.com/repos/coduo/php-matcher/issues/@integer@" + }, + "comments": { + "href": "https://api.github.com/repos/coduo/php-matcher/issues/@integer@/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/coduo/php-matcher/pulls/@integer@/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/coduo/php-matcher/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/coduo/php-matcher/pulls/@integer@/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/coduo/php-matcher/statuses/@string@" + } + }, + "author_association": "@string@", + "active_lock_reason": null + }, + "@array_previous_repeat@" +] +PATTERN + ); + + $this->assertSame( + 'Value "norberttech" does not match pattern "@integer@" at path: "[0][user][login]"', + $matcher->error() + ); + } } diff --git a/tests/PHPUnit/PHPMatcherAssertionsTest.php b/tests/PHPUnit/PHPMatcherAssertionsTest.php index 09360430..956bc12c 100644 --- a/tests/PHPUnit/PHPMatcherAssertionsTest.php +++ b/tests/PHPUnit/PHPMatcherAssertionsTest.php @@ -20,48 +20,114 @@ public function test_it_asserts_if_a_value_matches_the_pattern() : void public function test_it_throws_an_expectation_failed_exception_if_a_value_does_not_match_the_pattern() : void { - $this->expectException(AssertionFailedError::class); - - /* - * Expected console output: - * - * Failed asserting that '{"foo":"bar"}' matches given pattern. - * Pattern: '{"foo": "@integer@"}' - * Error: Value {"foo":"bar"} does not match pattern {"foo":"@integer@"} - * Backtrace: - * #1 Matcher Coduo\PHPMatcher\Matcher matching value "{"foo":"bar"}" with "{"foo":"@integer@"}" pattern - * #2 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) matching value "{"foo":"bar"}" with "{"foo":"@integer@"}" pattern - * #3 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (scalars) can match pattern "{"foo":"@integer@"}" - * #... - * #66 Matcher Coduo\PHPMatcher\Matcher error: Value {"foo":"bar"} does not match pattern {"foo":"@integer@"}. - */ - $this->expectExceptionMessageMatches("/Failed asserting that '{\"foo\":\"bar\"}' matches given pattern.\nPattern: '{\"foo\": \"@integer@\"}'\nError: Value {\"foo\":\"bar\"} does not match pattern {\"foo\":\"@integer@\"}\nBacktrace: \nEmpty/"); - - $this->assertMatchesPattern('{"foo": "@integer@"}', \json_encode(['foo' => 'bar'])); + try { + $this->assertMatchesPattern('{"foo": "@integer@"}', \json_encode(['foo' => 'bar'])); + } catch (\Exception $e) { + $this->assertSame( + <<<'ERROR' +Failed asserting that '{"foo":"bar"}' matches given pattern. +Pattern: '{"foo": "@integer@"}' +Error: Value "bar" does not match pattern "@integer@" at path: "[foo]" +Backtrace: +Empty. +ERROR, + $e->getMessage() + ); + } } public function test_it_throws_an_expectation_failed_exception_if_a_value_does_not_match_the_pattern_with_backtrace() : void { - $this->expectException(AssertionFailedError::class); - $this->setBacktrace($backtrace = new InMemoryBacktrace()); - /* - * Expected console output: - * - * Failed asserting that '{"foo":"bar"}' matches given pattern. - * Pattern: '{"foo": "@integer@"}' - * Error: Value {"foo":"bar"} does not match pattern {"foo":"@integer@"} - * Backtrace: - * #1 Matcher Coduo\PHPMatcher\Matcher matching value "{"foo":"bar"}" with "{"foo":"@integer@"}" pattern - * #2 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) matching value "{"foo":"bar"}" with "{"foo":"@integer@"}" pattern - * #3 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (scalars) can match pattern "{"foo":"@integer@"}" - * #... - * #66 Matcher Coduo\PHPMatcher\Matcher error: Value {"foo":"bar"} does not match pattern {"foo":"@integer@"}. - */ - $this->expectExceptionMessageMatches("/Failed asserting that '{\"foo\":\"bar\"}' matches given pattern.\nPattern: '{\"foo\": \"@integer@\"}'\nError: Value {\"foo\":\"bar\"} does not match pattern {\"foo\":\"@integer@\"}\nBacktrace: \n/"); + try { + $this->assertMatchesPattern('{"foo": "@integer@"}', \json_encode(['foo' => 'bar'])); + } catch (\Exception $e) { + $this->assertSame( + <<getMessage() + ); + } - $this->assertMatchesPattern('{"foo": "@integer@"}', \json_encode(['foo' => 'bar'])); $this->assertFalse($backtrace->isEmpty()); }