From 970c1786f7862675870cb3f410e6a1c827bdd637 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sun, 24 Apr 2022 17:06:22 -0700 Subject: [PATCH] Use explicit checks in tests for removal notifications Previously the number of notifications were checked for, but this allowed for an incorrect publication to sneak through. The tests now assert that the exact number of expected notifications are observed. --- .github/workflows/build.yml | 2 + .../benmanes/caffeine/cache/AsMapTest.java | 210 ++++++------ .../caffeine/cache/AsyncAsMapTest.java | 156 +++++---- .../caffeine/cache/AsyncCacheTest.java | 15 +- .../caffeine/cache/AsyncLoadingCacheTest.java | 9 +- .../caffeine/cache/BoundedLocalCacheTest.java | 46 ++- .../benmanes/caffeine/cache/CacheTest.java | 65 ++-- .../benmanes/caffeine/cache/EvictionTest.java | 27 +- .../caffeine/cache/ExpirationTest.java | 164 +++++---- .../caffeine/cache/ExpireAfterAccessTest.java | 50 ++- .../caffeine/cache/ExpireAfterVarTest.java | 103 ++++-- .../caffeine/cache/ExpireAfterWriteTest.java | 39 +-- .../caffeine/cache/LoadingCacheTest.java | 139 ++++++-- .../caffeine/cache/ReferenceTest.java | 315 ++++++++++++------ .../caffeine/cache/RefreshAfterWriteTest.java | 160 ++++++--- .../cache/testing/CacheContextSubject.java | 46 +-- .../caffeine/cache/testing/CacheSpec.java | 9 +- .../testing/CacheValidationListener.java | 18 +- .../cache/testing/CheckNoEvictions.java | 32 ++ .../caffeine/cache/testing/CheckNoStats.java | 3 +- gradle/dependencies.gradle | 2 +- 21 files changed, 1020 insertions(+), 590 deletions(-) create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CheckNoEvictions.java 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',