diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cbc81e5df..917a1685db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,6 +67,7 @@ jobs: if: always() run: ./gradlew --stop - uses: andymckay/cancel-action@0.2 + continue-on-error: true if: failure() guava: @@ -98,6 +99,7 @@ jobs: if: always() run: ./gradlew --stop - uses: andymckay/cancel-action@0.2 + continue-on-error: true if: failure() event_file: diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java index b23359ecbb..89d36e331e 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java @@ -32,7 +32,6 @@ import static java.util.stream.Collectors.toMap; import java.util.AbstractMap; -import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -60,6 +59,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.ConcurrentTestHarness; import com.github.benmanes.caffeine.testing.Int; @@ -72,6 +72,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ +@CheckNoEvictions @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) public final class AsMapTest { @@ -104,7 +105,7 @@ public void clear(Map map, CacheContext context) { map.clear(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } /* --------------- contains --------------- */ @@ -237,11 +238,8 @@ public void forEach_scan(Map map, CacheContext context) { removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void forEach_modify(Map map, CacheContext context) { // non-deterministic traversal behavior with modifications, but shouldn't become corrupted - @SuppressWarnings("ModifiedButNotUsed") - var modified = new ArrayList(); map.forEach((key, value) -> { - Int newKey = context.lastKey().add(key); - modified.add(newKey); // for weak keys + Int newKey = intern(context.lastKey().add(key)); map.put(newKey, key); }); } @@ -282,33 +280,34 @@ public void put_insert(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_sameValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = intern(new Int(context.original().get(key))); assertThat(map.put(key, value)).isSameInstanceAs(context.original().get(key)); assertThat(map).containsEntry(key, value); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @CheckNoStats @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.put(key, value)).isSameInstanceAs(context.original().get(key)); assertThat(map).containsEntry(key, value); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -318,18 +317,17 @@ public void put_replace_sameInstance(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.put(key, context.absentValue())).isEqualTo(value); assertThat(map).containsEntry(key, context.absentValue()); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -396,8 +394,7 @@ public void putAll_replace(Map map, CacheContext context) { map.putAll(entries); assertThat(map).isEqualTo(entries); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } @CheckNoStats @@ -408,7 +405,7 @@ public void putAll_mixed(Map map, CacheContext context) { var replaced = new HashMap(); context.original().forEach((key, value) -> { if ((key.intValue() % 2) == 0) { - value = value.add(1); + value = intern(value.add(1)); replaced.put(key, value); } entries.put(key, value); @@ -421,8 +418,7 @@ public void putAll_mixed(Map map, CacheContext context) { entry.setValue(context.original().get(entry.getKey())); } assertThat(context).removalNotifications().withCause(REPLACED) - .contains(expect.entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(expect).exclusively(); } /* --------------- putIfAbsent --------------- */ @@ -516,14 +512,16 @@ public void remove_absent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void remove_present(Map map, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { - map.remove(key); + Int value = map.remove(key); + removed.put(key, value); } int expectedSize = context.original().size() - context.firstMiddleLastKeys().size(); assertThat(map).hasSize(expectedSize); - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @Test(dataProvider = "caches") @@ -595,13 +593,16 @@ public void removeConditionally_presentKey(Map map, CacheContext conte @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void removeConditionally_presentKeyAndValue(Map map, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.remove(key, value)).isTrue(); + removed.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(map).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @Test(dataProvider = "caches") @@ -664,33 +665,34 @@ public void replace_absent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replace_sameValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = intern(new Int(context.original().get(key))); assertThat(map.replace(key, value)).isSameInstanceAs(context.original().get(key)); assertThat(map).containsEntry(key, value); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @CheckNoStats @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replace_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.replace(key, value)).isSameInstanceAs(value); assertThat(map).containsEntry(key, value); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -700,17 +702,16 @@ public void replace_sameInstance(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replace_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int oldValue = context.original().get(key); assertThat(map.replace(key, context.absentValue())).isEqualTo(oldValue); assertThat(map).containsEntry(key, context.absentValue()); + replaced.put(key, oldValue); } assertThat(map).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -813,34 +814,35 @@ public void replaceConditionally_wrongOldValue(Map map, CacheContext c @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replaceConditionally_sameValue(Cache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var newValue = intern(new Int(cache.asMap().get(key))); assertThat(cache.asMap().replace(key, oldValue, newValue)).isTrue(); assertThat(cache).containsEntry(key, newValue); + replaced.put(key, oldValue); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @CheckNoStats @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replaceConditionally_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.replace(key, value, value)).isTrue(); assertThat(map).containsEntry(key, value); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -850,16 +852,16 @@ public void replaceConditionally_sameInstance(Map map, CacheContext co @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replaceConditionally_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { - assertThat(map.replace(key, context.original().get(key), context.absentValue())).isTrue(); + Int value = context.original().get(key); + assertThat(map.replace(key, value, context.absentValue())).isTrue(); assertThat(map).containsEntry(key, context.absentValue()); + replaced.put(key, value); } assertThat(map).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -914,8 +916,7 @@ public void replaceAll_sameValue(Map map, CacheContext context) { if (context.isGuava()) { assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -930,8 +931,7 @@ public void replaceAll_differentValue(Map map, CacheContext context) { assertThat(value).isEqualTo(key); }); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } /* --------------- computeIfAbsent --------------- */ @@ -1017,7 +1017,7 @@ public void computeIfAbsent_absent(Map map, CacheContext context) { .isEqualTo(context.absentValue()); assertThat(context).stats().hits(0).misses(1).success(1).failures(0); assertThat(map).containsEntry(context.absentKey(), context.absentValue()); - assertThat(map).hasSize(1 + context.initialSize()); + assertThat(map).hasSize(context.initialSize() + 1); } @Test(dataProvider = "caches") @@ -1064,15 +1064,17 @@ public void computeIfPresent_nullMappingFunction(Map map, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_nullValue(Map map, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { map.computeIfPresent(key, (k, v) -> null); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(map).hasSize(context.initialSize() - count); assertThat(context).stats().hits(0).misses(0).success(0).failures(count); - assertThat(context).removalNotifications() - .withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -1140,30 +1142,29 @@ public void computeIfPresent_absent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_present_sameValue(Map map, CacheContext context) { - var expectedMap = new HashMap(); + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var value = intern(new Int(context.original().get(key))); assertThat(map.computeIfPresent(key, (k, v) -> value)).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(map).hasSize(context.initialSize()); - assertThat(map).containsAtLeastEntriesIn(expectedMap); + assertThat(map).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_present_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { assertThat(map.computeIfPresent(key, (k, v) -> v)) .isSameInstanceAs(context.original().get(key)); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1174,7 +1175,8 @@ public void computeIfPresent_present_sameInstance(Map map, CacheContex assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -1183,8 +1185,10 @@ public void computeIfPresent_present_sameInstance(Map map, CacheContex @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_present_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { assertThat(map.computeIfPresent(key, (k, v) -> k)).isEqualTo(key); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1193,10 +1197,7 @@ public void computeIfPresent_present_differentValue(Map map, CacheCont assertThat(map).containsEntry(key, key); } assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -1242,14 +1243,17 @@ public void compute_nullMappingFunction(Map map, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_remove(Map map, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { assertThat(map.compute(key, (k, v) -> null)).isNull(); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(map).hasSize(context.initialSize() - count); assertThat(context).stats().hits(0).misses(0).success(0).failures(count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CacheSpec @@ -1292,7 +1296,7 @@ public void compute_error(Map map, CacheContext context) { assertThat(map).isEqualTo(context.original()); assertThat(context).stats().hits(0).misses(0).success(0).failures(1); - assertThat(map.compute(context.absentKey(), (k, v) -> k.negate())) + assertThat(map.compute(context.absentKey(), (k, v) -> intern(k.negate()))) .isEqualTo(context.absentKey().negate()); } @@ -1318,30 +1322,29 @@ public void compute_absent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_sameValue(Map map, CacheContext context) { - var expectedMap = new HashMap(); + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = intern(new Int(context.original().get(key))); assertThat(map.compute(key, (k, v) -> value)).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(map).hasSize(context.initialSize()); - assertThat(map).containsAtLeastEntriesIn(expectedMap); + assertThat(map).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.compute(key, (k, v) -> value)).isSameInstanceAs(value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1353,7 +1356,8 @@ public void compute_sameInstance(Map map, CacheContext context) { assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -1362,8 +1366,10 @@ public void compute_sameInstance(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { assertThat(map.compute(key, (k, v) -> k)).isEqualTo(key); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1372,10 +1378,7 @@ public void compute_differentValue(Map map, CacheContext context) { assertThat(map).containsEntry(key, key); } assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -1430,14 +1433,17 @@ public void merge_nullMappingFunction(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_remove(Map map, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.merge(key, value, (oldValue, v) -> null)).isNull(); + removed.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(map).hasSize(context.initialSize() - count); assertThat(context).stats().hits(0).misses(0).success(0).failures(count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -1501,30 +1507,29 @@ public void merge_absent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_sameValue(Map map, CacheContext context) { - var expectedMap = new HashMap(); + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = intern(new Int(context.original().get(key))); assertThat(map.merge(key, key.negate(), (oldValue, v) -> value)).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(map).hasSize(context.initialSize()); - assertThat(map).containsAtLeastEntriesIn(expectedMap); + assertThat(map).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_sameInstance(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); assertThat(map.merge(key, key.negate(), (oldValue, v) -> value)).isSameInstanceAs(value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1536,7 +1541,8 @@ public void merge_sameInstance(Map map, CacheContext context) { assertThat(map).hasSize(context.initialSize()); if (context.isGuava()) { - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { assertThat(context).removalNotifications().isEmpty(); } @@ -1545,8 +1551,10 @@ public void merge_sameInstance(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_differentValue(Map map, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { - assertThat(map.merge(key, key, (oldValue, v) -> oldValue.add(v))).isEqualTo(0); + assertThat(map.merge(key, key, (oldValue, v) -> intern(oldValue.add(v)))).isEqualTo(0); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1555,10 +1563,7 @@ public void merge_differentValue(Map map, CacheContext context) { assertThat(map).containsEntry(key, Int.valueOf(0)); } assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -1745,7 +1750,7 @@ public void keySet_clear(Map map, CacheContext context) { map.keySet().clear(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1778,7 +1783,8 @@ public void keySet_iterator(Map map, CacheContext context) { int count = iterations; assertThat(map).isExhaustivelyEmpty(); assertThat(count).isEqualTo(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } @CheckNoStats @@ -1915,7 +1921,7 @@ public void values_clear(Map map, CacheContext context) { map.values().clear(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1956,7 +1962,7 @@ public void values(Map map, CacheContext context) { } assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1972,7 +1978,8 @@ public void valueIterator(Map map, CacheContext context) { int count = iterations; assertThat(map).isExhaustivelyEmpty(); assertThat(count).isEqualTo(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } @CheckNoStats @@ -2132,7 +2139,7 @@ public void entrySet_clear(Map map, CacheContext context) { map.entrySet().clear(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -2173,7 +2180,7 @@ public void entrySet(Map map, CacheContext context) { }); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -2190,7 +2197,8 @@ public void entryIterator(Map map, CacheContext context) { int count = iterations; assertThat(map).isExhaustivelyEmpty(); assertThat(count).isEqualTo(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } @CheckNoStats diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncAsMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncAsMapTest.java index b24fe0cf0e..bdc7cc9823 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncAsMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncAsMapTest.java @@ -32,7 +32,6 @@ import static java.util.stream.Collectors.toMap; import java.util.AbstractMap; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,6 +55,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.Int; import com.google.common.base.Splitter; @@ -67,6 +67,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ +@CheckNoEvictions @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) public final class AsyncAsMapTest { @@ -95,7 +96,7 @@ public void clear(AsyncCache cache, CacheContext context) { cache.asMap().clear(); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } /* ---------------- contains -------------- */ @@ -229,11 +230,8 @@ public void forEach_scan(AsyncCache cache, CacheContext context) { removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void forEach_modify(AsyncCache cache, CacheContext context) { // non-deterministic traversal behavior with modifications, but shouldn't become corrupted - @SuppressWarnings("ModifiedButNotUsed") - var modified = new ArrayList(); cache.asMap().forEach((key, value) -> { - Int newKey = context.lastKey().add(key); - modified.add(newKey); // for weak keys + Int newKey = intern(context.lastKey().add(key)); cache.synchronous().put(newKey, key); }); } @@ -273,18 +271,17 @@ public void put_insert(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_sameValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var value = cache.asMap().get(key).thenApply(val -> intern(new Int(val))); assertThat(cache.asMap().put(key, value)).isSameInstanceAs(oldValue); assertThat(cache).containsEntry(key, value); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -302,18 +299,17 @@ public void put_replace_sameInstance(AsyncCache cache, CacheContext co @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_differentValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var newValue = context.absentValue().asFuture(); assertThat(cache.asMap().put(key, newValue)).succeedsWith(context.original().get(key)); assertThat(cache).containsEntry(key, newValue); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- putAll -------------- */ @@ -352,8 +348,7 @@ public void putAll_replace(AsyncCache cache, CacheContext context) { cache.asMap().putAll(entries); assertThat(cache).containsExactlyEntriesIn(entries); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -373,8 +368,7 @@ public void putAll_mixed(AsyncCache cache, CacheContext context) { cache.asMap().putAll(entries); assertThat(cache).containsExactlyEntriesIn(entries); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(replaced.entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- putIfAbsent -------------- */ @@ -443,13 +437,14 @@ public void remove_absent(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void remove_present(AsyncCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { cache.asMap().remove(key); + removed.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize() - context.firstMiddleLastKeys().size()); - - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } /* ---------------- remove conditionally -------------- */ @@ -500,13 +495,16 @@ public void removeConditionally_presentKey(AsyncCache cache, CacheCont @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void removeConditionally_presentKeyAndValue( AsyncCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var value = cache.asMap().get(key); + removed.put(key, context.original().get(key)); assertThat(cache.asMap().remove(key, value)).isTrue(); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } /* ---------------- replace -------------- */ @@ -552,18 +550,17 @@ public void replace_failure(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replace_sameValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var newValue = cache.asMap().get(key).thenApply(val -> intern(new Int(val))); assertThat(cache.asMap().replace(key, newValue)).isSameInstanceAs(oldValue); assertThat(cache).containsEntry(key, newValue); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -581,18 +578,17 @@ public void replace_sameInstance(AsyncCache cache, CacheContext contex @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replace_differentValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var value = context.absentValue().asFuture(); + replaced.put(key, context.original().get(key)); assertThat(cache.asMap().replace(key, value)).isEqualTo(oldValue); assertThat(cache).containsEntry(key, value); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- replace conditionally -------------- */ @@ -680,18 +676,17 @@ public void replaceConditionally_wrongOldValue(AsyncCache cache, Cache @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replaceConditionally_sameValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var newValue = cache.asMap().get(key).thenApply(val -> intern(new Int(val))); assertThat(cache.asMap().replace(key, oldValue, newValue)).isTrue(); assertThat(cache).containsEntry(key, newValue); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -710,18 +705,17 @@ public void replaceConditionally_sameInstance(AsyncCache cache, CacheC @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void replaceConditionally_differentValue( AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var oldValue = cache.asMap().get(key); var value = context.absentValue().asFuture(); assertThat(cache.asMap().replace(key, oldValue, value)).isTrue(); assertThat(cache).containsEntry(key, value); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- replaceAll -------------- */ @@ -757,8 +751,7 @@ public void replaceAll_differentValue(AsyncCache cache, CacheContext c assertThat(value).succeedsWith(key); }); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } /* ---------------- computeIfAbsent -------------- */ @@ -875,14 +868,16 @@ public void computeIfPresent_nullMappingFunction( @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_nullValue(AsyncCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { cache.asMap().computeIfPresent(key, (k, v) -> null); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(count).exclusively(); + .contains(removed).exclusively(); } @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -954,21 +949,19 @@ public void computeIfPresent_absent(AsyncCache cache, CacheContext con @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_present_sameValue(AsyncCache cache, CacheContext context) { - var expectedMap = new HashMap>(); + var replaced = new HashMap>(); for (Int key : context.firstMiddleLastKeys()) { var value = intern(new Int(context.original().get(key))).asFuture(); assertThat(cache.asMap().computeIfPresent(key, (k, v) -> value)).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(cache).hasSize(context.initialSize()); - assertThat(cache.asMap()).containsAtLeastEntriesIn(expectedMap); + assertThat(cache.asMap()).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) + .contains(Maps.transformValues(replaced, CompletableFuture::join)) .exclusively(); } @@ -990,8 +983,10 @@ public void computeIfPresent_present_sameInstance( @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void computeIfPresent_present_differentValue( AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var value = key.asFuture(); + replaced.put(key, context.original().get(key)); assertThat(cache.asMap().computeIfPresent(key, (k, v) -> value)).isEqualTo(value); } int count = context.firstMiddleLastKeys().size(); @@ -1002,10 +997,7 @@ public void computeIfPresent_present_differentValue( } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- compute -------------- */ @@ -1028,13 +1020,16 @@ public void compute_nullMappingFunction(AsyncCache cache, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_remove(AsyncCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { assertThat(cache.asMap().compute(key, (k, v) -> null)).isNull(); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CacheSpec @@ -1104,21 +1099,19 @@ public void compute_absent(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_sameValue(AsyncCache cache, CacheContext context) { - var expectedMap = new HashMap>(); + var replaced = new HashMap>(); for (Int key : context.firstMiddleLastKeys()) { var value = intern(new Int(context.original().get(key))).asFuture(); assertThat(cache.asMap().compute(key, (k, v) -> value)).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(cache).hasSize(context.initialSize()); - assertThat(cache.asMap()).containsAtLeastEntriesIn(expectedMap); + assertThat(cache.asMap()).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) + .contains(Maps.transformValues(replaced, CompletableFuture::join)) .exclusively(); } @@ -1143,9 +1136,11 @@ public void compute_sameInstance(AsyncCache cache, CacheContext contex @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_differentValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var value = key.asFuture(); assertThat(cache.asMap().compute(key, (k, v) -> value)).isEqualTo(value); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1155,10 +1150,7 @@ public void compute_differentValue(AsyncCache cache, CacheContext cont } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- merge -------------- */ @@ -1188,13 +1180,16 @@ public void merge_nullMappingFunction(AsyncCache cache, CacheContext c @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_remove(AsyncCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var value = cache.asMap().get(key); assertThat(cache.asMap().merge(key, value, (oldValue, v) -> null)).isNull(); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -1266,22 +1261,20 @@ public void merge_absent(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_sameValue(AsyncCache cache, CacheContext context) { - var expectedMap = new HashMap>(); + var replaced = new HashMap>(); for (Int key : context.firstMiddleLastKeys()) { var value = cache.asMap().get(key).thenApply(Int::new); var result = cache.asMap().merge(key, key.negate().asFuture(), (oldValue, v) -> value); assertThat(result).isSameInstanceAs(value); - expectedMap.put(key, value); + replaced.put(key, value); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); assertThat(cache).hasSize(context.initialSize()); - assertThat(cache.asMap()).containsAtLeastEntriesIn(expectedMap); + assertThat(cache.asMap()).containsAtLeastEntriesIn(replaced); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) + .contains(Maps.transformValues(replaced, CompletableFuture::join)) .exclusively(); } @@ -1307,11 +1300,13 @@ public void merge_sameInstance(AsyncCache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void merge_differentValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); Int mergedValue = context.absentValue(); for (Int key : context.firstMiddleLastKeys()) { var result = cache.asMap().merge(key, key.asFuture(), (oldValue, v) -> mergedValue.asFuture()); assertThat(result).succeedsWith(mergedValue); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -1321,10 +1316,7 @@ public void merge_differentValue(AsyncCache cache, CacheContext contex } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* ---------------- equals / hashCode -------------- */ @@ -1480,7 +1472,7 @@ public void keySet_clear(AsyncCache cache, CacheContext context) { cache.asMap().keySet().clear(); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1512,7 +1504,8 @@ public void keySet_iterator(AsyncCache cache, CacheContext context) { } assertThat(cache).isEmpty(); assertThat(count).isEqualTo(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } @CheckNoStats @@ -1653,7 +1646,7 @@ public void values_clear(AsyncCache cache, CacheContext context) { cache.asMap().values().clear(); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1693,7 +1686,7 @@ public void values(AsyncCache cache, CacheContext context) { } assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1709,7 +1702,7 @@ public void valueIterator(AsyncCache cache, CacheContext context) { assertThat(cache).isEmpty(); assertThat(count).isEqualTo(context.initialSize()); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(count).exclusively(); + .contains(context.original()).exclusively(); } @CheckNoStats @@ -1885,7 +1878,7 @@ public void entrySet_clear(AsyncCache cache, CacheContext context) { cache.asMap().entrySet().clear(); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1926,7 +1919,7 @@ public void entrySet(AsyncCache cache, CacheContext context) { }); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -1944,7 +1937,8 @@ public void entryIterator(AsyncCache cache, CacheContext context) { int count = iterations; assertThat(cache).isEmpty(); assertThat(count).isEqualTo(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } @CheckNoStats diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncCacheTest.java index f03f5e58a3..a26de1888f 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncCacheTest.java @@ -24,7 +24,6 @@ import static com.github.benmanes.caffeine.testing.FutureSubject.assertThat; import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; -import static java.util.Map.entry; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import static org.mockito.ArgumentMatchers.any; @@ -58,6 +57,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.Int; import com.google.common.collect.ImmutableSet; @@ -69,6 +69,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ +@CheckNoEvictions @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) @SuppressWarnings("FutureReturnValueIgnored") @@ -934,31 +935,33 @@ public void put_replace_failure_after(AsyncCache cache, CacheContext c @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_nullValue(AsyncCache cache, CacheContext context) { + var removed = new HashMap(); var value = CompletableFuture.completedFuture((Int) null); for (Int key : context.firstMiddleLastKeys()) { cache.put(key, value); assertThat(cache).doesNotContainKey(key); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_differentValue(AsyncCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var newValue = context.absentValue().asFuture(); cache.put(key, newValue); assertThat(cache).containsEntry(key, newValue); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } /* --------------- misc --------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java index ea923e6459..e7193b21ac 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java @@ -56,6 +56,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Loader; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.Int; import com.google.common.collect.ImmutableSet; @@ -67,6 +68,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ +@CheckNoEvictions @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) @SuppressWarnings({"FutureReturnValueIgnored", "PreferJavaTimeOverload"}) @@ -408,15 +410,16 @@ private static final class LoadAllException extends RuntimeException {} @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace(AsyncLoadingCache cache, CacheContext context) { + var replaced = new HashMap(); var value = context.absentValue().asFuture(); for (Int key : context.firstMiddleLastKeys()) { cache.put(key, value); assertThat(cache.get(key)).succeedsWith(context.absentValue()); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); - - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } /* --------------- refresh --------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java index 16c8cd74eb..c909b30609 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java @@ -26,6 +26,7 @@ import static com.github.benmanes.caffeine.cache.RemovalCause.COLLECTED; import static com.github.benmanes.caffeine.cache.RemovalCause.EXPIRED; import static com.github.benmanes.caffeine.cache.RemovalCause.EXPLICIT; +import static com.github.benmanes.caffeine.cache.RemovalCause.REPLACED; import static com.github.benmanes.caffeine.cache.RemovalCause.SIZE; import static com.github.benmanes.caffeine.cache.testing.CacheContextSubject.assertThat; import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_ACCESS; @@ -36,6 +37,7 @@ import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static java.lang.Thread.State.BLOCKED; +import static java.util.Map.entry; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -182,8 +184,7 @@ public void rescheduleDrainBuffers() { } @Test(dataProvider = "caches") - @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, - population = Population.FULL, maximumSize = Maximum.FULL, + @CacheSpec(compute = Compute.SYNC, population = Population.FULL, maximumSize = Maximum.FULL, executor = CacheExecutor.REJECTING, executorFailure = ExecutorFailure.EXPECTED, removalListener = Listener.CONSUMING) public void scheduleDrainBuffers_rejected( @@ -296,7 +297,8 @@ public void evict_alreadyRemoved(BoundedLocalCache cache, CacheContext checkStatus(node, Status.DEAD); assertThat(cache).containsKey(newEntry.getKey()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(oldEntry).exclusively(); } finally { cache.evictionLock.unlock(); } @@ -425,8 +427,11 @@ public void evict_update_entryTooBig_window( cache.put(Int.valueOf(1), Int.valueOf(20)); assertThat(cache.weightedSize()).isAtMost(context.maximumSize()); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(Int.valueOf(1), Int.valueOf(1)); assertThat(context).removalNotifications().withCause(SIZE) .contains(Int.valueOf(1), Int.valueOf(20)); + assertThat(context).removalNotifications().hasSize(2); } @Test(dataProvider = "caches") @@ -444,8 +449,11 @@ public void evict_update_entryTooBig_probation( cache.put(Int.valueOf(1), Int.valueOf(20)); assertThat(cache.weightedSize()).isAtMost(context.maximumSize()); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(Int.valueOf(1), Int.valueOf(1)); assertThat(context).removalNotifications().withCause(SIZE) .contains(Int.valueOf(1), Int.valueOf(20)); + assertThat(context).removalNotifications().hasSize(2); } @Test(dataProvider = "caches") @@ -465,8 +473,11 @@ public void evict_update_entryTooBig_protected( cache.put(Int.valueOf(1), Int.valueOf(20)); assertThat(cache.weightedSize()).isAtMost(context.maximumSize()); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(Int.valueOf(1), Int.valueOf(1)); assertThat(context).removalNotifications().withCause(SIZE) .contains(Int.valueOf(1), Int.valueOf(20)); + assertThat(context).removalNotifications().hasSize(2); } @Test(dataProvider = "caches") @@ -535,7 +546,8 @@ public void evict_resurrect_weight(Cache> cache, CacheContext con await().untilTrue(done); assertThat(cache).containsEntry(key, List.of()); - assertThat(context).removalNotifications().withCause(SIZE).isEmpty(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(entry(key, List.of(key))).exclusively(); } @Test(dataProvider = "caches") @@ -567,7 +579,8 @@ public void evict_resurrect_expireAfter(Cache cache, CacheContext cont await().untilTrue(done); assertThat(cache).containsEntry(key, key.negate()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(key, key).exclusively(); } @Test(dataProvider = "caches") @@ -659,7 +672,8 @@ public void evict_resurrect_expireAfterWrite_entry(Cache cache, CacheC await().untilTrue(done); assertThat(cache).containsEntry(key, key.negate()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(key, key).exclusively(); } @Test(dataProvider = "caches") @@ -700,12 +714,15 @@ public void evict_resurrect_expireAfterVar( keys = ReferenceType.WEAK, removalListener = Listener.CONSUMING) public void evict_collected_candidate(BoundedLocalCache cache, CacheContext context) { var candidate = cache.accessOrderWindowDeque().getFirst(); + var value = candidate.getValue(); + @SuppressWarnings("unchecked") var keyReference = (WeakKeyReference) candidate.getKeyReference(); keyReference.clear(); cache.put(context.absentKey(), context.absentValue()); - assertThat(context).removalNotifications().withCause(COLLECTED).hasSize(1); + assertThat(context).removalNotifications().withCause(COLLECTED) + .contains(null, value).exclusively(); } @Test(dataProvider = "caches") @@ -714,6 +731,8 @@ public void evict_collected_candidate(BoundedLocalCache cache, CacheCo keys = ReferenceType.WEAK, removalListener = Listener.CONSUMING) public void evict_collected_victim(BoundedLocalCache cache, CacheContext context) { var victim = cache.accessOrderProbationDeque().getFirst(); + var value = victim.getValue(); + @SuppressWarnings("unchecked") var keyReference = (WeakKeyReference) victim.getKeyReference(); keyReference.clear(); @@ -721,7 +740,8 @@ public void evict_collected_victim(BoundedLocalCache cache, CacheConte cache.setMaximumSize(cache.size() - 1); cache.cleanUp(); - assertThat(context).removalNotifications().withCause(COLLECTED).hasSize(1); + assertThat(context).removalNotifications().withCause(COLLECTED) + .contains(null, value).exclusively(); } @Test(dataProvider = "caches") @@ -1065,9 +1085,9 @@ public void put_expireTolerance_expiry(BoundedLocalCache cache, CacheC } @Test(dataProvider = "caches", groups = "isolated") - @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - refreshAfterWrite = Expire.DISABLED, expireAfterAccess = Expire.DISABLED, - expireAfterWrite = Expire.DISABLED, expiry = CacheExpiry.DISABLED, + @CacheSpec(population = Population.EMPTY, + expireAfterAccess = Expire.DISABLED, expireAfterWrite = Expire.DISABLED, + refreshAfterWrite = Expire.DISABLED, expiry = CacheExpiry.DISABLED, maximumSize = Maximum.UNREACHABLE, weigher = CacheWeigher.DEFAULT, compute = Compute.SYNC, loader = Loader.DISABLED, stats = Stats.DISABLED, removalListener = Listener.DEFAULT, evictionListener = Listener.DEFAULT, @@ -1342,8 +1362,8 @@ public CompletableFuture asyncReload(Int key, Int oldValue, Executor execut /* --------------- Node --------------- */ @Test(dataProviderClass = CacheProvider.class, dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, population = Population.SINGLETON, - initialCapacity = {InitialCapacity.DEFAULT, InitialCapacity.FULL}, compute = Compute.SYNC) + @CacheSpec(population = Population.SINGLETON, compute = Compute.SYNC, + initialCapacity = {InitialCapacity.DEFAULT, InitialCapacity.FULL}) public void string(BoundedLocalCache cache, CacheContext context) { var node = cache.data.values().iterator().next(); var description = node.toString(); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java index b0bea07c64..b3b72681fe 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java @@ -23,7 +23,6 @@ import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static java.util.Map.entry; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; @@ -66,6 +65,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.Int; import com.google.common.collect.ImmutableMap; @@ -83,6 +83,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ +@CheckNoEvictions @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) public final class CacheTest { @@ -436,7 +437,8 @@ public void getAll_exceeds(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_different(Cache cache, CacheContext context) { - var actual = context.absentKeys().stream().collect(toMap(Int::negate, identity())); + var actual = context.absentKeys().stream() + .collect(toMap(key -> intern(key.negate()), identity())); var result = cache.getAll(context.absentKeys(), keys -> actual); assertThat(result).isEmpty(); @@ -525,7 +527,7 @@ class Key { var keys = new ArrayList(); for (int i = 0; i < Population.FULL.size(); i++) { - keys.add(new Key()); + keys.add(intern(new Key())); } Key key = Iterables.getLast(keys); Int value = context.absentValue(); @@ -557,50 +559,50 @@ public void put_insert(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_sameValue(Cache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); cache.put(key, intern(new Int(value))); assertThat(cache).containsEntry(key, value); + replaced.put(key, value); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_sameInstance(Cache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = context.original().get(key); cache.put(key, value); assertThat(cache).containsEntry(key, value); + replaced.put(key, value); } assertThat(cache).hasSize(context.initialSize()); if (context.isGuava()) { - int count = context.firstMiddleLastKeys().size(); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } else { - assertThat(context).removalNotifications().withCause(REPLACED).isEmpty(); + assertThat(context).removalNotifications().isEmpty(); } } @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void put_replace_differentValue(Cache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { cache.put(key, context.absentValue()); assertThat(cache).containsEntry(key, context.absentValue()); + replaced.put(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.middleKey(), context.original().get(context.middleKey())), - entry(context.lastKey(), context.original().get(context.lastKey()))) - .exclusively(); + .contains(replaced).exclusively(); } @CheckNoStats @@ -642,12 +644,11 @@ public void putAll_insert(Cache cache, CacheContext context) { @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void putAll_replace(Cache cache, CacheContext context) { var entries = new HashMap<>(context.original()); - entries.replaceAll((key, value) -> value.add(1)); + entries.replaceAll((key, value) -> intern(value.add(1))); cache.putAll(entries); assertThat(cache).containsExactlyEntriesIn(entries); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.original().entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -657,7 +658,7 @@ public void putAll_mixed(Cache cache, CacheContext context) { var replaced = new HashMap(); context.original().forEach((key, value) -> { if ((key.intValue() % 2) == 0) { - value = value.add(1); + value = intern(value.add(1)); replaced.put(key, value); } entries.put(key, value); @@ -670,8 +671,7 @@ public void putAll_mixed(Cache cache, CacheContext context) { entry.setValue(context.original().get(entry.getKey())); } assertThat(context).removalNotifications().withCause(REPLACED) - .contains(expect.entrySet().toArray(Map.Entry[]::new)) - .exclusively(); + .contains(expect).exclusively(); } @CheckNoStats @@ -702,12 +702,15 @@ public void invalidate_absent(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void invalidate_present(Cache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { cache.invalidate(key); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @CheckNoStats @@ -725,7 +728,7 @@ public void invalidateAll(Cache cache, CacheContext context) { cache.invalidateAll(); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CheckNoStats @@ -738,13 +741,13 @@ public void invalidateAll_empty(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.PARTIAL, Population.FULL }) public void invalidateAll_partial(Cache cache, CacheContext context) { - var keys = cache.asMap().keySet().stream() - .filter(i -> ((i.intValue() % 2) == 0)) - .collect(Collectors.toList()); - cache.invalidateAll(keys); - assertThat(cache).hasSize(context.initialSize() - keys.size()); + var removals = cache.asMap().entrySet().stream() + .filter(entry -> ((entry.getKey().intValue() % 2) == 0)) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + cache.invalidateAll(removals.keySet()); + assertThat(cache).hasSize(context.initialSize() - removals.size()); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(keys.size()).exclusively(); + .contains(removals).exclusively(); } @Test(dataProvider = "caches") @@ -753,7 +756,7 @@ public void invalidateAll_full(Cache cache, CacheContext context) { cache.invalidateAll(context.original().keySet()); assertThat(cache).isEmpty(); assertThat(context).removalNotifications().withCause(EXPLICIT) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec @@ -769,8 +772,8 @@ public void invalidateAll_null(Cache cache, CacheContext context) { executor = CacheExecutor.REJECTING, removalListener = Listener.CONSUMING) public void removalListener_rejected(Cache cache, CacheContext context) { cache.invalidateAll(); - assertThat(context).removalNotifications() - .withCause(EXPLICIT).hasSize(context.initialSize()).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.original()).exclusively(); } /* --------------- cleanup --------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java index 7a5306a1af..1c0263b0e6 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java @@ -28,6 +28,7 @@ import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static java.util.Map.entry; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.mockito.ArgumentMatchers.any; @@ -36,6 +37,7 @@ import static org.mockito.Mockito.verify; import java.util.ConcurrentModificationException; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -64,6 +66,7 @@ import com.github.benmanes.caffeine.cache.testing.RemovalListeners.RejectingRemovalListener; import com.github.benmanes.caffeine.testing.Int; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; import com.google.common.collect.Range; /** @@ -106,9 +109,14 @@ public void evict(Cache cache, CacheContext context) { } else { assertThat(cache).hasSize(context.maximumSize()); } - int count = context.absentKeys().size(); - assertThat(context).stats().evictions(count); - assertThat(context).notifications().withCause(SIZE).hasSize(count).exclusively(); + + var evicted = new HashMap(); + evicted.putAll(Maps.difference(context.original(), cache.asMap()).entriesOnlyOnLeft()); + evicted.putAll(Maps.difference(context.absent(), cache.asMap()).entriesOnlyOnLeft()); + assertThat(context).stats().evictions(evicted.size()); + assertThat(evicted).hasSize(context.absentKeys().size()); + assertThat(context).notifications().withCause(SIZE) + .contains(evicted).exclusively(); } @Test(dataProvider = "caches") @@ -219,7 +227,8 @@ public void evict_weighted_async(AsyncCache cache, CacheContext contex await().untilAsserted(() -> assertThat(context).hasWeightedSize(10)); assertThat(context).stats().evictionWeight(5); - assertThat(context).notifications().withCause(SIZE).hasSize(1).exclusively(); + assertThat(context).notifications().withCause(SIZE) + .contains(Int.valueOf(5), Int.valueOf(5)).exclusively(); } @Test(dataProvider = "caches") @@ -242,7 +251,9 @@ public void evict_zero_async(AsyncCache> cache, CacheContext cont ready.set(true); await().untilTrue(done); await().untilAsserted(() -> assertThat(cache).isEmpty()); - assertThat(context).notifications().withCause(SIZE).hasSize(1).exclusively(); + assertThat(context).notifications().withCause(SIZE) + .contains(entry(context.absentKey(), Int.listOf(1, 2, 3, 4, 5))) + .exclusively(); } @CheckNoStats @@ -519,7 +530,9 @@ public void maximumSize_decrease(Cache cache, assertThat(context).notifications().isEmpty(); } else { assertThat(cache).hasSize(newSize); - assertThat(context).notifications().withCause(SIZE).hasSize(newSize).exclusively(); + assertThat(context).notifications().withCause(SIZE) + .contains(Maps.difference(context.original(), cache.asMap()).entriesOnlyOnLeft()) + .exclusively(); } } } @@ -535,7 +548,7 @@ public void maximumSize_decrease_min(Cache cache, assertThat(cache).hasSize(expectedSize); } assertThat(context).notifications().withCause(SIZE) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @CacheSpec(maximumSize = Maximum.FULL) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java index c2634c2c0a..1a4ceb040c 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java @@ -28,6 +28,7 @@ import static com.google.common.base.Functions.identity; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static java.util.Map.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -40,6 +41,7 @@ import static org.mockito.Mockito.when; import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -99,7 +101,9 @@ public void expire_zero(Cache cache, CacheContext context) { } else { runVariableExpiration(context); assertThat(cache).isEmpty(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); } } @@ -234,9 +238,9 @@ public void put_insert(Cache cache, CacheContext context) { cache.put(context.firstKey(), context.absentValue()); runVariableExpiration(context); - long count = context.initialSize(); assertThat(cache).hasSize(1); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); if (context.expiryType() == CacheExpiry.MOCKITO) { verify(context.expiry()).expireAfterCreate(any(), any(), anyLong()); @@ -270,8 +274,11 @@ public void put_replace(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -305,9 +312,9 @@ public void putAll_insert(Cache cache, CacheContext context) { cache.putAll(Map.of(context.firstKey(), context.absentValue(), context.middleKey(), context.absentValue(), context.lastKey(), context.absentValue())); - long count = context.initialSize(); assertThat(cache).hasSize(3); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -331,8 +338,10 @@ public void putAll_replace(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED).contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -345,8 +354,8 @@ public void invalidate(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.invalidate(context.firstKey()); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -355,12 +364,12 @@ public void invalidate(Cache cache, CacheContext context) { expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) - public void invalidateAll(Cache cache, CacheContext context) { + public void invalidateAll_iterable(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.invalidateAll(context.firstMiddleLastKeys()); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -373,8 +382,8 @@ public void invalidateAll_full(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.invalidateAll(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -401,8 +410,8 @@ public void cleanUp(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).isEmpty(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } /* --------------- LoadingCache --------------- */ @@ -416,10 +425,10 @@ public void cleanUp(Cache cache, CacheContext context) { public void refresh(LoadingCache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); Int key = context.firstKey(); - assertThat(cache.refresh(key)).succeedsWith(key); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(cache.refresh(key)).succeedsWith(key); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } /* --------------- AsyncCache --------------- */ @@ -453,8 +462,8 @@ public void get(AsyncCache cache, CacheContext context) { cache.get(context.middleKey(), k -> context.absentValue()).join(); cache.get(context.lastKey(), (k, executor) -> context.absentValue().asFuture()).join(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -494,7 +503,8 @@ public void get_async(AsyncCache cache, CacheContext context) { assertThat(cache).doesNotContainKey(context.absentKey()); cache.synchronous().cleanUp(); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()).exclusively(); } @Test(dataProvider = "caches") @@ -523,12 +533,12 @@ public void getAll(AsyncCache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.getAll(context.firstMiddleLastKeys(), keysToLoad -> Maps.asMap(keysToLoad, identity())).join(); - var expectedMap = Maps.asMap(keys, identity()); - assertThat(cache.getAll(keys, keysToLoad -> Maps.asMap(keysToLoad, identity())).join()) - .containsExactlyEntriesIn(expectedMap).inOrder(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = Maps.asMap(keys, identity()); + assertThat(cache.getAll(keys, keysToLoad -> Maps.asMap(keysToLoad, identity())).join()) + .containsExactlyEntriesIn(expected).inOrder(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -543,8 +553,8 @@ public void put_insert(AsyncCache cache, CacheContext context) { runVariableExpiration(context); assertThat(cache).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -568,7 +578,8 @@ public void put_insert_async(AsyncCache cache, CacheContext context) { assertThat(cache).doesNotContainKey(context.absentKey()); cache.synchronous().cleanUp(); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()).exclusively(); } @Test(dataProvider = "caches") @@ -592,8 +603,11 @@ public void put_replace(AsyncCache cache, CacheContext context) { cache.synchronous().cleanUp(); assertThat(cache).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } /* --------------- Map --------------- */ @@ -684,8 +698,8 @@ public void clear(Map map, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); map.clear(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -716,8 +730,8 @@ public void put_insert(Map map, CacheContext context) { assertThat(map.put(context.firstKey(), context.absentValue())).isNull(); assertThat(map).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); if (context.expiryType() == CacheExpiry.MOCKITO) { verify(context.expiry()).expireAfterCreate(any(), any(), anyLong()); @@ -751,8 +765,11 @@ public void put_replace(Map map, CacheContext context) { context.cleanUp(); assertThat(map).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -786,8 +803,8 @@ public void replace(Map map, CacheContext context) { context.cleanUp(); } assertThat(map).isExhaustivelyEmpty(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -837,8 +854,8 @@ public void replaceConditionally(Map map, CacheContext context) { context.cleanUp(); } assertThat(map).isExhaustivelyEmpty(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -883,9 +900,8 @@ public void replaceConditionally_inFlight(AsyncCache cache, CacheConte public void remove(Map map, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); assertThat(map.remove(context.firstKey())).isNull(); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -914,10 +930,10 @@ public void remove_inFlight(AsyncCache cache, CacheContext context) { public void removeConditionally(Map map, CacheContext context) { Int key = context.firstKey(); context.ticker().advance(1, TimeUnit.MINUTES); - assertThat(map.remove(key, context.original().get(key))).isFalse(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(map.remove(key, context.original().get(key))).isFalse(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -950,8 +966,8 @@ public void computeIfAbsent_prescreen(Map map, CacheContext context) { assertThat(result).isEqualTo(context.absentValue()); assertThat(map).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); if (context.expiryType() == CacheExpiry.MOCKITO) { verify(context.expiry()).expireAfterCreate(any(), any(), anyLong()); @@ -971,7 +987,7 @@ public void computeIfAbsent_expiresInCompute(Map map, CacheContext con assertThat(map.computeIfAbsent(context.firstKey(), k -> null)).isNull(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1068,8 +1084,8 @@ public void computeIfPresent_prescreen(Map map, CacheContext context) context.cleanUp(); } - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); if (context.expiryType() == CacheExpiry.MOCKITO) { verifyNoInteractions(context.expiry()); @@ -1091,7 +1107,7 @@ public void computeIfPresent_expiresInCompute(Map map, CacheContext co })).isNull(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1181,8 +1197,16 @@ public void compute(Map map, CacheContext context) { return value; })).isEqualTo(value); - long count = context.initialSize() - map.size() + 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var evicted = new ArrayList>(context.original().size()); + var difference = Maps.difference(context.original(), map); + evicted.addAll(difference.entriesOnlyOnRight().entrySet()); + evicted.addAll(difference.entriesOnlyOnLeft().entrySet()); + evicted.add(entry(key, context.original().get(key))); + + assertThat(evicted).hasSize(context.original().size() - map.size() + 1); + assertThat(context).notifications().withCause(EXPIRED) + .contains(evicted.toArray(Map.Entry[]::new)) + .exclusively(); if (context.expiryType() == CacheExpiry.MOCKITO) { verify(context.expiry()).expireAfterCreate(any(), any(), anyLong()); @@ -1315,8 +1339,16 @@ public void merge(Map map, CacheContext context) { throw new AssertionError("Should never be called"); })).isEqualTo(value); - long count = context.initialSize() - map.size() + 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var evicted = new ArrayList>(context.original().size()); + var difference = Maps.difference(context.original(), map); + evicted.addAll(difference.entriesOnlyOnRight().entrySet()); + evicted.addAll(difference.entriesOnlyOnLeft().entrySet()); + evicted.add(entry(key, context.original().get(key))); + + assertThat(evicted).hasSize(context.original().size() - map.size() + 1); + assertThat(context).notifications().withCause(EXPIRED) + .contains(evicted.toArray(Map.Entry[]::new)) + .exclusively(); } @Test(dataProvider = "caches") @@ -1381,7 +1413,7 @@ public void keySet_iterator(Map map, CacheContext context) { assertThat(map.keySet().iterator().hasNext()).isFalse(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1402,7 +1434,7 @@ public void keySet_iterator_traversal(Map map, CacheContext context) { assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1446,7 +1478,7 @@ public void values_iterator(Map map, CacheContext context) { assertThat(map.values().iterator().hasNext()).isFalse(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1467,7 +1499,7 @@ public void values_iterator_traversal(Map map, CacheContext context) { assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1511,7 +1543,7 @@ public void entrySet_iterator(Map map, CacheContext context) { assertThat(map.keySet().iterator().hasNext()).isFalse(); assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1532,7 +1564,7 @@ public void entrySet_iterator_traversal(Map map, CacheContext context) assertThat(map).isExhaustivelyEmpty(); assertThat(context).removalNotifications().withCause(EXPIRED) - .hasSize(context.initialSize()).exclusively(); + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java index 95d03a4b94..1c59c3d69c 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java @@ -31,6 +31,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ConcurrentModificationException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -82,8 +83,10 @@ public void getIfPresent(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(1); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -101,8 +104,10 @@ public void get(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -118,8 +123,10 @@ public void getAllPresent(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(3); - long count = context.initialSize() - 3; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.keySet().removeAll(context.firstMiddleLastKeys()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -148,8 +155,10 @@ public void getAll(Cache cache, CacheContext context) { context.absentKey(), context.absentKey()); assertThat(cache).hasSize(3); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } /* --------------- LoadingCache --------------- */ @@ -168,10 +177,10 @@ public void get_loading(LoadingCache cache, CacheContext context) { context.ticker().advance(45, TimeUnit.SECONDS); cache.cleanUp(); - assertThat(cache).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(cache).hasSize(1); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -198,8 +207,10 @@ public void getAll_loading(LoadingCache cache, CacheContext context) { context.absentKey(), context.absentKey()); assertThat(cache).hasSize(3); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } /* --------------- AsyncCache --------------- */ @@ -217,8 +228,11 @@ public void getIfPresent_async(AsyncCache cache, CacheContext context) context.cleanUp(); assertThat(cache).hasSize(1); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } /* --------------- Map --------------- */ @@ -235,8 +249,10 @@ public void putIfAbsent(Map map, CacheContext context) { assertThat(map.putIfAbsent(context.lastKey(), context.absentValue())).isNull(); assertThat(map).hasSize(2); - long count = context.initialSize() - 1; - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + var expected = new HashMap<>(context.original()); + expected.remove(context.firstKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } /* --------------- Policy --------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java index 01c09f7fb7..3737294e81 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java @@ -173,8 +173,11 @@ public void put_replace(Cache cache, CacheContext context) { context.cleanUp(); assertThat(cache).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.put(context.firstKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -195,8 +198,11 @@ public void put_replace(AsyncCache cache, CacheContext context) { context.cleanUp(); assertThat(cache).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.put(context.firstKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -216,8 +222,11 @@ public void put_replace(Map map, CacheContext context) { context.cleanUp(); assertThat(map).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.put(context.firstKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -238,8 +247,11 @@ public void putAll_replace(Cache cache, CacheContext context) { context.cleanUp(); assertThat(cache).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + + var expected = new HashMap<>(context.original()); + expected.put(context.firstKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(expected).exclusively(); } @Test(dataProvider = "caches") @@ -899,15 +911,21 @@ public void compute_excessiveDuration(Cache cache, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_remove(Cache cache, CacheContext context, VarExpiration expireAfterVar) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { - assertThat(expireAfterVar.compute(key, (k, v) -> null, Duration.ofDays(1))).isNull(); + assertThat(expireAfterVar.compute(key, (k, v) -> { + assertThat(v).isNotNull(); + return null; + }, Duration.ofDays(1))).isNull(); + removed.put(key, context.original().get(key)); } verifyNoInteractions(context.expiry()); int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize() - count); assertThat(context).stats().hits(0).misses(0).success(0).failures(count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } @Test(dataProvider = "caches") @@ -1003,7 +1021,8 @@ public void compute_absent_expires(Cache cache, verifyNoInteractions(context.expiry()); assertThat(cache).hasSize(context.initialSize()); assertThat(cache).doesNotContainKey(context.absentKey()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()).exclusively(); } @Test(dataProvider = "caches") @@ -1017,7 +1036,8 @@ public void compute_absent_expiresImmediately(Cache cache, verifyNoInteractions(context.expiry()); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()).exclusively(); } @Test(dataProvider = "caches") @@ -1032,7 +1052,8 @@ public void compute_absent_expiresLater(Cache cache, assertThat(cache).hasSize(1); verifyNoInteractions(context.expiry()); assertThat(cache).containsKey(context.absentKey()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -1041,12 +1062,12 @@ public void compute_absent_expiresLater(Cache cache, public void compute_sameValue(Cache cache, CacheContext context, VarExpiration expireAfterVar) { var duration = Duration.ofNanos(context.expiryTime().timeNanos() / 2); - var expectedMap = new HashMap(); + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { Int value = intern(new Int(context.original().get(key))); Int result = expireAfterVar.compute(key, (k, v) -> value, duration); - expectedMap.put(key, value); + replaced.put(key, value); assertThat(result).isSameInstanceAs(value); assertThat(expireAfterVar.getExpiresAfter(key)).hasValue(duration); } @@ -1055,8 +1076,9 @@ public void compute_sameValue(Cache cache, verifyNoInteractions(context.expiry()); assertThat(cache).hasSize(context.initialSize()); - assertThat(cache.asMap()).containsAtLeastEntriesIn(expectedMap); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(cache.asMap()).containsAtLeastEntriesIn(replaced); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -1090,11 +1112,13 @@ public void compute_sameInstance(Cache cache, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_differentValue(Cache cache, CacheContext context, VarExpiration expireAfterVar) { + var replaced = new HashMap(); var duration = Duration.ofNanos(context.expiryTime().timeNanos() / 2); for (Int key : context.firstMiddleLastKeys()) { - Int value = context.original().get(key).negate(); - Int result = expireAfterVar.compute(key, (k, v) -> value, duration); + Int value = context.original().get(key); + Int result = expireAfterVar.compute(key, (k, v) -> value.negate(), duration); + replaced.put(key, value); assertThat(result).isEqualTo(key); assertThat(expireAfterVar.getExpiresAfter(key)).hasValue(duration); } @@ -1106,7 +1130,8 @@ public void compute_differentValue(Cache cache, assertThat(cache).containsEntry(key, key); } assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } @Test(dataProvider = "caches") @@ -1114,15 +1139,21 @@ public void compute_differentValue(Cache cache, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_present_expires(Cache cache, CacheContext context, VarExpiration expireAfterVar) { - expireAfterVar.compute(context.firstKey(), - (k, v) -> context.absentValue(), Duration.ofMinutes(1)); + expireAfterVar.compute(context.firstKey(), (k, v) -> { + assertThat(v).isNotNull(); + return context.absentValue(); + }, Duration.ofMinutes(1)); context.ticker().advance(Duration.ofMinutes(5)); context.cleanUp(); verifyNoInteractions(context.expiry()); assertThat(cache).hasSize(context.initialSize() - 1); assertThat(cache).doesNotContainKey(context.firstKey()); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.firstKey(), context.absentValue()); + assertThat(context).removalNotifications().hasSize(2); } @Test(dataProvider = "caches") @@ -1130,14 +1161,21 @@ public void compute_present_expires(Cache cache, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_present_expiresImmediately(Cache cache, CacheContext context, VarExpiration expireAfterVar) { - expireAfterVar.compute(context.firstKey(), (k, v) -> context.absentValue(), Duration.ZERO); + expireAfterVar.compute(context.firstKey(), (k, v) -> { + assertThat(v).isNotNull(); + return context.absentValue(); + }, Duration.ZERO); assertThat(cache).doesNotContainKey(context.firstKey()); context.ticker().advance(Duration.ofMinutes(1)); context.cleanUp(); verifyNoInteractions(context.expiry()); assertThat(cache).hasSize(context.initialSize() - 1); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(context.firstKey(), context.absentValue()); + assertThat(context).removalNotifications().hasSize(2); } @Test(dataProvider = "caches") @@ -1145,16 +1183,23 @@ public void compute_present_expiresImmediately(Cache cache, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void compute_present_expiresLater(Cache cache, CacheContext context, VarExpiration expireAfterVar) { - expireAfterVar.compute(context.firstKey(), - (k, v) -> context.absentValue(), Duration.ofMinutes(5)); + expireAfterVar.compute(context.firstKey(), (k, v) -> { + assertThat(v).isNotNull(); + return context.absentValue(); + }, Duration.ofMinutes(5)); context.ticker().advance(Duration.ofMinutes(3)); context.cleanUp(); assertThat(cache).hasSize(1); verifyNoInteractions(context.expiry()); assertThat(cache).containsKey(context.firstKey()); - assertThat(context).removalNotifications() - .withCause(EXPIRED).hasSize(context.initialSize() - 1); + + var expired = new HashMap<>(context.original()); + expired.remove(context.firstKey()); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())); + assertThat(context).removalNotifications().withCause(EXPIRED).contains(expired); + assertThat(context).removalNotifications().hasSize(context.initialSize()); } @Test(dataProvider = "caches") diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java index 971ec56a0c..4f72b50558 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java @@ -81,9 +81,8 @@ public void getIfPresent(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).isEmpty(); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -99,9 +98,8 @@ public void get(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).hasSize(1); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -116,9 +114,8 @@ public void getAllPresent(Cache cache, CacheContext context) { cache.cleanUp(); assertThat(cache).isEmpty(); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -140,8 +137,8 @@ public void getAll(Cache cache, CacheContext context) { context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey()); assertThat(cache).hasSize(2); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } /* --------------- LoadingCache --------------- */ @@ -155,12 +152,12 @@ public void get_loading(LoadingCache cache, CacheContext context) { cache.get(context.firstKey()); cache.get(context.absentKey()); context.ticker().advance(45, TimeUnit.SECONDS); + cache.cleanUp(); assertThat(cache).hasSize(1); assertThat(cache).containsEntry(context.absentKey(), context.absentKey().negate()); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } @Test(dataProvider = "caches") @@ -176,13 +173,13 @@ public void getAll_loading(LoadingCache cache, CacheContext context) { context.ticker().advance(45, TimeUnit.SECONDS); cache.cleanUp(); + assertThat(cache.getAll(List.of(context.firstKey(), context.absentKey()))) .containsExactly(context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); assertThat(cache).hasSize(2); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); } /* --------------- AsyncCache --------------- */ @@ -200,8 +197,8 @@ public void getIfPresent(AsyncCache cache, CacheContext context) { context.cleanUp(); assertThat(cache).isEmpty(); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } /* --------------- Map --------------- */ @@ -218,8 +215,8 @@ public void putIfAbsent(Map map, CacheContext context) { assertThat(map.putIfAbsent(context.lastKey(), context.absentValue())).isNull(); assertThat(map).hasSize(1); - long count = context.initialSize(); - assertThat(context).notifications().withCause(EXPIRED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.original()).exclusively(); } /* --------------- Policy --------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java index 65d32b5780..dd154a6d20 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java @@ -28,6 +28,7 @@ import static com.github.benmanes.caffeine.testing.IntSubject.assertThat; import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; +import static java.util.Map.entry; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import static uk.org.lidalia.slf4jext.ConventionalLevelHierarchy.INFO_LEVELS; @@ -63,6 +64,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Maximum; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; import com.github.benmanes.caffeine.testing.Int; import com.github.valfirst.slf4jtest.TestLoggerFactory; @@ -85,11 +87,13 @@ public final class LoadingCacheTest { /* --------------- get --------------- */ @CacheSpec + @CheckNoEvictions @CheckNoStats @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void get_null(LoadingCache cache, CacheContext context) { cache.get(null); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.NULL) public void get_absent_null(LoadingCache cache, CacheContext context) { @@ -97,6 +101,7 @@ public void get_absent_null(LoadingCache cache, CacheContext context) assertThat(context).stats().hits(0).misses(1).success(0).failures(1); } + @CheckNoEvictions @CacheSpec(loader = Loader.EXCEPTIONAL) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void get_absent_throwsException(LoadingCache cache, CacheContext context) { @@ -107,8 +112,9 @@ public void get_absent_throwsException(LoadingCache cache, CacheContex } } - @CacheSpec(loader = Loader.CHECKED_EXCEPTIONAL) + @CheckNoEvictions @Test(dataProvider = "caches") + @CacheSpec(loader = Loader.CHECKED_EXCEPTIONAL) public void get_absent_throwsCheckedException( LoadingCache cache, CacheContext context) { try { @@ -120,6 +126,7 @@ public void get_absent_throwsCheckedException( } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, loader = Loader.INTERRUPTED) public void get_absent_interrupted(LoadingCache cache, CacheContext context) { @@ -134,6 +141,7 @@ public void get_absent_interrupted(LoadingCache cache, CacheContext co } @CacheSpec + @CheckNoEvictions @Test(dataProvider = "caches") public void get_absent(LoadingCache cache, CacheContext context) { Int key = context.absentKey(); @@ -141,6 +149,7 @@ public void get_absent(LoadingCache cache, CacheContext context) { assertThat(context).stats().hits(0).misses(1).success(1).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void get_present(LoadingCache cache, CacheContext context) { @@ -152,12 +161,14 @@ public void get_present(LoadingCache cache, CacheContext context) { /* --------------- getAll --------------- */ + @CheckNoEvictions @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void getAll_iterable_null(LoadingCache cache, CacheContext context) { cache.getAll(null); } + @CheckNoEvictions @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) @@ -165,6 +176,7 @@ public void getAll_iterable_nullKey(LoadingCache cache, CacheContext c cache.getAll(Collections.singletonList(null)); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -173,6 +185,7 @@ public void getAll_iterable_empty(LoadingCache cache, CacheContext con assertThat(context).stats().hits(0).misses(0); } + @CheckNoEvictions @CacheSpec(loader = Loader.BULK_MODIFY_KEYS) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void getAll_immutable_keys(LoadingCache cache, CacheContext context) { @@ -180,23 +193,27 @@ public void getAll_immutable_keys(LoadingCache cache, CacheContext con } @CacheSpec + @CheckNoEvictions @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void getAll_immutable_result(LoadingCache cache, CacheContext context) { cache.getAll(context.absentKeys()).clear(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.NULL) public void getAll_absent_null(LoadingCache cache, CacheContext context) { assertThat(cache.getAll(context.absentKeys())).isExhaustivelyEmpty(); } + @CheckNoEvictions @CacheSpec(loader = Loader.BULK_NULL) @Test(dataProvider = "caches", expectedExceptions = Exception.class) public void getAll_absent_bulkNull(LoadingCache cache, CacheContext context) { cache.getAll(context.absentKeys()); } + @CheckNoEvictions @CacheSpec(loader = { Loader.EXCEPTIONAL, Loader.BULK_EXCEPTIONAL }) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void getAll_absent_throwsExecption(LoadingCache cache, CacheContext context) { @@ -209,6 +226,7 @@ public void getAll_absent_throwsExecption(LoadingCache cache, CacheCon } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.CHECKED_EXCEPTIONAL, Loader.BULK_CHECKED_EXCEPTIONAL }) public void getAll_absent_throwsCheckedExecption( @@ -225,6 +243,7 @@ public void getAll_absent_throwsCheckedExecption( } } + @CheckNoEvictions @CacheSpec(loader = { Loader.EXCEPTIONAL, Loader.BULK_EXCEPTIONAL }) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void getAll_absent_throwsExecption_iterable( @@ -238,6 +257,7 @@ public void getAll_absent_throwsExecption_iterable( } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.INTERRUPTED, Loader.BULK_INTERRUPTED }) public void getAll_absent_interrupted(LoadingCache cache, CacheContext context) { @@ -256,6 +276,7 @@ public void getAll_absent_interrupted(LoadingCache cache, CacheContext } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -268,6 +289,7 @@ public void getAll_absent(LoadingCache cache, CacheContext context) { assertThat(context).stats().hits(0).misses(count).success(loads).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -283,6 +305,7 @@ public void getAll_present_partial(LoadingCache cache, CacheContext co assertThat(context).stats().hits(expect.size()).misses(0).success(0).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -293,6 +316,7 @@ public void getAll_present_full(LoadingCache cache, CacheContext conte assertThat(context).stats().hits(result.size()).misses(0).success(0).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.BULK_NEGATIVE_EXCEEDS }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -304,6 +328,7 @@ public void getAll_exceeds(LoadingCache cache, CacheContext context) { assertThat(context).stats().hits(0).misses(result.size()).success(1).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.BULK_DIFFERENT, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -315,6 +340,7 @@ public void getAll_different(LoadingCache cache, CacheContext context) assertThat(context).stats().hits(0).misses(context.absent().size()).success(1).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -332,6 +358,7 @@ public void getAll_duplicates(LoadingCache cache, CacheContext context .misses(absentKeys.size()).success(loads).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -343,6 +370,7 @@ public void getAll_present_ordered_absent(LoadingCache cache, CacheCon assertThat(cache.getAll(keys).keySet()).containsExactlyElementsIn(keys).inOrder(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL }, @@ -355,6 +383,7 @@ public void getAll_present_ordered_partial(LoadingCache cache, CacheCo assertThat(cache.getAll(keys).keySet()).containsExactlyElementsIn(keys).inOrder(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, @@ -366,6 +395,7 @@ public void getAll_present_ordered_present(LoadingCache cache, CacheCo assertThat(cache.getAll(keys).keySet()).containsExactlyElementsIn(keys).inOrder(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.BULK_NEGATIVE_EXCEEDS, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -378,6 +408,7 @@ public void getAll_present_ordered_exceeds(LoadingCache cache, CacheCo assertThat(result.subList(0, keys.size())).containsExactlyElementsIn(keys).inOrder(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY) public void getAll_jdk8186171(CacheContext context) { @@ -403,14 +434,16 @@ class Key { /* --------------- refresh --------------- */ + @CheckNoEvictions @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void refresh_null(LoadingCache cache, CacheContext context) { cache.refresh(null); } + @CheckNoEvictions @Test(dataProvider = "caches") - @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine) + @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE) public void refresh_dedupe(LoadingCache cache, CacheContext context) { var key = context.original().isEmpty() ? context.absentKey() : context.firstKey(); var future1 = cache.refresh(key); @@ -421,6 +454,7 @@ public void refresh_dedupe(LoadingCache cache, CacheContext context) { assertThat(cache).containsEntry(key, context.absentValue()); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.NULL, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) @@ -429,9 +463,12 @@ public void refresh_remove(LoadingCache cache, CacheContext context) { assertThat(future).succeedsWithNull(); assertThat(cache).hasSize(context.initialSize() - 1); assertThat(cache).doesNotContainKey(context.firstKey()); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.NULL, population = Population.EMPTY) @@ -442,6 +479,7 @@ public void refresh_ignored(LoadingCache cache, CacheContext context) assertThat(context).removalNotifications().isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -458,14 +496,14 @@ public void refresh_failure(LoadingCache cache, CacheContext context) assertThat(context).stats().success(0).failures(3); } - @CheckNoStats + @CheckNoEvictions @CheckNoStats @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.REFRESH_EXCEPTIONAL) public void refresh_throwsException(LoadingCache cache, CacheContext context) { cache.refresh(context.absentKey()); } - @CheckNoStats + @CheckNoEvictions @CheckNoStats @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.REFRESH_CHECKED_EXCEPTIONAL) public void refresh_throwsCheckedException(LoadingCache cache, CacheContext context) { @@ -477,6 +515,7 @@ public void refresh_throwsCheckedException(LoadingCache cache, CacheCo } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.REFRESH_INTERRUPTED) public void refresh_interrupted(LoadingCache cache, CacheContext context) { @@ -492,8 +531,9 @@ public void refresh_interrupted(LoadingCache cache, CacheContext conte } } + @CheckNoEvictions @Test(dataProvider = "caches") - @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine, + @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void refresh_cancel(LoadingCache cache, CacheContext context) { var key = context.original().isEmpty() ? context.absentKey() : context.firstKey(); @@ -508,14 +548,16 @@ public void refresh_cancel(LoadingCache cache, CacheContext context) { assertThat(cache).containsExactlyEntriesIn(context.original()); } - @CacheSpec(loader = Loader.NULL) + @CheckNoEvictions @Test(dataProvider = "caches") + @CacheSpec(loader = Loader.NULL) public void refresh_absent_null(LoadingCache cache, CacheContext context) { var future = cache.refresh(context.absentKey()); assertThat(future).succeedsWithNull(); assertThat(cache).hasSize(context.initialSize()); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec( maximumSize = Maximum.UNREACHABLE, @@ -529,13 +571,16 @@ public void refresh_absent(LoadingCache cache, CacheContext context) { assertThat(context).stats().hits(0).misses(0).success(1).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.NULL, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_null(LoadingCache cache, CacheContext context) { + var removed = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var future = cache.refresh(key); assertThat(future).succeedsWithNull(); + removed.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(0).failures(count); @@ -544,15 +589,19 @@ public void refresh_present_null(LoadingCache cache, CacheContext cont assertThat(cache).doesNotContainKey(key); } assertThat(cache).hasSize(context.initialSize() - count); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(removed).exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_sameValue(LoadingCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var future = cache.refresh(key); assertThat(future).succeedsWith(context.original().get(key)); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); @@ -561,38 +610,51 @@ public void refresh_present_sameValue(LoadingCache cache, CacheContext assertThat(cache).containsEntry(key, context.original().get(key)); } assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, - population = Population.EMPTY, loader = Loader.IDENTITY) + @CacheSpec(population = Population.EMPTY, loader = Loader.IDENTITY) public void refresh_present_sameInstance(LoadingCache cache, CacheContext context) { cache.put(context.absentKey(), context.absentKey()); var future = cache.refresh(context.absentKey()); assertThat(cache).hasSize(1); assertThat(future).succeedsWith(context.absentKey()); - assertThat(context).removalNotifications().isEmpty(); assertThat(context).stats().hits(0).misses(0).success(1).failures(0); assertThat(cache).containsEntry(context.absentKey(), context.absentKey()); + + if (context.isGuava()) { + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.absentKey(), context.absentKey()) + .exclusively(); + } else { + assertThat(context).removalNotifications().isEmpty(); + } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.IDENTITY, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_differentValue(LoadingCache cache, CacheContext context) { + var replaced = new HashMap(); for (Int key : context.firstMiddleLastKeys()) { var future = cache.refresh(key); assertThat(future).succeedsWith(key); assertThat(cache).containsEntry(key, key); + replaced.put(key, context.original().get(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache).hasSize(context.initialSize()); assertThat(context).stats().hits(0).misses(0).success(count).failures(0); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(replaced).exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) @@ -617,15 +679,16 @@ public void refresh_conflict(CacheContext context) { } else { assertThat(future).succeedsWith(refreshed); } - await().untilAsserted(() -> assertThat(context).removalNotifications().hasSize(2)); - assertThat(cache).containsEntry(key, updated); - assertThat(context).removalNotifications().containsExactlyValues(original, refreshed); + assertThat(cache).containsEntry(key, updated); assertThat(context).stats().success(1).failures(0); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(2).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(entry(key, original), entry(key, refreshed)) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) @@ -661,9 +724,12 @@ public void refresh_put(CacheContext context) { }); assertThat(context).stats().success(1).failures(0); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(2).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(entry(key, original), entry(key, refreshed)) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) @@ -694,11 +760,14 @@ public void refresh_invalidate(CacheContext context) { if (context.isGuava()) { await().untilAsserted(() -> assertThat(cache).containsEntry(key, refreshed)); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(key, original).exclusively(); } else { // linearizable await().untilAsserted(() -> assertThat(cache).doesNotContainKey(key)); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(2).exclusively(); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(entry(key, original), entry(key, refreshed)) + .exclusively(); } assertThat(context).stats().success(1).failures(0); } @@ -734,12 +803,14 @@ public void refresh_expired(CacheContext context) { if (context.isGuava()) { await().untilAsserted(() -> assertThat(cache).containsEntry(key, refreshed)); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(EXPIRED) + .contains(key, original).exclusively(); } else { // linearizable await().untilAsserted(() -> assertThat(cache).doesNotContainKey(key)); - assertThat(context).removalNotifications().withCause(EXPIRED).hasSize(1); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1); + assertThat(context).removalNotifications().withCause(EXPIRED).contains(key, original); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key, refreshed); + assertThat(context).removalNotifications().hasSize(2); } assertThat(context).stats().success(1).failures(0); } @@ -779,17 +850,20 @@ public void refresh_evicted(CacheContext context) { if (context.isGuava()) { await().untilAsserted(() -> assertThat(cache).containsEntry(key1, refreshed)); await().untilAsserted(() -> assertThat(cache).doesNotContainKey(key2)); - assertThat(context).removalNotifications().withCause(SIZE).hasSize(2).exclusively(); + assertThat(context).removalNotifications().withCause(SIZE) + .contains(Map.of(key1, original, key2, original)).exclusively(); } else { // linearizable await().untilAsserted(() -> assertThat(cache).doesNotContainKey(key1)); await().untilAsserted(() -> assertThat(cache).containsEntry(key2, original)); - assertThat(context).removalNotifications().withCause(SIZE).hasSize(1); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1); + assertThat(context).removalNotifications().withCause(SIZE).contains(key1, original); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key1, original); + assertThat(context).removalNotifications().hasSize(2); } assertThat(context).stats().success(1).failures(0); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY) public void refresh_cancel_noLog(CacheContext context) { @@ -813,6 +887,7 @@ public void refresh_cancel_noLog(CacheContext context) { assertThat(TestLoggerFactory.getLoggingEvents()).isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY) public void refresh_timeout_noLog(CacheContext context) { @@ -837,6 +912,7 @@ public void refresh_timeout_noLog(CacheContext context) { assertThat(TestLoggerFactory.getLoggingEvents()).isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY) public void refresh_error_log(CacheContext context) throws Exception { @@ -856,18 +932,21 @@ public void refresh_error_log(CacheContext context) throws Exception { /* --------------- refreshAll --------------- */ + @CheckNoEvictions @CheckNoStats @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void refreshAll_null(LoadingCache cache, CacheContext context) { cache.refreshAll(null); } + @CheckNoEvictions @CheckNoStats @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void refreshAll_nullKey(LoadingCache cache, CacheContext context) { cache.refreshAll(Collections.singletonList(null)); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void refreshAll_absent(LoadingCache cache, CacheContext context) { @@ -877,6 +956,7 @@ public void refreshAll_absent(LoadingCache cache, CacheContext context assertThat(cache).hasSize(context.initialSize() + count); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -889,6 +969,7 @@ public void refreshAll_present(LoadingCache cache, CacheContext contex assertThat(cache).containsExactlyEntriesIn(expected); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @@ -899,6 +980,7 @@ public void refreshAll_failure(LoadingCache cache, CacheContext contex assertThat(cache).hasSize(context.initialSize()); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(loader = Loader.REFRESH_INTERRUPTED) public void refreshAll_interrupted(LoadingCache cache, CacheContext context) { @@ -914,8 +996,9 @@ public void refreshAll_interrupted(LoadingCache cache, CacheContext co } } + @CheckNoEvictions @Test(dataProvider = "caches") - @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine, + @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void refreshAll_cancel(LoadingCache cache, CacheContext context) { var key = context.original().isEmpty() ? context.absentKey() : context.firstKey(); @@ -1021,7 +1104,7 @@ public void bulk_present() throws Exception { /* --------------- Policy: refreshes --------------- */ @Test(dataProvider = "caches") - @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine) + @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE) public void refreshes(LoadingCache cache, CacheContext context) { var key1 = Iterables.get(context.absentKeys(), 0); var key2 = context.original().isEmpty() diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ReferenceTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ReferenceTest.java index cb3819df42..16800ce8d2 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ReferenceTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ReferenceTest.java @@ -24,6 +24,8 @@ import static com.github.benmanes.caffeine.cache.testing.CacheSubject.assertThat; import static com.github.benmanes.caffeine.testing.FutureSubject.assertThat; import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.base.Predicates.not; import static com.google.common.truth.Truth.assertThat; import static java.util.Map.entry; import static java.util.function.Function.identity; @@ -34,8 +36,11 @@ import static org.mockito.Mockito.verify; import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.testng.Assert; @@ -134,13 +139,15 @@ public void getIfPresent(Cache cache, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void get(Cache cache, CacheContext context) { Int key = context.firstKey(); + var collected = getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); assertThat(cache.get(key, k -> context.absentValue())).isEqualTo(context.absentValue()); assertThat(cache).whenCleanedUp().hasSize(1); - long count = context.initialSize() - cache.estimatedSize() + 1; - assertThat(context).notifications().withCause(COLLECTED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -163,20 +170,18 @@ public void getAllPresent(Cache cache, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void getAll(Cache cache, CacheContext context) { var keys = context.firstMiddleLastKeys(); + var collected = getExpectedAfterGc(context, context.isStrongValues() + ? Maps.filterKeys(context.original(), not(keys::contains)) + : context.original()); context.clear(); GcFinalization.awaitFullGc(); assertThat(cache.getAll(keys, keysToLoad -> Maps.asMap(keysToLoad, Int::negate))) - .containsExactlyEntriesIn(Maps.toMap(keys, Int::negate)); - assertThat(cache).whenCleanedUp().hasSize(keys.size()); + .containsExactlyEntriesIn(Maps.asMap(keys, Int::negate)); - if (context.isStrongValues()) { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - keys.size()).exclusively(); - } else { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); - } + assertThat(cache).whenCleanedUp().hasSize(keys.size()); + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -185,19 +190,25 @@ public void getAll(Cache cache, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void put(Cache cache, CacheContext context) { - var firstKey = intern(context.firstKey()); + var collected = getExpectedAfterGc(context, context.isStrongValues() + ? Maps.filterKeys(context.original(), not(equalTo(context.firstKey()))) + : context.original()); + var key = intern(context.firstKey()); context.clear(); GcFinalization.awaitFullGc(); - cache.put(firstKey, context.absentValue()); + cache.put(key, context.absentValue()); assertThat(cache).whenCleanedUp().hasSize(1); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, key.negate()); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -208,6 +219,7 @@ public void put(Cache cache, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void putAll(Cache cache, CacheContext context) { + var collected = getExpectedAfterGc(context, context.original()); var entries = Map.of(context.firstKey(), context.absentValue(), context.middleKey(), context.absentValue(), context.absentKey(), context.absentValue()); context.clear(); @@ -217,7 +229,7 @@ public void putAll(Cache cache, CacheContext context) { assertThat(context.cache()).whenCleanedUp().hasSize(entries.size()); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -228,16 +240,22 @@ public void putAll(Cache cache, CacheContext context) { public void invalidate(Cache cache, CacheContext context) { Int key = context.firstKey(); Int value = cache.getIfPresent(key); - context.clear(); + var collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(equalTo(key)))); + context.clear(); GcFinalization.awaitFullGc(); assertThat(cache).whenCleanedUp().hasSize(1); cache.invalidate(key); assertThat(cache).isEmpty(); assertThat(value).isNotNull(); - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, EXPLICIT, 1)); + + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key, value); } @Test(dataProvider = "caches") @@ -246,20 +264,34 @@ public void invalidate(Cache cache, CacheContext context) { expireAfterAccess = Expire.DISABLED, expireAfterWrite = Expire.DISABLED, maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) - public void invalidateAll(Cache cache, CacheContext context) { + public void invalidateAll_iterable(Cache cache, CacheContext context) { + Map retained; + Entry[] collected; var keys = context.firstMiddleLastKeys(); - context.clear(); + if (context.isStrongValues()) { + retained = context.firstMiddleLastKeys().stream() + .collect(toMap(identity(), key -> context.original().get(key))); + collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(keys::contains))); + } else { + retained = Map.of(); + collected = getExpectedAfterGc(context, context.original()); + } + context.clear(); GcFinalization.awaitFullGc(); cache.invalidateAll(keys); assertThat(cache).whenCleanedUp().isEmpty(); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - keys.size(), EXPLICIT, keys.size())); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(retained); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -269,18 +301,32 @@ public void invalidateAll(Cache cache, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void invalidateAll_full(Cache cache, CacheContext context) { + Map retained; + Entry[] collected; var keys = context.firstMiddleLastKeys(); - context.clear(); + if (context.isStrongValues()) { + retained = context.firstMiddleLastKeys().stream() + .collect(toMap(identity(), key -> context.original().get(key))); + collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(keys::contains))); + } else { + retained = Map.of(); + collected = getExpectedAfterGc(context, context.original()); + } + context.clear(); GcFinalization.awaitFullGc(); cache.invalidateAll(); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - keys.size(), EXPLICIT, keys.size())); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(retained); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -290,12 +336,13 @@ public void invalidateAll_full(Cache cache, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void cleanUp(CacheContext context) { + var collected = getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); assertThat(context.cache()).whenCleanedUp().isEmpty(); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(COLLECTED).hasSize(count).exclusively(); + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } /* --------------- LoadingCache --------------- */ @@ -308,13 +355,16 @@ public void cleanUp(CacheContext context) { public void get_loading(LoadingCache cache, CacheContext context) { Int key = context.firstKey(); Int value = context.original().get(key); - context.clear(); + var collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(equalTo(key)))); + context.clear(); GcFinalization.awaitFullGc(); assertThat(cache).whenCleanedUp().hasSize(1); assertThat(cache.get(key)).isEqualTo(value); + assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - 1).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -325,19 +375,17 @@ public void get_loading(LoadingCache cache, CacheContext context) { loader = {Loader.NEGATIVE, Loader.BULK_NEGATIVE}) public void getAll_loading(LoadingCache cache, CacheContext context) { var keys = context.firstMiddleLastKeys(); - context.clear(); + var collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(keys::contains))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); - assertThat(cache.getAll(keys)).containsExactlyEntriesIn(Maps.toMap(keys, Int::negate)); + assertThat(cache.getAll(keys)).containsExactlyEntriesIn(Maps.asMap(keys, Int::negate)); assertThat(cache).whenCleanedUp().hasSize(keys.size()); - if (context.isStrongValues()) { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - keys.size()).exclusively(); - } else { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); - } + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -348,14 +396,20 @@ public void getAll_loading(LoadingCache cache, CacheContext context) { public void refresh(LoadingCache cache, CacheContext context) { Int key = context.firstKey(); Int value = context.original().get(key); - context.clear(); + var collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(equalTo(key)))); + context.clear(); GcFinalization.awaitFullGc(); assertThat(context.cache()).whenCleanedUp().hasSize(1); assertThat(cache.refresh(key)).succeedsWith(key); assertThat(cache).doesNotContainEntry(key, value); - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, value); + assertThat(context).removalNotifications().hasSize(context.initialSize()); } /* --------------- AsyncCache --------------- */ @@ -381,13 +435,14 @@ public void getIfPresent_async(AsyncCache cache, CacheContext context) maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void get_async(AsyncCache cache, CacheContext context) { - context.clear(); + var collected = getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); - assertThat(cache.get(context.absentKey(), identity())).succeedsWith(context.absentKey()); - - long count = context.initialSize(); - assertThat(context).notifications().withCause(COLLECTED).hasSize(count); + assertThat(cache.get(context.absentKey(), identity())) + .succeedsWith(context.absentKey()); + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -397,13 +452,15 @@ public void get_async(AsyncCache cache, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void getAll_async(AsyncCache cache, CacheContext context) { var keys = Set.of(context.firstKey(), context.lastKey(), context.absentKey()); - context.clear(); + var collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(keys::contains))); + context.clear(); GcFinalization.awaitFullGc(); assertThat(cache.getAll(keys, keysToLoad -> Maps.asMap(keysToLoad, Int::negate)).join()) .containsExactlyEntriesIn(Maps.asMap(keys, Int::negate)); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - 2).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -412,6 +469,7 @@ public void getAll_async(AsyncCache cache, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void put_async(AsyncCache cache, CacheContext context) { + var collected = getExpectedAfterGc(context, context.original()); Int key = context.absentKey(); context.clear(); GcFinalization.awaitFullGc(); @@ -419,7 +477,7 @@ public void put_async(AsyncCache cache, CacheContext context) { assertThat(cache).hasSize(1); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } /* --------------- Map --------------- */ @@ -486,13 +544,18 @@ public void containsValue(Map map, CacheContext context) { public void clear(Map map, CacheContext context) { var retained = context.firstMiddleLastKeys().stream() .collect(toMap(identity(), key -> context.original().get(key))); - context.clear(); + var collected = getExpectedAfterGc(context, Maps.difference( + context.original(), retained).entriesOnlyOnLeft()); + context.clear(); GcFinalization.awaitFullGc(); map.clear(); - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - retained.size(), EXPLICIT, retained.size())); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(retained); + assertThat(context).removalNotifications().hasSize(context.initialSize()); } @Test(dataProvider = "caches") @@ -502,8 +565,11 @@ public void clear(Map map, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void putIfAbsent(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Entry[] collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.putIfAbsent(key, context.absentValue()); if (context.isStrongValues()) { @@ -514,13 +580,8 @@ public void putIfAbsent(Map map, CacheContext context) { assertThat(context.cache()).whenCleanedUp().hasSize(1); assertThat(map).containsKey(key); - if (context.isStrongValues()) { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - 1).exclusively(); - } else { - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); - } + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -549,8 +610,12 @@ public void put_weighted(Cache> cache, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void put_map(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Int replaced = new Int(context.original().get(key)); + var collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.put(key, context.absentValue()); if (context.isStrongValues()) { @@ -563,11 +628,14 @@ public void put_map(Map map, CacheContext context) { assertThat(map).containsKey(key); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, replaced); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -610,8 +678,12 @@ public void replaceConditionally(Map map, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void remove(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Int removed = new Int(context.original().get(key)); + Entry[] collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.remove(key); if (context.isStrongValues()) { @@ -622,11 +694,14 @@ public void remove(Map map, CacheContext context) { assertThat(context.cache()).whenCleanedUp().isEmpty(); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, EXPLICIT, 1)); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key, removed); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -638,13 +713,19 @@ public void remove(Map map, CacheContext context) { public void removeConditionally_found(Map map, CacheContext context) { Int key = context.firstKey(); Int value = context.original().get(key); - context.clear(); + var collected = getExpectedAfterGc(context, + Maps.filterKeys(context.original(), not(equalTo(key)))); + context.clear(); GcFinalization.awaitFullGc(); assertThat(map.remove(key, value)).isTrue(); assertThat(context.cache()).whenCleanedUp().isEmpty(); - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, EXPLICIT, 1)); + + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key, value); } @Test(dataProvider = "caches") @@ -653,14 +734,15 @@ public void removeConditionally_found(Map map, CacheContext context) { maximumSize = Maximum.DISABLED, weigher = CacheWeigher.DEFAULT, stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void removeConditionally_notFound(Map map, CacheContext context) { + var collected = getExpectedAfterGc(context, context.original()); Int key = context.firstKey(); - context.clear(); + context.clear(); GcFinalization.awaitFullGc(); assertThat(map.remove(key, context.absentValue())).isFalse(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -670,20 +752,21 @@ public void removeConditionally_notFound(Map map, CacheContext context stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void computeIfAbsent(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + var collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.computeIfAbsent(key, k -> context.absentValue()); assertThat(context.cache()).whenCleanedUp().hasSize(1); if (context.isStrongValues()) { assertThat(value).isNotEqualTo(context.absentValue()); - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize() - 1).exclusively(); } else { assertThat(value).isEqualTo(context.absentValue()); - assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); } + assertThat(context).notifications().withCause(COLLECTED) + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -693,13 +776,14 @@ public void computeIfAbsent(Map map, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void computeIfAbsent_nullValue(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + var collected = getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); assertThat(map.computeIfAbsent(key, k -> null)).isNull(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -727,20 +811,28 @@ public void computeIfAbsent_weighted(Cache> cache, CacheContext c stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void computeIfPresent(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Int replaced = new Int(context.original().get(key)); + var collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.computeIfPresent(key, (k, v) -> context.absentValue()); if (context.isStrongValues()) { assertThat(value).isEqualTo(context.absentValue()); assertThat(context.cache()).whenCleanedUp().hasSize(1); - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, replaced); } else { assertThat(value).isNull(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -751,8 +843,12 @@ public void computeIfPresent(Map map, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void compute(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Int replaced = new Int(context.original().get(key)); + Entry[] collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.compute(key, (k, v) -> { if (context.isStrongValues()) { @@ -766,11 +862,14 @@ public void compute(Map map, CacheContext context) { assertThat(context.cache()).whenCleanedUp().hasSize(1); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, replaced); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -781,8 +880,9 @@ public void compute(Map map, CacheContext context) { stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void compute_nullValue(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + var collected = getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); assertThat(map.compute(key, (k, v) -> { assertThat(v).isNull(); @@ -790,7 +890,7 @@ public void compute_nullValue(Map map, CacheContext context) { })).isNull(); assertThat(context.cache()).whenCleanedUp().isEmpty(); assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } @Test(dataProvider = "caches") @@ -814,8 +914,12 @@ public void compute_weighted(Cache> cache, CacheContext context) stats = Stats.ENABLED, removalListener = Listener.CONSUMING) public void merge(Map map, CacheContext context) { Int key = context.firstKey(); - context.clear(); + Int replaced = new Int(context.original().get(key)); + var collected = context.isStrongValues() + ? getExpectedAfterGc(context, Maps.filterKeys(context.original(), not(equalTo(key)))) + : getExpectedAfterGc(context, context.original()); + context.clear(); GcFinalization.awaitFullGc(); Int value = map.merge(key, context.absentValue(), (oldValue, v) -> { if (context.isWeakKeys() && !context.isStrongValues()) { @@ -827,11 +931,14 @@ public void merge(Map map, CacheContext context) { assertThat(context.cache()).whenCleanedUp().hasSize(1); if (context.isStrongValues()) { - assertThat(context).notifications().hasRemovalCauses(Map.of( - COLLECTED, context.initialSize() - 1, REPLACED, 1)); + assertThat(context).evictionNotifications().withCause(COLLECTED) + .contains(collected).exclusively(); + assertThat(context).removalNotifications().hasSize(context.initialSize()); + assertThat(context).removalNotifications().withCause(COLLECTED).contains(collected); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, replaced); } else { assertThat(context).notifications().withCause(COLLECTED) - .hasSize(context.initialSize()).exclusively(); + .contains(collected).exclusively(); } } @@ -1070,4 +1177,14 @@ public Object[][] providesReferences() { new Object[] {new SoftValueReference<>(item, item, null), item, true, false}, }; } + + private Entry[] getExpectedAfterGc(CacheContext context, Map original) { + var expected = new ArrayList>(); + original.forEach((key, value) -> { + key = context.isStrongKeys() ? new Int(key) : null; + value = context.isStrongValues() ? new Int(value) : null; + expected.add(new SimpleEntry<>(key, value)); + }); + return expected.toArray(Map.Entry[]::new); + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java index 773e39f471..5bfc7da2dd 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java @@ -18,6 +18,7 @@ import static com.github.benmanes.caffeine.cache.RemovalCause.EXPLICIT; import static com.github.benmanes.caffeine.cache.RemovalCause.REPLACED; import static com.github.benmanes.caffeine.cache.testing.AsyncCacheSubject.assertThat; +import static com.github.benmanes.caffeine.cache.testing.CacheContext.intern; import static com.github.benmanes.caffeine.cache.testing.CacheContextSubject.assertThat; import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_ACCESS; import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_WRITE; @@ -36,6 +37,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.HashMap; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -60,12 +62,13 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Loader; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; +import com.github.benmanes.caffeine.cache.testing.CheckNoEvictions; import com.github.benmanes.caffeine.cache.testing.TrackingExecutor; import com.github.benmanes.caffeine.testing.ConcurrentTestHarness; import com.github.benmanes.caffeine.testing.Int; import com.github.valfirst.slf4jtest.TestLoggerFactory; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; /** * The test cases for caches that support the refresh after write policy. @@ -79,14 +82,15 @@ public final class RefreshAfterWriteTest { /* --------------- refreshIfNeeded --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED) public void refreshIfNeeded_nonblocking(CacheContext context) { Int key = context.absentKey(); - Int original = Int.valueOf(1); - Int refresh1 = original.add(1); - Int refresh2 = refresh1.add(1); + Int original = intern(Int.valueOf(1)); + Int refresh1 = intern(original.add(1)); + Int refresh2 = intern(refresh1.add(1)); var duration = Duration.ofMinutes(2); var refresh = new AtomicBoolean(); @@ -119,6 +123,7 @@ public CompletableFuture asyncReload(Int key, Int oldValue, Executor execut assertThat(cache.get(key)).isEqualTo(refresh2); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED) @@ -145,6 +150,7 @@ public CompletableFuture asyncReload(Int key, Int oldValue, Executor execut } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, loader = Loader.REFRESH_INTERRUPTED, refreshAfterWrite = Expire.ONE_MINUTE, @@ -155,6 +161,7 @@ public void refreshIfNeeded_interrupted(LoadingCache cache, CacheConte assertThat(Thread.interrupted()).isTrue(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, removalListener = Listener.CONSUMING) public void refreshIfNeeded_replace(LoadingCache cache, CacheContext context) { @@ -164,9 +171,11 @@ public void refreshIfNeeded_replace(LoadingCache cache, CacheContext c assertThat(cache).containsEntry(context.absentKey(), context.absentValue()); assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.absentKey(), context.absentKey()).exclusively(); + .contains(context.absentKey(), context.absentKey()) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE, removalListener = Listener.CONSUMING, loader = Loader.NULL) @@ -177,9 +186,11 @@ public void refreshIfNeeded_remove(LoadingCache cache, CacheContext co assertThat(cache).doesNotContainKey(context.absentKey()); assertThat(context).removalNotifications().withCause(EXPLICIT) - .contains(context.absentKey(), context.absentValue()).exclusively(); + .contains(context.absentKey(), context.absentValue()) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE, population = Population.EMPTY, @@ -200,6 +211,7 @@ public void refreshIfNeeded_noChange(CacheContext context) { assertThat(context).removalNotifications().isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE, removalListener = Listener.CONSUMING, @@ -215,12 +227,15 @@ public void refreshIfNeeded_discard(LoadingCache cache, CacheContext c executor.resume(); await().until(() -> executor.submitted() == executor.completed()); - assertThat(context).removalNotifications().withCause(REPLACED).contains( - entry(context.firstKey(), context.original().get(context.firstKey())), - entry(context.firstKey(), context.firstKey())).exclusively(); assertThat(cache).containsEntry(context.firstKey(), context.absentValue()); + + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(entry(context.firstKey(), context.original().get(context.firstKey())), + entry(context.firstKey(), context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE, removalListener = Listener.CONSUMING, @@ -236,11 +251,16 @@ public void refreshIfNeeded_absent_newValue(LoadingCache cache, CacheC executor.resume(); await().until(() -> executor.submitted() == executor.completed()); + assertThat(cache).doesNotContainKey(context.firstKey()); + assertThat(context).removalNotifications().withCause(REPLACED) .contains(context.firstKey(), context.firstKey()); - assertThat(cache).doesNotContainKey(context.firstKey()); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.firstKey(), context.firstKey()); + assertThat(context).removalNotifications().hasSize(2); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE, removalListener = Listener.CONSUMING, @@ -256,11 +276,14 @@ public void refreshIfNeeded_absent_nullValue(LoadingCache cache, Cache executor.resume(); await().until(() -> executor.submitted() == executor.completed()); - assertThat(context).removalNotifications().withCause(REPLACED) - .contains(context.firstKey(), context.original().get(context.firstKey())); assertThat(cache).doesNotContainKey(context.firstKey()); + + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE) @@ -288,6 +311,7 @@ public void refreshIfNeeded_cancel_noLog(CacheContext context) { assertThat(TestLoggerFactory.getLoggingEvents()).isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE) @@ -316,6 +340,7 @@ public void refreshIfNeeded_timeout_noLog(CacheContext context) { assertThat(TestLoggerFactory.getLoggingEvents()).isEmpty(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE) @@ -338,6 +363,7 @@ public void refreshIfNeeded_error_log(CacheContext context) { /* --------------- getIfPresent --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) @@ -348,9 +374,12 @@ public void getIfPresent_immediate(LoadingCache cache, CacheContext co assertThat(cache.getIfPresent(context.middleKey())).isEqualTo(context.middleKey()); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.middleKey(), context.original().get(context.middleKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.ASYNC_INCOMPLETE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) @@ -365,10 +394,13 @@ public void getIfPresent_delayed(LoadingCache cache, CacheContext cont if (context.isCaffeine()) { cache.policy().refreshes().get(context.middleKey()).complete(context.middleKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.middleKey(), context.original().get(context.middleKey())) + .exclusively(); } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NEGATIVE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) @@ -379,27 +411,31 @@ public void getIfPresent_async(AsyncLoadingCache cache, CacheContext c assertThat(cache.getIfPresent(context.middleKey())).succeedsWith(context.middleKey().negate()); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.middleKey(), context.original().get(context.middleKey())) + .exclusively(); } /* --------------- getAllPresent --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) public void getAllPresent_immediate(LoadingCache cache, CacheContext context) { - int count = context.firstMiddleLastKeys().size(); context.ticker().advance(30, TimeUnit.SECONDS); cache.getAllPresent(context.firstMiddleLastKeys()); context.ticker().advance(45, TimeUnit.SECONDS); - assertThat(cache.getAllPresent(context.firstMiddleLastKeys())).containsExactly( - context.firstKey(), context.firstKey(), context.middleKey(), context.middleKey(), - context.lastKey(), context.lastKey()); + assertThat(cache.getAllPresent(context.firstMiddleLastKeys())) + .containsExactlyEntriesIn(Maps.asMap(context.firstMiddleLastKeys(), key -> key)); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(count).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(Maps.filterKeys(context.original(), context.firstMiddleLastKeys()::contains)) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.ASYNC_INCOMPLETE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) @@ -411,16 +447,19 @@ public void getAllPresent_delayed(LoadingCache cache, CacheContext con .containsExactlyEntriesIn(expected); if (context.isCaffeine()) { + var replaced = new HashMap(); for (var key : context.firstMiddleLastKeys()) { cache.policy().refreshes().get(key).complete(key); + replaced.put(key, context.original().get(key)); } assertThat(context).removalNotifications().withCause(REPLACED) - .hasSize(expected.size()).exclusively(); + .contains(replaced).exclusively(); } } /* --------------- getFunc --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @@ -431,9 +470,12 @@ public void getFunc_immediate(LoadingCache cache, CacheContext context assertThat(cache.get(context.lastKey(), identity())).isEqualTo(context.lastKey()); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.lastKey(), context.original().get(context.lastKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.ASYNC_INCOMPLETE, population = { Population.PARTIAL, Population.FULL }) @@ -446,10 +488,13 @@ public void getFunc_delayed(LoadingCache cache, CacheContext context) if (context.isCaffeine()) { cache.policy().refreshes().get(context.lastKey()).complete(context.lastKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.lastKey(), context.original().get(context.lastKey())) + .exclusively(); } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) @@ -461,11 +506,14 @@ public void getFunc_async(AsyncLoadingCache cache, CacheContext contex cache.get(context.lastKey(), mappingFunction).join(); // refreshed assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.lastKey(), context.original().get(context.lastKey())) + .exclusively(); } /* --------------- get --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @@ -476,9 +524,12 @@ public void get_immediate(LoadingCache cache, CacheContext context) { assertThat(cache.get(context.firstKey())).isEqualTo(context.firstKey()); assertThat(cache).hasSize(context.initialSize()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.ASYNC_INCOMPLETE, population = { Population.PARTIAL, Population.FULL }) @@ -490,10 +541,13 @@ public void get_delayed(LoadingCache cache, CacheContext context) { if (context.isCaffeine()) { cache.policy().refreshes().get(context.firstKey()).complete(context.firstKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @@ -503,13 +557,16 @@ public void get_async(AsyncLoadingCache cache, CacheContext context) { cache.get(context.absentKey()).join(); context.ticker().advance(45, TimeUnit.SECONDS); - var oldValue = cache.getIfPresent(context.firstKey()); - assertThat(oldValue).succeedsWith(context.firstKey()); + var value = cache.getIfPresent(context.firstKey()); + assertThat(value).succeedsWith(context.firstKey()); assertThat(cache).containsEntry(context.firstKey(), context.firstKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, @@ -518,7 +575,7 @@ public void get_sameFuture(CacheContext context) { var done = new AtomicBoolean(); var cache = context.buildAsync((Int key) -> { await().untilTrue(done); - return key.negate(); + return intern(key.negate()); }); Int key = Int.valueOf(1); @@ -533,13 +590,14 @@ public void get_sameFuture(CacheContext context) { await().untilAsserted(() -> assertThat(cache).containsEntry(key, key.negate())); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED) public void get_slowRefresh(CacheContext context) { Int key = context.absentKey(); Int originalValue = context.absentValue(); - Int refreshedValue = originalValue.add(1); + Int refreshedValue = intern(originalValue.add(1)); var started = new AtomicBoolean(); var done = new AtomicBoolean(); var cache = context.build((Int k) -> { @@ -563,6 +621,7 @@ public void get_slowRefresh(CacheContext context) { await().untilAsserted(() -> assertThat(cache).containsEntry(key, refreshedValue)); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NULL) public void get_null(AsyncLoadingCache cache, CacheContext context) { @@ -576,6 +635,7 @@ public void get_null(AsyncLoadingCache cache, CacheContext context) { /* --------------- getAll --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @@ -590,18 +650,18 @@ public void getAll_immediate(LoadingCache cache, CacheContext context) context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getAll(keys)).containsExactly( context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.ASYNC_INCOMPLETE, population = { Population.PARTIAL, Population.FULL }) public void getAll_delayed(LoadingCache cache, CacheContext context) { var keys = context.firstMiddleLastKeys(); - var expected = ImmutableMap.of( - context.firstKey(), context.firstKey().negate(), - context.middleKey(), context.middleKey().negate(), - context.lastKey(), context.lastKey().negate()); + var expected = Maps.toMap(context.firstMiddleLastKeys(), Int::negate); context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getAll(keys)).containsExactlyEntriesIn(expected); @@ -614,10 +674,12 @@ public void getAll_delayed(LoadingCache cache, CacheContext context) { cache.policy().refreshes().get(key).complete(key); } assertThat(context).removalNotifications().withCause(REPLACED) - .hasSize(keys.size()).exclusively(); + .contains(Maps.filterKeys(context.original(), context.firstMiddleLastKeys()::contains)) + .exclusively(); } } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @@ -635,11 +697,14 @@ public void getAll_async(AsyncLoadingCache cache, CacheContext context // Ensure new values are present assertThat(cache.getAll(keys).join()).containsExactly( context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey()); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(1).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.firstKey(), context.original().get(context.firstKey())) + .exclusively(); } /* --------------- put --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) @@ -669,14 +734,15 @@ public void put(CacheContext context) { await().untilAsserted(() -> assertThat(context).removalNotifications().hasSize(2)); assertThat(cache).containsEntry(key, updated); - assertThat(context).removalNotifications().containsExactlyValues(original, refreshed); - assertThat(context).stats().success(1).failures(0); - assertThat(context).removalNotifications().withCause(REPLACED).hasSize(2).exclusively(); + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(entry(key, original), entry(key, refreshed)) + .exclusively(); } /* --------------- invalidate --------------- */ + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) @@ -709,15 +775,19 @@ public void invalidate(CacheContext context) { // Guava does not protect against ABA when the entry was removed by allowing a possibly // stale value from being inserted. assertThat(cache.getIfPresent(key)).isEqualTo(refreshed); + assertThat(context).removalNotifications().withCause(EXPLICIT) + .contains(key, original).exclusively(); } else { // Maintain linearizability by discarding the refresh if completing after an explicit removal assertThat(cache.getIfPresent(key)).isNull(); + assertThat(context).removalNotifications().hasSize(2); + assertThat(context).removalNotifications().withCause(REPLACED).contains(key, original); + assertThat(context).removalNotifications().withCause(EXPLICIT).contains(key, refreshed); } - assertThat(context).stats().success(1).failures(0); - assertThat(context).removalNotifications().withCause(EXPLICIT).hasSize(1); } + @CheckNoEvictions @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE, refreshAfterWrite = Expire.ONE_MINUTE) @@ -735,7 +805,7 @@ public void refresh(LoadingCache cache, CacheContext context) { // return in-flight future var future1 = cache.refresh(context.absentKey()); assertThat(executor.submitted()).isEqualTo(submitted + 1); - future1.complete(context.absentValue().negate()); + future1.complete(intern(context.absentValue().negate())); // trigger a new automatic refresh submitted = executor.submitted(); @@ -751,7 +821,7 @@ public void refresh(LoadingCache cache, CacheContext context) { /* --------------- Policy: refreshes --------------- */ @Test(dataProvider = "caches") - @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine, + @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.ASYNC_INCOMPLETE, refreshAfterWrite = Expire.ONE_MINUTE, population = Population.FULL) public void refreshes(LoadingCache cache, CacheContext context) { context.ticker().advance(2, TimeUnit.MINUTES); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContextSubject.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContextSubject.java index 060806a797..36dcd871af 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContextSubject.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContextSubject.java @@ -49,8 +49,8 @@ import com.github.benmanes.caffeine.cache.testing.RemovalListeners.ConsumingRemovalListener; import com.github.benmanes.caffeine.testing.Int; import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultiset; -import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import com.google.common.truth.FailureMetadata; import com.google.common.truth.StandardSubjectBuilder; @@ -307,21 +307,10 @@ public void containsExactlyValues(Object... values) { }); } - /** - * Fails if the number of notifications per removal cause do not have the given sizes. The - * expected, explicit removals are ignored when checking the eviction listener's notifications. - */ - public void hasRemovalCauses(Map expectedRemovals) { + public void hasNoEvictions() { awaitUntil((type, listener) -> { - var consumed = listener.removed().stream() - .map(RemovalNotification::getCause) - .collect(toImmutableMultiset()); - var expected = expectedRemovals.entrySet().stream() - .filter(entry -> entry.getKey().wasEvicted() - || (type == RemovalListenerType.REMOVAL_LISTENER)) - .collect(toImmutableMultiset(Entry::getKey, - entry -> Ints.checkedCast(entry.getValue().longValue()))); - check(type).that(consumed).isEqualTo(expected); + var stream = listener.removed().stream().filter(entry -> entry.getCause().wasEvicted()); + check(type).about(streams()).that(stream).isEmpty(); }); } @@ -351,27 +340,6 @@ private WithCause(RemovalCause cause) { this.cause = requireNonNull(cause); } - /** Fails if there are notifications with the given cause. */ - public void isEmpty() { - awaitUntil((type, listener) -> { - var notifications = listener.removed().stream() - .filter(notification -> cause == notification.getCause()); - check(type).withMessage("%s(%s)", cause, listener.removed()).about(streams()) - .that(notifications).isEmpty(); - }); - } - - /** Fails if the number of notifications of the given cause does not have the given size. */ - public Exclusive hasSize(long expectedSize) { - awaitUntil((type, listener) -> { - var notifications = listener.removed().stream() - .filter(notification -> cause == notification.getCause()); - check(type).withMessage("%s(%s)", cause, listener.removed()).about(streams()) - .that(notifications).hasSize(Ints.checkedCast(expectedSize)); - }); - return new Exclusive(expectedSize); - } - public Exclusive contains(Int key, Int value) { awaitUntil((type, listener) -> { check(type).withMessage("%s", cause) @@ -380,6 +348,10 @@ public Exclusive contains(Int key, Int value) { return new Exclusive(1); } + public Exclusive contains(Map map) { + return contains(map.entrySet().toArray(Map.Entry[]::new)); + } + public Exclusive contains(Entry... entries) { awaitUntil((type, listener) -> { var notifications = Stream.of(entries) @@ -388,7 +360,7 @@ public Exclusive contains(Entry... entries) { check(type).withMessage("%s", cause) .that(listener.removed()).containsAtLeastElementsIn(notifications); }); - return new Exclusive(ImmutableSet.copyOf(entries).size()); + return new Exclusive(ImmutableList.copyOf(entries).size()); } public final class Exclusive { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java index a9361edbe5..60ce393417 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java @@ -15,6 +15,7 @@ */ package com.github.benmanes.caffeine.cache.testing; +import static com.github.benmanes.caffeine.cache.testing.CacheContext.intern; import static com.github.benmanes.caffeine.testing.ConcurrentTestHarness.scheduledExecutor; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -431,14 +432,14 @@ enum Loader implements CacheLoader { IDENTITY { @Override public Int load(Int key) { requireNonNull(key); - return key; + return intern(key); } }, /** A loader that returns the key's negation. */ NEGATIVE { @Override public Int load(Int key) { // Intern the loader's return value so that it is retained on a refresh - return CacheContext.intern(key, k -> new Int(-k.intValue())); + return intern(key, k -> new Int(-k.intValue())); } }, /** A loader that always throws a runtime exception. */ @@ -478,6 +479,7 @@ enum Loader implements CacheLoader { var result = new HashMap(keys.size()); for (Int key : keys) { result.put(key, key); + intern(key); } return result; } @@ -490,6 +492,7 @@ enum Loader implements CacheLoader { var result = new HashMap(keys.size()); for (Int key : keys) { result.put(key, NEGATIVE.load(key)); + intern(key); } return result; } @@ -500,7 +503,7 @@ enum Loader implements CacheLoader { throw new UnsupportedOperationException(); } @Override public Map loadAll(Set keys) { - return keys.stream().collect(toMap(Int::negate, identity())); + return keys.stream().collect(toMap(key -> intern(intern(key).negate()), identity())); } }, /** A bulk-only loader that loads more than requested. */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java index e5a8474b72..8449501ddd 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java @@ -158,6 +158,7 @@ private void validate(ITestResult testResult) { } checkNoStats(testResult, context); checkExecutor(testResult, context); + checkNoEvictions(testResult, context); } } @@ -223,7 +224,8 @@ private static void checkExecutor(ITestResult testResult, CacheContext context) /** Checks the statistics if {@link CheckNoStats} is found. */ private static void checkNoStats(ITestResult testResult, CacheContext context) { var testMethod = testResult.getMethod().getConstructorOrMethod().getMethod(); - boolean checkNoStats = testMethod.isAnnotationPresent(CheckNoStats.class); + boolean checkNoStats = testMethod.isAnnotationPresent(CheckNoStats.class) + || testResult.getTestClass().getRealClass().isAnnotationPresent(CheckNoStats.class); if (!checkNoStats) { return; } @@ -232,6 +234,20 @@ private static void checkNoStats(ITestResult testResult, CacheContext context) { assertThat(context).stats().hits(0).misses(0).success(0).failures(0); } + /** Checks the statistics if {@link CheckNoEvictions} is found. */ + private static void checkNoEvictions(ITestResult testResult, CacheContext context) { + var testMethod = testResult.getMethod().getConstructorOrMethod().getMethod(); + boolean checkNoEvictions = testMethod.isAnnotationPresent(CheckNoEvictions.class) + || testResult.getTestClass().getRealClass().isAnnotationPresent(CheckNoEvictions.class); + if (!checkNoEvictions) { + return; + } + + assertWithMessage("Test requires CacheContext param for validation").that(context).isNotNull(); + assertThat(context).removalNotifications().hasNoEvictions(); + assertThat(context).evictionNotifications().isEmpty(); + } + /** Free memory by clearing unused resources after test execution. */ private void cleanUp(ITestResult testResult) { resultQueues.forEach(Collection::clear); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoEvictions.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoEvictions.java new file mode 100644 index 0000000000..038ee1ae79 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoEvictions.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.benmanes.caffeine.cache.testing; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A test class or method with this annotation has indicated that no eviction notifications should + * have been recorded and the {@link CacheValidationListener} should verify that expectation. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@Target({TYPE, METHOD}) @Retention(RUNTIME) +public @interface CheckNoEvictions {} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoStats.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoStats.java index 64447c3283..e1a79c4b62 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoStats.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoStats.java @@ -16,6 +16,7 @@ package com.github.benmanes.caffeine.cache.testing; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; @@ -27,5 +28,5 @@ * * @author ben.manes@gmail.com (Ben Manes) */ -@Target(METHOD) @Retention(RUNTIME) +@Target({TYPE, METHOD}) @Retention(RUNTIME) public @interface CheckNoStats {} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 54e67f0cb5..4539e13cdc 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -86,7 +86,7 @@ ext { ] pluginVersions = [ bnd: '6.2.0', - checkstyle: '10.1', + checkstyle: '10.2', coveralls: '2.12.0', dependencyCheck: '7.1.0.1', errorprone: '2.0.2',