Skip to content

Commit

Permalink
Extract iterables matching logic to a standalone class
Browse files Browse the repository at this point in the history
  • Loading branch information
pamil committed Oct 19, 2016
1 parent 993a38c commit 4b7d3fc
Show file tree
Hide file tree
Showing 10 changed files with 452 additions and 33 deletions.
134 changes: 134 additions & 0 deletions spec/PhpSpec/Matcher/Iterate/IterablesMatcherSpec.php
@@ -0,0 +1,134 @@
<?php

namespace spec\PhpSpec\Matcher\Iterate;

use PhpSpec\Matcher\Iterate\SubjectElementDoesNotMatchException;
use PhpSpec\Matcher\Iterate\SubjectHasLessElementsException;
use PhpSpec\Matcher\Iterate\SubjectHasMoreElementsException;
use PhpSpec\ObjectBehavior;

/**
* @author Kamil Kokot <kamil.kokot@lakion.com>
*/
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;
}
}
}
@@ -0,0 +1,33 @@
<?php

namespace spec\PhpSpec\Matcher\Iterate;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class SubjectElementDoesNotMatchExceptionSpec extends ObjectBehavior
{
function let()
{
$this->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');
}
}
@@ -0,0 +1,19 @@
<?php

namespace spec\PhpSpec\Matcher\Iterate;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

final class SubjectHasLessElementsExceptionSpec extends ObjectBehavior
{
function it_is_a_length_exception()
{
$this->shouldHaveType(\LengthException::class);
}

function it_has_a_predefined_message()
{
$this->getMessage()->shouldReturn('Subject has less elements than expected.');
}
}
@@ -0,0 +1,19 @@
<?php

namespace spec\PhpSpec\Matcher\Iterate;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

final class SubjectHasMoreElementsExceptionSpec extends ObjectBehavior
{
function it_is_a_length_exception()
{
$this->shouldHaveType(\LengthException::class);
}

function it_has_a_predefined_message()
{
$this->getMessage()->shouldReturn('Subject has more elements than expected.');
}
}
4 changes: 2 additions & 2 deletions spec/PhpSpec/Matcher/IterateMatcherSpec.php
Expand Up @@ -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']),
Expand All @@ -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']),
Expand Down
79 changes: 79 additions & 0 deletions src/PhpSpec/Matcher/Iterate/IterablesMatcher.php
@@ -0,0 +1,79 @@
<?php

namespace PhpSpec\Matcher\Iterate;

final class IterablesMatcher
{
/**
* @param array|\Traversable $subject
* @param array|\Traversable $expected
*
* @throws \InvalidArgumentException
* @throws SubjectElementDoesNotMatchException
* @throws SubjectHasLessElementsException
* @throws SubjectHasMoreElementsException
*/
public function match($subject, $expected)
{
if (!$this->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;
}
}

0 comments on commit 4b7d3fc

Please sign in to comment.