@@ -70,7 +70,7 @@ public void isEqualTo(@NullableDecl Object other) {
70
70
return ;
71
71
}
72
72
73
- boolean mapEquals = containsExactlyEntriesInAnyOrder ((Map <?, ?>) other , "is equal to" );
73
+ boolean mapEquals = containsEntriesInAnyOrder ((Map <?, ?>) other , "is equal to" , false );
74
74
if (mapEquals ) {
75
75
failWithoutActual (
76
76
simpleFact (
@@ -194,11 +194,16 @@ public Ordered containsExactly() {
194
194
*/
195
195
@ CanIgnoreReturnValue
196
196
public Ordered containsExactly (@ NullableDecl Object k0 , @ NullableDecl Object v0 , Object ... rest ) {
197
- return containsExactlyEntriesIn (accumulateMap (k0 , v0 , rest ));
197
+ return containsExactlyEntriesIn (accumulateMap ("containsExactly" , k0 , v0 , rest ));
198
+ }
199
+
200
+ @ CanIgnoreReturnValue
201
+ public Ordered containsAtLeast (@ NullableDecl Object k0 , @ NullableDecl Object v0 , Object ... rest ) {
202
+ return containsAtLeastEntriesIn (accumulateMap ("containsAtLeast" , k0 , v0 , rest ));
198
203
}
199
204
200
205
private static Map <Object , Object > accumulateMap (
201
- @ NullableDecl Object k0 , @ NullableDecl Object v0 , Object ... rest ) {
206
+ String functionName , @ NullableDecl Object k0 , @ NullableDecl Object v0 , Object ... rest ) {
202
207
checkArgument (
203
208
rest .length % 2 == 0 ,
204
209
"There must be an equal number of key/value pairs "
@@ -216,8 +221,9 @@ private static Map<Object, Object> accumulateMap(
216
221
}
217
222
checkArgument (
218
223
keys .size () == expectedMap .size (),
219
- "Duplicate keys (%s) cannot be passed to containsExactly()." ,
220
- keys );
224
+ "Duplicate keys (%s) cannot be passed to %s()." ,
225
+ keys ,
226
+ functionName );
221
227
return expectedMap ;
222
228
}
223
229
@@ -232,18 +238,35 @@ public Ordered containsExactlyEntriesIn(Map<?, ?> expectedMap) {
232
238
return ALREADY_FAILED ;
233
239
}
234
240
}
235
- boolean containsAnyOrder = containsExactlyEntriesInAnyOrder (expectedMap , "contains exactly" );
241
+ boolean containsAnyOrder =
242
+ containsEntriesInAnyOrder (expectedMap , "contains exactly" , /* allowUnexpected= */ false );
236
243
if (containsAnyOrder ) {
237
244
return new MapInOrder (expectedMap , "contains exactly these entries in order" );
238
245
} else {
239
246
return ALREADY_FAILED ;
240
247
}
241
248
}
242
249
250
+ /** Fails if the map does not contain at least the given set of entries in the given map. */
243
251
@ CanIgnoreReturnValue
244
- private boolean containsExactlyEntriesInAnyOrder (Map <?, ?> expectedMap , String failVerb ) {
252
+ public Ordered containsAtLeastEntriesIn (Map <?, ?> expectedMap ) {
253
+ if (expectedMap .isEmpty ()) {
254
+ return IN_ORDER ;
255
+ }
256
+ boolean containsAnyOrder =
257
+ containsEntriesInAnyOrder (expectedMap , "contains at least" , /* allowUnexpected= */ true );
258
+ if (containsAnyOrder ) {
259
+ return new MapInOrder (expectedMap , "contains at least these entries in order" );
260
+ } else {
261
+ return ALREADY_FAILED ;
262
+ }
263
+ }
264
+
265
+ @ CanIgnoreReturnValue
266
+ private boolean containsEntriesInAnyOrder (
267
+ Map <?, ?> expectedMap , String failVerb , boolean allowUnexpected ) {
245
268
MapDifference <Object , Object , Object > diff =
246
- MapDifference .create (actual (), expectedMap , EQUALITY );
269
+ MapDifference .create (actual (), expectedMap , allowUnexpected , EQUALITY );
247
270
if (diff .isEmpty ()) {
248
271
return true ;
249
272
}
@@ -274,10 +297,12 @@ private static class MapDifference<K, A, E> {
274
297
private final Map <K , E > missing ;
275
298
private final Map <K , A > unexpected ;
276
299
private final Map <K , ValueDifference <A , E >> wrongValues ;
300
+ private final Set <K > allKeys ;
277
301
278
302
static <K , A , E > MapDifference <K , A , E > create (
279
303
Map <? extends K , ? extends A > actual ,
280
304
Map <? extends K , ? extends E > expected ,
305
+ boolean allowUnexpected ,
281
306
ValueTester <? super A , ? super E > valueTester ) {
282
307
Map <K , A > unexpected = new LinkedHashMap <>(actual );
283
308
Map <K , E > missing = new LinkedHashMap <>();
@@ -294,14 +319,22 @@ static <K, A, E> MapDifference<K, A, E> create(
294
319
missing .put (expectedKey , expectedValue );
295
320
}
296
321
}
297
- return new MapDifference <>(missing , unexpected , wrongValues );
322
+ if (allowUnexpected ) {
323
+ unexpected .clear ();
324
+ }
325
+ return new MapDifference <>(
326
+ missing , unexpected , wrongValues , Sets .union (actual .keySet (), expected .keySet ()));
298
327
}
299
328
300
329
private MapDifference (
301
- Map <K , E > missing , Map <K , A > unexpected , Map <K , ValueDifference <A , E >> wrongValues ) {
330
+ Map <K , E > missing ,
331
+ Map <K , A > unexpected ,
332
+ Map <K , ValueDifference <A , E >> wrongValues ,
333
+ Set <K > allKeys ) {
302
334
this .missing = missing ;
303
335
this .unexpected = unexpected ;
304
336
this .wrongValues = wrongValues ;
337
+ this .allKeys = allKeys ;
305
338
}
306
339
307
340
boolean isEmpty () {
@@ -343,7 +376,7 @@ private boolean includeKeyTypes() {
343
376
keys .addAll (missing .keySet ());
344
377
keys .addAll (unexpected .keySet ());
345
378
keys .addAll (wrongValues .keySet ());
346
- return hasMatchingToStringPair (keys , keys );
379
+ return hasMatchingToStringPair (keys , allKeys );
347
380
}
348
381
}
349
382
@@ -413,10 +446,19 @@ private class MapInOrder implements Ordered {
413
446
this .failVerb = failVerb ;
414
447
}
415
448
449
+ /**
450
+ * Checks whether the common elements between actual and expected are in the same order.
451
+ *
452
+ * <p>This doesn't check whether the keys have the same values or whether all the required keys
453
+ * are actually present. That was supposed to be done before the "in order" part.
454
+ */
416
455
@ Override
417
456
public void inOrder () {
418
- List <?> expectedKeyOrder = Lists .newArrayList (expectedMap .keySet ());
419
- List <?> actualKeyOrder = Lists .newArrayList (actual ().keySet ());
457
+ // We're using the fact that Sets.intersection keeps the order of the first set.
458
+ List <?> expectedKeyOrder =
459
+ Lists .newArrayList (Sets .intersection (expectedMap .keySet (), actual ().keySet ()));
460
+ List <?> actualKeyOrder =
461
+ Lists .newArrayList (Sets .intersection (actual ().keySet (), expectedMap .keySet ()));
420
462
if (!actualKeyOrder .equals (expectedKeyOrder )) {
421
463
failWithoutActual (
422
464
simpleFact (
@@ -614,10 +656,29 @@ public void doesNotContainEntry(
614
656
@ CanIgnoreReturnValue
615
657
public Ordered containsExactly (@ NullableDecl Object k0 , @ NullableDecl E v0 , Object ... rest ) {
616
658
@ SuppressWarnings ("unchecked" ) // throwing ClassCastException is the correct behaviour
617
- Map <Object , E > expectedMap = (Map <Object , E >) accumulateMap (k0 , v0 , rest );
659
+ Map <Object , E > expectedMap = (Map <Object , E >) accumulateMap ("containsExactly" , k0 , v0 , rest );
618
660
return containsExactlyEntriesIn (expectedMap );
619
661
}
620
662
663
+ /**
664
+ * Fails if the map does not contain at least the given set of keys mapping to values that
665
+ * correspond to the given values.
666
+ *
667
+ * <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
668
+ * if any other type is encountered.
669
+ *
670
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
671
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
672
+ */
673
+ // TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
674
+ // For bonus points, checking that the even-numbered values are of type E would be sweet.
675
+ @ CanIgnoreReturnValue
676
+ public Ordered containsAtLeast (@ NullableDecl Object k0 , @ NullableDecl E v0 , Object ... rest ) {
677
+ @ SuppressWarnings ("unchecked" ) // throwing ClassCastException is the correct behaviour
678
+ Map <Object , E > expectedMap = (Map <Object , E >) accumulateMap ("containsAtLeast" , k0 , v0 , rest );
679
+ return containsAtLeastEntriesIn (expectedMap );
680
+ }
681
+
621
682
/**
622
683
* Fails if the map does not contain exactly the keys in the given map, mapping to values that
623
684
* correspond to the values of the given map.
@@ -632,11 +693,29 @@ public <K, V extends E> Ordered containsExactlyEntriesIn(Map<K, V> expectedMap)
632
693
return ALREADY_FAILED ;
633
694
}
634
695
}
696
+ return internalContainsEntriesIn ("exactly" , expectedMap , false );
697
+ }
698
+
699
+ /**
700
+ * Fails if the map does not contain at least the keys in the given map, mapping to values that
701
+ * correspond to the values of the given map.
702
+ */
703
+ @ CanIgnoreReturnValue
704
+ public <K , V extends E > Ordered containsAtLeastEntriesIn (Map <K , V > expectedMap ) {
705
+ if (expectedMap .isEmpty ()) {
706
+ return IN_ORDER ;
707
+ }
708
+ return internalContainsEntriesIn ("at least" , expectedMap , true );
709
+ }
710
+
711
+ private <K , V extends E > Ordered internalContainsEntriesIn (
712
+ String modifier , Map <K , V > expectedMap , boolean allowUnexpected ) {
635
713
final Correspondence .ExceptionStore exceptions = Correspondence .ExceptionStore .forMapValues ();
636
714
MapDifference <Object , A , V > diff =
637
715
MapDifference .create (
638
716
getCastSubject (),
639
717
expectedMap ,
718
+ allowUnexpected ,
640
719
new ValueTester <A , E >() {
641
720
@ Override
642
721
public boolean test (A actualValue , E expectedValue ) {
@@ -650,18 +729,19 @@ public boolean test(A actualValue, E expectedValue) {
650
729
return new MapInOrder (
651
730
expectedMap ,
652
731
lenientFormat (
653
- "contains, in order, exactly one entry that has a key that is equal to and a value "
654
- + "that %s the key and value of each entry of" ,
655
- correspondence ));
732
+ "contains, in order, %s one entry that has a key that is equal to and a "
733
+ + "value that %s the key and value of each entry of" ,
734
+ modifier , correspondence ));
656
735
}
657
736
failWithoutActual (
658
737
facts (
659
738
simpleFact (
660
739
lenientFormat (
661
- "Not true that %s contains exactly one entry that has a key that is "
740
+ "Not true that %s contains %s one entry that has a key that is "
662
741
+ "equal to and a value that %s the key and value of each entry of "
663
742
+ "<%s>. It %s" ,
664
743
actualAsString (),
744
+ modifier ,
665
745
correspondence ,
666
746
expectedMap ,
667
747
diff .describe (this .<V >valueDiffFormat (exceptions )))))
0 commit comments