From 4b7d3fca12f5ff68397682b24782b4f4bc7ad50a Mon Sep 17 00:00:00 2001 From: Kamil Kokot Date: Wed, 19 Oct 2016 23:51:16 +0200 Subject: [PATCH] Extract iterables matching logic to a standalone class --- .../Matcher/Iterate/IterablesMatcherSpec.php | 134 ++++++++++++++++++ ...ubjectElementDoesNotMatchExceptionSpec.php | 33 +++++ .../SubjectHasLessElementsExceptionSpec.php | 19 +++ .../SubjectHasMoreElementsExceptionSpec.php | 19 +++ spec/PhpSpec/Matcher/IterateMatcherSpec.php | 4 +- .../Matcher/Iterate/IterablesMatcher.php | 79 +++++++++++ .../SubjectElementDoesNotMatchException.php | 100 +++++++++++++ .../SubjectHasLessElementsException.php | 22 +++ .../SubjectHasMoreElementsException.php | 22 +++ src/PhpSpec/Matcher/IterateMatcher.php | 53 +++---- 10 files changed, 452 insertions(+), 33 deletions(-) create mode 100644 spec/PhpSpec/Matcher/Iterate/IterablesMatcherSpec.php create mode 100644 spec/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchExceptionSpec.php create mode 100644 spec/PhpSpec/Matcher/Iterate/SubjectHasLessElementsExceptionSpec.php create mode 100644 spec/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsExceptionSpec.php create mode 100644 src/PhpSpec/Matcher/Iterate/IterablesMatcher.php create mode 100644 src/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchException.php create mode 100644 src/PhpSpec/Matcher/Iterate/SubjectHasLessElementsException.php create mode 100644 src/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsException.php diff --git a/spec/PhpSpec/Matcher/Iterate/IterablesMatcherSpec.php b/spec/PhpSpec/Matcher/Iterate/IterablesMatcherSpec.php new file mode 100644 index 000000000..e172ad6f3 --- /dev/null +++ b/spec/PhpSpec/Matcher/Iterate/IterablesMatcherSpec.php @@ -0,0 +1,134 @@ + + */ +final class IterablesMatcherSpec extends ObjectBehavior +{ + function it_should_throw_an_invalid_argument_exception_if_subject_is_not_iterable() + { + $this + ->shouldThrow(new \InvalidArgumentException('Subject value should be an array or implement \Traversable.')) + ->during('match', ['not iterable', []]) + ; + + $this + ->shouldThrow(new \InvalidArgumentException('Subject value should be an array or implement \Traversable.')) + ->during('match', [9, []]) + ; + + $this + ->shouldThrow(new \InvalidArgumentException('Subject value should be an array or implement \Traversable.')) + ->during('match', [new \stdClass(), []]) + ; + } + + function it_should_throw_an_invalid_argument_exception_if_expected_value_is_not_iterable() + { + $this + ->shouldThrow(new \InvalidArgumentException('Expected value should be an array or implement \Traversable.')) + ->during('match', [[], 'not iterable']) + ; + + $this + ->shouldThrow(new \InvalidArgumentException('Expected value should be an array or implement \Traversable.')) + ->during('match', [[], 9]) + ; + + $this + ->shouldThrow(new \InvalidArgumentException('Expected value should be an array or implement \Traversable.')) + ->during('match', [[], new \stdClass()]) + ; + } + + function it_should_throw_an_exception_if_subject_has_less_elements_than_expected() + { + $this + ->shouldThrow(new SubjectHasLessElementsException()) + ->during('match', [['a' => 'b'], ['a' => 'b', 'c' => 'd']]) + ; + } + + function it_should_throw_an_exception_if_subject_has_more_elements_than_expected() + { + $this + ->shouldThrow(new SubjectHasMoreElementsException()) + ->during('match', [['a' => 'b', 'c' => 'd'], ['a' => 'b']]) + ; + } + + function it_should_throw_an_exception_if_subject_element_does_not_match_the_expected_one() + { + $this + ->shouldThrow(new SubjectElementDoesNotMatchException(0, 'a', 'b', 'a', 'c')) + ->during('match', [['a' => 'b'], ['a' => 'c']]) + ; + + $this + ->shouldThrow(new SubjectElementDoesNotMatchException(0, 'a', 'b', 'c', 'b')) + ->during('match', [['a' => 'b'], ['c' => 'b']]) + ; + + $this + ->shouldThrow(new SubjectElementDoesNotMatchException(1, 'c', 'd', 'c', 'e')) + ->during('match', [['a' => 'b', 'c' => 'd'], ['a' => 'b', 'c' => 'e']]) + ; + } + + function it_should_not_throw_any_exception_if_subject_iterates_as_expected() + { + $this + ->shouldNotThrow() + ->during('match', [['a' => 'b', 'c' => 'd'], ['a' => 'b', 'c' => 'd']]) + ; + + $this + ->shouldNotThrow() + ->during('match', [['a' => 'b', 'c' => 'd'], new \ArrayIterator(['a' => 'b', 'c' => 'd'])]) + ; + + $this + ->shouldNotThrow() + ->during('match', [new \ArrayIterator(['a' => 'b', 'c' => 'd']), ['a' => 'b', 'c' => 'd']]) + ; + + $this + ->shouldNotThrow() + ->during('match', [['a' => 'b', 'c' => 'd'], new \ArrayObject(['a' => 'b', 'c' => 'd'])]) + ; + + $this + ->shouldNotThrow() + ->during('match', [new \ArrayObject(['a' => 'b', 'c' => 'd']), ['a' => 'b', 'c' => 'd']]) + ; + + $this + ->shouldNotThrow() + ->during('match', [$this->createGeneratorReturningArray(['a' => 'b', 'c' => 'd']), ['a' => 'b', 'c' => 'd']]) + ; + + $this + ->shouldNotThrow() + ->during('match', [['a' => 'b', 'c' => 'd'], $this->createGeneratorReturningArray(['a' => 'b', 'c' => 'd'])]) + ; + } + + /** + * @param array $array + * + * @return \Generator + */ + private function createGeneratorReturningArray(array $array) + { + foreach ($array as $key => $value) { + yield $key => $value; + } + } +} diff --git a/spec/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchExceptionSpec.php b/spec/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchExceptionSpec.php new file mode 100644 index 000000000..9e5b4f1fa --- /dev/null +++ b/spec/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchExceptionSpec.php @@ -0,0 +1,33 @@ +beConstructedWith(42, 'subject key', 'subject value', 'expected key', 'expected value'); + } + + function it_is_a_runtime_exception() + { + $this->shouldHaveType(\RuntimeException::class); + } + + function it_has_a_predefined_message() + { + $this->getMessage()->shouldReturn('Subject element does not match with expected element.'); + } + + function it_contains_the_details_of_matched_element() + { + $this->getElementNumber()->shouldReturn(42); + $this->getSubjectKey()->shouldReturn('subject key'); + $this->getSubjectValue()->shouldReturn('subject value'); + $this->getExpectedKey()->shouldReturn('expected key'); + $this->getExpectedValue()->shouldReturn('expected value'); + } +} diff --git a/spec/PhpSpec/Matcher/Iterate/SubjectHasLessElementsExceptionSpec.php b/spec/PhpSpec/Matcher/Iterate/SubjectHasLessElementsExceptionSpec.php new file mode 100644 index 000000000..1b5cb4715 --- /dev/null +++ b/spec/PhpSpec/Matcher/Iterate/SubjectHasLessElementsExceptionSpec.php @@ -0,0 +1,19 @@ +shouldHaveType(\LengthException::class); + } + + function it_has_a_predefined_message() + { + $this->getMessage()->shouldReturn('Subject has less elements than expected.'); + } +} diff --git a/spec/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsExceptionSpec.php b/spec/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsExceptionSpec.php new file mode 100644 index 000000000..032cadb2c --- /dev/null +++ b/spec/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsExceptionSpec.php @@ -0,0 +1,19 @@ +shouldHaveType(\LengthException::class); + } + + function it_has_a_predefined_message() + { + $this->getMessage()->shouldReturn('Subject has more elements than expected.'); + } +} diff --git a/spec/PhpSpec/Matcher/IterateMatcherSpec.php b/spec/PhpSpec/Matcher/IterateMatcherSpec.php index 471d4214c..72a00b058 100644 --- a/spec/PhpSpec/Matcher/IterateMatcherSpec.php +++ b/spec/PhpSpec/Matcher/IterateMatcherSpec.php @@ -71,7 +71,7 @@ function it_does_not_positive_match_generator_while_not_iterating_the_same() ; $this - ->shouldThrow(new FailureException('Expect subject to have the same count than matched value, but it has less records.')) + ->shouldThrow(new FailureException('Expected subject to have the same count than matched value, but it has less records.')) ->during('positiveMatch', [ 'iterate', $this->createGeneratorReturningArray(['a' => 'b', 'c' => 'd']), @@ -80,7 +80,7 @@ function it_does_not_positive_match_generator_while_not_iterating_the_same() ; $this - ->shouldThrow(new FailureException('Expect subject to have the same count than matched value, but it has more records.')) + ->shouldThrow(new FailureException('Expected subject to have the same count than matched value, but it has more records.')) ->during('positiveMatch', [ 'iterate', $this->createGeneratorReturningArray(['a' => 'b', 'c' => 'd']), diff --git a/src/PhpSpec/Matcher/Iterate/IterablesMatcher.php b/src/PhpSpec/Matcher/Iterate/IterablesMatcher.php new file mode 100644 index 000000000..3d5131528 --- /dev/null +++ b/src/PhpSpec/Matcher/Iterate/IterablesMatcher.php @@ -0,0 +1,79 @@ +isIterable($subject)) { + throw new \InvalidArgumentException('Subject value should be an array or implement \Traversable.'); + } + + if (!$this->isIterable($expected)) { + throw new \InvalidArgumentException('Expected value should be an array or implement \Traversable.'); + } + + $expectedIterator = $this->createIteratorFromIterable($expected); + + $count = 0; + foreach ($subject as $subjectKey => $subjectValue) { + if (!$expectedIterator->valid()) { + throw new SubjectHasMoreElementsException(); + } + + if ($subjectKey !== $expectedIterator->key() || $subjectValue !== $expectedIterator->current()) { + throw new SubjectElementDoesNotMatchException( + $count, + $subjectKey, + $subjectValue, + $expectedIterator->key(), + $expectedIterator->current() + ); + } + + $expectedIterator->next(); + ++$count; + } + + if ($expectedIterator->valid()) { + throw new SubjectHasLessElementsException(); + } + } + + /** + * @param mixed $variable + * + * @return bool + */ + private function isIterable($variable) + { + return is_array($variable) || $variable instanceof \Traversable; + } + + /** + * @param array|\Traversable $expected + * + * @return \Iterator + */ + private function createIteratorFromIterable($expected) + { + if (is_array($expected)) { + return new \ArrayIterator($expected); + } + + $iterator = new \IteratorIterator($expected); + $iterator->rewind(); + + return $iterator; + } +} diff --git a/src/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchException.php b/src/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchException.php new file mode 100644 index 000000000..141ab2021 --- /dev/null +++ b/src/PhpSpec/Matcher/Iterate/SubjectElementDoesNotMatchException.php @@ -0,0 +1,100 @@ + + * (c) Konstantin Kudryashov + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpSpec\Matcher\Iterate; + +class SubjectElementDoesNotMatchException extends \RuntimeException +{ + /** + * @var int + */ + private $elementNumber; + + /** + * @var string + */ + private $subjectKey; + + /** + * @var string + */ + private $subjectValue; + + /** + * @var string + */ + private $expectedKey; + + /** + * @var string + */ + private $expectedValue; + + /** + * @param int $elementNumber + * @param string $subjectKey + * @param string $subjectValue + * @param string $expectedKey + * @param string $expectedValue + */ + public function __construct($elementNumber, $subjectKey, $subjectValue, $expectedKey, $expectedValue) + { + $this->elementNumber = $elementNumber; + $this->subjectKey = $subjectKey; + $this->subjectValue = $subjectValue; + $this->expectedKey = $expectedKey; + $this->expectedValue = $expectedValue; + + parent::__construct('Subject element does not match with expected element.'); + } + + /** + * @return int + */ + public function getElementNumber() + { + return $this->elementNumber; + } + + /** + * @return string + */ + public function getSubjectKey() + { + return $this->subjectKey; + } + + /** + * @return string + */ + public function getSubjectValue() + { + return $this->subjectValue; + } + + /** + * @return string + */ + public function getExpectedKey() + { + return $this->expectedKey; + } + + /** + * @return string + */ + public function getExpectedValue() + { + return $this->expectedValue; + } +} diff --git a/src/PhpSpec/Matcher/Iterate/SubjectHasLessElementsException.php b/src/PhpSpec/Matcher/Iterate/SubjectHasLessElementsException.php new file mode 100644 index 000000000..11cd78521 --- /dev/null +++ b/src/PhpSpec/Matcher/Iterate/SubjectHasLessElementsException.php @@ -0,0 +1,22 @@ + + * (c) Konstantin Kudryashov + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpSpec\Matcher\Iterate; + +class SubjectHasLessElementsException extends \LengthException +{ + public function __construct() + { + parent::__construct('Subject has less elements than expected.'); + } +} diff --git a/src/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsException.php b/src/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsException.php new file mode 100644 index 000000000..95cdbfe23 --- /dev/null +++ b/src/PhpSpec/Matcher/Iterate/SubjectHasMoreElementsException.php @@ -0,0 +1,22 @@ + + * (c) Konstantin Kudryashov + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpSpec\Matcher\Iterate; + +class SubjectHasMoreElementsException extends \LengthException +{ + public function __construct() + { + parent::__construct('Subject has more elements than expected.'); + } +} diff --git a/src/PhpSpec/Matcher/IterateMatcher.php b/src/PhpSpec/Matcher/IterateMatcher.php index a698a5c52..28bb46b57 100644 --- a/src/PhpSpec/Matcher/IterateMatcher.php +++ b/src/PhpSpec/Matcher/IterateMatcher.php @@ -16,6 +16,7 @@ use PhpSpec\Formatter\Presenter\Presenter; use PhpSpec\Exception\Example\FailureException; use ArrayAccess; +use PhpSpec\Matcher\Iterate\IterablesMatcher; final class IterateMatcher implements Matcher { @@ -24,12 +25,18 @@ final class IterateMatcher implements Matcher */ private $presenter; + /** + * @var IterablesMatcher + */ + private $iterablesMatcher; + /** * @param Presenter $presenter */ public function __construct(Presenter $presenter) { $this->presenter = $presenter; + $this->iterablesMatcher = new IterablesMatcher(); } /** @@ -49,37 +56,21 @@ public function supports($name, $subject, array $arguments) */ public function positiveMatch($name, $subject, array $arguments) { - $expected = $arguments[0]; - if (is_array($expected)) { - $expected = new \ArrayIterator($expected); - } - - $expectedIterator = new \IteratorIterator($expected); - - $count = 0; - $expectedIterator->rewind(); - foreach ($subject as $subjectKey => $subjectValue) { - if (!$expectedIterator->valid()) { - throw new FailureException('Expect subject to have the same count than matched value, but it has more records.'); - } - - if ($subjectKey !== $expectedIterator->key() || $subjectValue !== $expectedIterator->current()) { - throw new FailureException(sprintf( - 'Expected subject to have record #%d with key %s and value %s, but got key %s and value %s.', - $count, - $this->presenter->presentValue($expectedIterator->key()), - $this->presenter->presentValue($expectedIterator->current()), - $this->presenter->presentValue($subjectKey), - $this->presenter->presentValue($subjectValue) - )); - } - - $expectedIterator->next(); - ++$count; - } - - if ($expectedIterator->valid()) { - throw new FailureException('Expect subject to have the same count than matched value, but it has less records.'); + try { + $this->iterablesMatcher->match($subject, $arguments[0]); + } catch (Iterate\SubjectHasLessElementsException $exception) { + throw new FailureException('Expected subject to have the same count than matched value, but it has less records.', 0, $exception); + } catch (Iterate\SubjectHasMoreElementsException $exception) { + throw new FailureException('Expected subject to have the same count than matched value, but it has more records.', 0, $exception); + } catch (Iterate\SubjectElementDoesNotMatchException $exception) { + throw new FailureException(sprintf( + 'Expected subject to have record #%d with key %s and value %s, but got key %s and value %s.', + $exception->getElementNumber(), + $this->presenter->presentValue($exception->getExpectedKey()), + $this->presenter->presentValue($exception->getExpectedValue()), + $this->presenter->presentValue($exception->getSubjectKey()), + $this->presenter->presentValue($exception->getSubjectValue()) + ), 0, $exception); } }