diff --git a/src/Framework/Constraint/ArraySubset.php b/src/Framework/Constraint/ArraySubset.php index d2199df7741..bdf68e43742 100644 --- a/src/Framework/Constraint/ArraySubset.php +++ b/src/Framework/Constraint/ArraySubset.php @@ -64,13 +64,11 @@ public function evaluate($other, $description = '', $returnResult = false) $other = $this->toArray($other); $this->subset = $this->toArray($this->subset); - $patched = \array_replace_recursive($other, $this->subset); + $intersect = $this->arrayIntersectRecursive($other, $this->subset); + $this->deepSort($intersect); + $this->deepSort($this->subset); - if ($this->strict) { - $result = $other === $patched; - } else { - $result = $other == $patched; - } + $result = $this->compare($intersect, $this->subset); if ($returnResult) { return $result; @@ -78,9 +76,9 @@ public function evaluate($other, $description = '', $returnResult = false) if (!$result) { $f = new ComparisonFailure( - $patched, + $this->subset, $other, - \print_r($patched, true), + \print_r($this->subset, true), \print_r($other, true) ); @@ -130,4 +128,83 @@ private function toArray(iterable $other): array // Keep BC even if we know that array would not be the expected one return (array) $other; } + + private function isAssociative(array $array): bool + { + return \array_reduce(\array_keys($array), function (bool $carry, $key): bool { + return $carry || \is_string($key); + }, false); + } + + private function compare($first, $second): bool + { + return $this->strict ? $first === $second : $first == $second; + } + + private function deepSort(array &$array): void + { + foreach ($array as &$value) { + if (\is_array($value)) { + $this->deepSort($value); + } + } + + if ($this->isAssociative($array)) { + \ksort($array); + } else { + \sort($array); + } + } + + private function arrayIntersectRecursive(array $array, array $subset): array + { + $intersect = []; + + if ($this->isAssociative($subset)) { + // If the subset is an associative array, get the intersection while + // preserving the keys. + foreach ($subset as $key => $subset_value) { + if (\array_key_exists($key, $array)) { + $array_value = $array[$key]; + + if (\is_array($subset_value) && \is_array($array_value)) { + $intersect[$key] = $this->arrayIntersectRecursive($array_value, $subset_value); + } elseif ($this->compare($subset_value, $array_value)) { + $intersect[$key] = $array_value; + } + } + } + } else { + // If the subset is an indexed array, loop over all entries in the + // haystack and check if they match the ones in the subset. + foreach ($array as $array_value) { + if (\is_array($array_value)) { + foreach ($subset as $key => $subset_value) { + if (\is_array($subset_value)) { + $recursed = $this->arrayIntersectRecursive($array_value, $subset_value); + + if (!empty($recursed)) { + $intersect[$key] = $recursed; + + break; + } + } + } + } else { + foreach ($subset as $key => $subset_value) { + if (!\is_array($subset_value) && $this->compare( + $subset_value, + $array_value + )) { + $intersect[$key] = $array_value; + + break; + } + } + } + } + } + + return $intersect; + } } diff --git a/tests/Framework/AssertTest.php b/tests/Framework/AssertTest.php index b8b8a4e3192..c4e923d6455 100644 --- a/tests/Framework/AssertTest.php +++ b/tests/Framework/AssertTest.php @@ -159,13 +159,17 @@ public function testAssertArraySubset(): void 'd' => ['a2' => ['a3' => 'item a3', 'b3' => 'item b3']] ]; + $this->assertArraySubset(['a' => 'item a'], $array); $this->assertArraySubset(['a' => 'item a', 'c' => ['a2' => 'item a2']], $array); $this->assertArraySubset(['a' => 'item a', 'd' => ['a2' => ['b3' => 'item b3']]], $array); + $this->assertArraySubset(['b' => 'item b', 'd' => ['a2' => ['b3' => 'item b3']]], $array); $arrayAccessData = new \ArrayObject($array); + $this->assertArraySubset(['a' => 'item a'], $arrayAccessData); $this->assertArraySubset(['a' => 'item a', 'c' => ['a2' => 'item a2']], $arrayAccessData); $this->assertArraySubset(['a' => 'item a', 'd' => ['a2' => ['b3' => 'item b3']]], $arrayAccessData); + $this->assertArraySubset(['b' => 'item b', 'd' => ['a2' => ['b3' => 'item b3']]], $arrayAccessData); try { $this->assertArraySubset(['a' => 'bad value'], $array); @@ -181,6 +185,39 @@ public function testAssertArraySubset(): void $this->fail(); } + public function testAssertArraySubsetWithIndexedArrays(): void + { + $array = [ + 'item a', + 'item b', + ['a2' => 'item a2', 'b2' => 'item b2'], + ['a2' => ['a3' => 'item a3', 'b3' => 'item b3']] + ]; + + $this->assertArraySubset(['item a', ['a2' => 'item a2']], $array); + $this->assertArraySubset(['item a', ['a2' => ['b3' => 'item b3']]], $array); + $this->assertArraySubset(['item b', ['a2' => ['b3' => 'item b3']]], $array); + + $arrayAccessData = new \ArrayObject($array); + + $this->assertArraySubset(['item a', ['a2' => 'item a2']], $arrayAccessData); + $this->assertArraySubset(['item a', ['a2' => ['b3' => 'item b3']]], $arrayAccessData); + $this->assertArraySubset(['item b', ['a2' => ['b3' => 'item b3']]], $arrayAccessData); + + try { + $this->assertArraySubset(['bad value'], $array); + } catch (AssertionFailedError $e) { + } + + try { + $this->assertArraySubset([['a2' => ['bad index' => 'item b3']]], $array); + } catch (AssertionFailedError $e) { + return; + } + + $this->fail(); + } + public function testAssertArraySubsetWithDeepNestedArrays(): void { $array = [ diff --git a/tests/Framework/Constraint/ArraySubsetTest.php b/tests/Framework/Constraint/ArraySubsetTest.php index 9cf24ae337a..20b90fefcb5 100644 --- a/tests/Framework/Constraint/ArraySubsetTest.php +++ b/tests/Framework/Constraint/ArraySubsetTest.php @@ -17,30 +17,146 @@ class ArraySubsetTest extends ConstraintTestCase public static function evaluateDataProvider() { return [ - 'loose array subset and array other' => [ + 'loose associative array subset and array other' => [ 'expected' => true, 'subset' => ['bar' => 0], 'other' => ['foo' => '', 'bar' => '0'], 'strict' => false ], - 'strict array subset and array other' => [ + 'strict associative array subset and array other' => [ 'expected' => false, 'subset' => ['bar' => 0], 'other' => ['foo' => '', 'bar' => '0'], 'strict' => true ], - 'loose array subset and ArrayObject other' => [ + 'loose associative array subset and ArrayObject other' => [ 'expected' => true, 'subset' => ['bar' => 0], 'other' => new \ArrayObject(['foo' => '', 'bar' => '0']), 'strict' => false ], - 'strict ArrayObject subset and array other' => [ + 'strict associative ArrayObject subset and array other' => [ 'expected' => true, 'subset' => new \ArrayObject(['bar' => 0]), 'other' => ['foo' => '', 'bar' => 0], 'strict' => true ], + 'loose indexed array subset and array other' => [ + 'expected' => true, + 'subset' => [0], + 'other' => ['', '0'], + 'strict' => false + ], + 'strict indexed array subset and array other' => [ + 'expected' => false, + 'subset' => [0], + 'other' => ['', '0'], + 'strict' => true + ], + 'loose indexed array subset and ArrayObject other' => [ + 'expected' => true, + 'subset' => [0], + 'other' => new \ArrayObject(['', '0']), + 'strict' => false + ], + 'strict indexed ArrayObject subset and array other' => [ + 'expected' => true, + 'subset' => new \ArrayObject([0]), + 'other' => ['', 0], + 'strict' => true + ], + 'loose unordered indexed array subset and array other' => [ + 'expected' => true, + 'subset' => [0, '1'], + 'other' => ['1', '2', '0'], + 'strict' => false + ], + 'strict unordered indexed array subset and array other' => [ + 'expected' => false, + 'subset' => [0, '1'], + 'other' => ['1', '2', '0'], + 'strict' => true + ], + 'loose unordered indexed array subset and ArrayObject other' => [ + 'expected' => true, + 'subset' => [0, '1'], + 'other' => new \ArrayObject(['1', '2', '0']), + 'strict' => false + ], + 'strict unordered indexed ArrayObject subset and array other' => [ + 'expected' => true, + 'subset' => new \ArrayObject([0, '1']), + 'other' => ['1', '2', 0], + 'strict' => true + ], + 'loose unordered multidimensional indexed array subset and array other' => [ + 'expected' => true, + 'subset' => [ + [[3, 4], 2], + '10', + ], + 'other' => [ + 0 => '1', + 'a' => [ + 'aa' => '2', + 'ab' => [5, 4, 3], + 'ac' => 10, + ], + 'b' => '10', + ], + 'strict' => false + ], + 'strict unordered multidimensional indexed array subset and array other' => [ + 'expected' => false, + 'subset' => [ + [[3, 4], 2], + '10', + ], + 'other' => [ + 0 => '1', + 'a' => [ + 'aa' => '2', + 'ab' => [5, 4, 3], + 'ac' => 10, + ], + 'b' => '10', + ], + 'strict' => true + ], + 'loose unordered multidimensional indexed array subset and ArrayObject other' => [ + 'expected' => true, + 'subset' => [ + [[3, 4], 2], + '10', + ], + 'other' => new \ArrayObject([ + 0 => '1', + 'a' => [ + 'aa' => '2', + 'ab' => [5, 4, 3], + 'ac' => 10, + ], + 'b' => '10', + ]), + 'strict' => false + ], + 'strict unordered multidimensional indexed ArrayObject subset and array other' => [ + 'expected' => true, + 'subset' => new \ArrayObject([ + [[3, 4], '2'], + '10', + ]), + 'other' => [ + 0 => '1', + 'a' => [ + 'aa' => '2', + 'ab' => [5, 4, 3], + 'ac' => 10, + ], + 'b' => '10', + ], + 'strict' => true + ], ]; }