88use ArrayObject ;
99use InvalidArgumentException ;
1010use RuntimeException ;
11+ use SebastianBergmann \Comparator \ComparisonFailure ;
12+ use SebastianBergmann \Comparator \Factory ;
1113use stdClass ;
1214use Symfony \Bridge \PhpUnit \ConstraintTrait ;
1315
@@ -31,6 +33,12 @@ class DocumentsMatchConstraint extends Constraint
3133 private $ sortKeys = false ;
3234 private $ value ;
3335
36+ /** @var ComparisonFailure|null */
37+ private $ lastFailure ;
38+
39+ /** @var Factory */
40+ private $ comparatorFactory ;
41+
3442 /**
3543 * Creates a new constraint.
3644 *
@@ -45,6 +53,44 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
4553 $ this ->ignoreExtraKeysInRoot = $ ignoreExtraKeysInRoot ;
4654 $ this ->ignoreExtraKeysInEmbedded = $ ignoreExtraKeysInEmbedded ;
4755 $ this ->placeholders = $ placeholders ;
56+ $ this ->comparatorFactory = Factory::getInstance ();
57+ }
58+
59+ public function evaluate ($ other , $ description = '' , $ returnResult = false )
60+ {
61+ /* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
62+ * able to skip preparation, convert both documents to extended JSON,
63+ * and compare strings.
64+ *
65+ * If ignoreExtraKeys is false and sortKeys is true, we still be able to
66+ * compare JSON strings but will still require preparation to sort keys
67+ * in all documents and sub-documents. */
68+ $ other = $ this ->prepareBSON ($ other , true , $ this ->sortKeys );
69+
70+ $ success = false ;
71+ $ this ->lastFailure = null ;
72+
73+ try {
74+ $ this ->assertEquals ($ this ->value , $ other , $ this ->ignoreExtraKeysInRoot );
75+ $ success = true ;
76+ } catch (RuntimeException $ e ) {
77+ $ this ->lastFailure = new ComparisonFailure (
78+ $ this ->value ,
79+ $ other ,
80+ $ this ->exporter ()->export ($ this ->value ),
81+ $ this ->exporter ()->export ($ other ),
82+ false ,
83+ $ e ->getMessage ()
84+ );
85+ }
86+
87+ if ($ returnResult ) {
88+ return $ success ;
89+ }
90+
91+ if (!$ success ) {
92+ $ this ->fail ($ other , $ description , $ this ->lastFailure );
93+ }
4894 }
4995
5096 /**
@@ -53,19 +99,24 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
5399 * @param ArrayObject $expected
54100 * @param ArrayObject $actual
55101 * @param boolean $ignoreExtraKeys
102+ * @param string $keyPrefix
56103 * @throws RuntimeException if the documents do not match
57104 */
58- private function assertEquals (ArrayObject $ expected , ArrayObject $ actual , $ ignoreExtraKeys )
105+ private function assertEquals (ArrayObject $ expected , ArrayObject $ actual , $ ignoreExtraKeys, $ keyPrefix = '' )
59106 {
60107 if (get_class ($ expected ) !== get_class ($ actual )) {
61- throw new RuntimeException (sprintf ('$expected is %s but $actual is %s ' , get_class ($ expected ), get_class ($ actual )));
108+ throw new RuntimeException (sprintf (
109+ '%s is not instance of expected class "%s" ' ,
110+ $ this ->exporter ()->shortenedExport ($ actual ),
111+ get_class ($ expected )
112+ ));
62113 }
63114
64115 foreach ($ expected as $ key => $ expectedValue ) {
65116 $ actualHasKey = $ actual ->offsetExists ($ key );
66117
67118 if (!$ actualHasKey ) {
68- throw new RuntimeException ('$actual is missing key: ' . $ key );
119+ throw new RuntimeException (sprintf ( '$actual is missing key: "%s" ' , $ keyPrefix . $ key) );
69120 }
70121
71122 if (in_array ($ expectedValue , $ this ->placeholders , true )) {
@@ -76,12 +127,53 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
76127
77128 if (($ expectedValue instanceof BSONArray && $ actualValue instanceof BSONArray) ||
78129 ($ expectedValue instanceof BSONDocument && $ actualValue instanceof BSONDocument)) {
79- $ this ->assertEquals ($ expectedValue , $ actualValue , $ this ->ignoreExtraKeysInEmbedded );
130+ $ this ->assertEquals ($ expectedValue , $ actualValue , $ this ->ignoreExtraKeysInEmbedded , $ keyPrefix . $ key . ' . ' );
80131 continue ;
81132 }
82133
83- if (gettype ($ expectedValue ) != gettype ($ actualValue ) || $ expectedValue != $ actualValue ) {
84- throw new RuntimeException ('$expectedValue != $actualValue for key: ' . $ key );
134+ if (is_scalar ($ expectedValue ) && is_scalar ($ actualValue )) {
135+ if ($ expectedValue !== $ actualValue ) {
136+ throw new ComparisonFailure (
137+ $ expectedValue ,
138+ $ actualValue ,
139+ '' ,
140+ '' ,
141+ false ,
142+ sprintf ('Field path "%s": %s ' , $ keyPrefix . $ key , 'Failed asserting that two values are equal. ' )
143+ );
144+ }
145+
146+ continue ;
147+ }
148+
149+ // Workaround for ObjectComparator printing the whole actual object
150+ if (get_class ($ expectedValue ) !== get_class ($ actualValue )) {
151+ throw new ComparisonFailure (
152+ $ expectedValue ,
153+ $ actualValue ,
154+ '' ,
155+ '' ,
156+ false ,
157+ \sprintf (
158+ 'Field path "%s": %s is not instance of expected class "%s". ' ,
159+ $ keyPrefix . $ key ,
160+ $ this ->exporter ()->shortenedExport ($ actualValue ),
161+ get_class ($ expectedValue )
162+ )
163+ );
164+ }
165+
166+ try {
167+ $ this ->comparatorFactory ->getComparatorFor ($ expectedValue , $ actualValue )->assertEquals ($ expectedValue , $ actualValue );
168+ } catch (ComparisonFailure $ failure ) {
169+ throw new ComparisonFailure (
170+ $ expectedValue ,
171+ $ actualValue ,
172+ '' ,
173+ '' ,
174+ false ,
175+ sprintf ('Field path "%s": %s ' , $ keyPrefix . $ key , $ failure ->getMessage ())
176+ );
85177 }
86178 }
87179
@@ -91,11 +183,25 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
91183
92184 foreach ($ actual as $ key => $ value ) {
93185 if (!$ expected ->offsetExists ($ key )) {
94- throw new RuntimeException ('$actual has extra key: ' . $ key );
186+ throw new RuntimeException (sprintf ( '$actual has extra key: "%s" ' , $ keyPrefix . $ key) );
95187 }
96188 }
97189 }
98190
191+ private function doAdditionalFailureDescription ($ other )
192+ {
193+ if ($ this ->lastFailure === null ) {
194+ return '' ;
195+ }
196+
197+ return $ this ->lastFailure ->getMessage ();
198+ }
199+
200+ private function doFailureDescription ($ other )
201+ {
202+ return 'two BSON objects are equal ' ;
203+ }
204+
99205 private function doMatches ($ other )
100206 {
101207 /* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
@@ -118,7 +224,7 @@ private function doMatches($other)
118224
119225 private function doToString ()
120226 {
121- return 'matches ' . json_encode ($ this ->value );
227+ return 'matches ' . $ this -> exporter ()-> export ($ this ->value );
122228 }
123229
124230 /**
0 commit comments