Skip to content

Commit 0d32959

Browse files
committed
Add constraint
1 parent 5b6b531 commit 0d32959

File tree

1 file changed

+242
-0
lines changed

1 file changed

+242
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPUnit\Framework\Constraint\Dictionary;
6+
7+
use PHPUnit\Framework\Constraint\Constraint;
8+
use PHPUnit\Framework\ExpectationFailedException;
9+
use SebastianBergmann\Comparator\ComparisonFailure;
10+
use SebastianBergmann\Exporter\Exporter;
11+
12+
final class IsIdenticalKeysValues extends Constraint
13+
{
14+
private readonly mixed $value;
15+
16+
public function __construct(mixed $value)
17+
{
18+
$this->value = $value;
19+
}
20+
21+
/**
22+
* Evaluates the constraint for parameter $other.
23+
*
24+
* If $returnResult is set to false (the default), an exception is thrown
25+
* in case of a failure. null is returned otherwise.
26+
*
27+
* If $returnResult is true, the result of the evaluation is returned as
28+
* a boolean value instead: true in case of success, false in case of a
29+
* failure.
30+
*
31+
* @throws ExpectationFailedException
32+
*/
33+
public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool
34+
{
35+
assert(is_array($this->value));
36+
assert(is_array($other));
37+
38+
// cribbed from `src/Framework/Constraint/Equality/IsEqualCanonicalizing.php`
39+
try {
40+
$this->compareDictionary($this->value, $other);
41+
} catch (ComparisonFailure $f) {
42+
if ($returnResult) {
43+
return false;
44+
}
45+
throw new ExpectationFailedException(
46+
trim($description . "\n" . $f->getMessage()),
47+
$f
48+
);
49+
}
50+
return true;
51+
}
52+
53+
/**
54+
* Returns a string representation of the constraint.
55+
*/
56+
public function toString(): string
57+
{
58+
return 'is identical to ' . (new Exporter)->export($this->value);
59+
}
60+
61+
/**
62+
* cribbed from `vendor/sebastian/comparator/src/ArrayComparator.php`
63+
* This potentially should be a dictionarycomparator or type-strict arraycomparator
64+
*/
65+
private function compareDictionary(array $expected, array $actual, array &$processed = []): void
66+
{
67+
$remaining = $actual;
68+
$actualAsString = "Array (\n";
69+
$expectedAsString = "Array (\n";
70+
$equal = true;
71+
$exporter = new Exporter;
72+
73+
foreach ($expected as $key => $value) {
74+
unset($remaining[$key]);
75+
76+
if (!array_key_exists($key, $actual)) {
77+
$expectedAsString .= sprintf(
78+
" %s => %s\n",
79+
$exporter->export($key),
80+
$exporter->shortenedExport($value),
81+
);
82+
$equal = false;
83+
continue;
84+
}
85+
86+
try {
87+
switch (true) {
88+
// type mismatch, expected array, got something else
89+
case is_array($value) && !is_array($actual[$key]):
90+
throw new ComparisonFailure(
91+
$value,
92+
$actual[$key],
93+
$exporter->export($value),
94+
$exporter->export($actual[$key]),
95+
);
96+
97+
// expected array, got array
98+
case is_array($value) && is_array($actual[$key]):
99+
$this->compareDictionary($value, $actual[$key]);
100+
break;
101+
102+
// type mismatch, expected object, got something else
103+
case is_object($value) && !is_object($actual[$key]):
104+
throw new ComparisonFailure(
105+
$value,
106+
$actual[$key],
107+
$exporter->export($value),
108+
$exporter->export($actual[$key]),
109+
);
110+
111+
// type mismatch, expected object, got object
112+
case is_object($value) && is_object($actual[$key]):
113+
$this->compareObjects($value, $actual[$key], $processed);
114+
break;
115+
116+
// both are not array, both are not objects, strict comparison check
117+
default:
118+
if ($value === $actual[$key]) {
119+
continue 2;
120+
}
121+
throw new ComparisonFailure(
122+
$value,
123+
$actual[$key],
124+
$exporter->export($value),
125+
$exporter->export($actual[$key]),
126+
);
127+
}
128+
129+
$expectedAsString .= sprintf(
130+
" %s => %s\n",
131+
$exporter->export($key),
132+
$exporter->shortenedExport($value),
133+
);
134+
$actualAsString .= sprintf(
135+
" %s => %s\n",
136+
$exporter->export($key),
137+
$exporter->shortenedExport($actual[$key]),
138+
);
139+
} catch (ComparisonFailure $e) {
140+
$expectedAsString .= sprintf(
141+
" %s => %s\n",
142+
$exporter->export($key),
143+
$e->getExpectedAsString() !== '' ? $this->indent(
144+
$e->getExpectedAsString()
145+
) : $exporter->shortenedExport($e->getExpected()),
146+
);
147+
$actualAsString .= sprintf(
148+
" %s => %s\n",
149+
$exporter->export($key),
150+
$e->getActualAsString() !== '' ? $this->indent(
151+
$e->getActualAsString()
152+
) : $exporter->shortenedExport($e->getActual()),
153+
);
154+
$equal = false;
155+
}
156+
}
157+
158+
foreach ($remaining as $key => $value) {
159+
$actualAsString .= sprintf(
160+
" %s => %s\n",
161+
$exporter->export($key),
162+
$exporter->shortenedExport($value),
163+
);
164+
$equal = false;
165+
}
166+
167+
$expectedAsString .= ')';
168+
$actualAsString .= ')';
169+
170+
if (!$equal) {
171+
throw new ComparisonFailure(
172+
$expected,
173+
$actual,
174+
$expectedAsString,
175+
$actualAsString,
176+
'Failed asserting that two arrays are equal.',
177+
);
178+
}
179+
}
180+
181+
/**
182+
* cribbed from `vendor/sebastian/comparator/src/ObjectComparator.php`
183+
* this potentially should be a type-strict objectcomparator
184+
*/
185+
private function compareObjects(object $expected, object $actual, array &$processed = [])
186+
{
187+
if ($actual::class !== $expected::class) {
188+
$exporter = new Exporter;
189+
190+
throw new ComparisonFailure(
191+
$expected,
192+
$actual,
193+
$exporter->export($expected),
194+
$exporter->export($actual),
195+
sprintf(
196+
'%s is not instance of expected class "%s".',
197+
$exporter->export($actual),
198+
$expected::class,
199+
),
200+
);
201+
}
202+
203+
// don't compare twice to allow for cyclic dependencies
204+
if (in_array([$actual, $expected], $processed, true) ||
205+
in_array([$expected, $actual], $processed, true)) {
206+
return;
207+
}
208+
209+
$processed[] = [$actual, $expected];
210+
if ($actual === $expected) {
211+
return;
212+
}
213+
try {
214+
$this->compareDictionary($this->toArray($expected), $this->toArray($actual), $processed);
215+
} catch (ComparisonFailure $e) {
216+
throw new ComparisonFailure(
217+
$expected,
218+
$actual,
219+
// replace "Array" with "MyClass object"
220+
substr_replace($e->getExpectedAsString(), $expected::class . ' Object', 0, 5),
221+
substr_replace($e->getActualAsString(), $actual::class . ' Object', 0, 5),
222+
'Failed asserting that two objects are equal.',
223+
);
224+
}
225+
}
226+
227+
/**
228+
* cribbed from `vendor/sebastian/comparator/src/ObjectComparator.php`
229+
*/
230+
private function toArray(object $object): array
231+
{
232+
return (new Exporter)->toArray($object);
233+
}
234+
235+
/**
236+
* cribbed from `vendor/sebastian/comparator/src/ArrayComparator.php`
237+
*/
238+
private function indent(string $lines): string
239+
{
240+
return trim(str_replace("\n", "\n ", $lines));
241+
}
242+
}

0 commit comments

Comments
 (0)