Loading core/java/android/app/PropertyInvalidatedCache.java +23 −6 Original line number Diff line number Diff line Loading @@ -1294,6 +1294,13 @@ public class PropertyInvalidatedCache<Query, Result> { public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries, boolean mIsolateUids, boolean mTestMode, boolean mCacheNulls) { /** * Default values for fields. */ public static final int DEFAULT_MAX_ENTRIES = 32; public static final boolean DEFAULT_ISOLATE_UIDS = true; public static final boolean DEFAULT_CACHE_NULLS = false; // Validation: the module must be one of the known module strings and the maxEntries must // be positive. public Args { Loading @@ -1308,10 +1315,10 @@ public class PropertyInvalidatedCache<Query, Result> { public Args(@NonNull String module) { this(module, null, // api 32, // maxEntries true, // isolateUids DEFAULT_MAX_ENTRIES, DEFAULT_ISOLATE_UIDS, false, // testMode true // allowNulls DEFAULT_CACHE_NULLS ); } Loading Loading @@ -1361,7 +1368,7 @@ public class PropertyInvalidatedCache<Query, Result> { * Burst a property name into module and api. Throw if the key is invalid. This method is * used in to transition legacy cache constructors to the args constructor. */ private static Args parseProperty(@NonNull String name) { private static Args argsFromProperty(@NonNull String name) { throwIfInvalidCacheKey(name); // Strip off the leading well-known prefix. String base = name.substring(CACHE_KEY_PREFIX.length() + 1); Loading @@ -1384,8 +1391,9 @@ public class PropertyInvalidatedCache<Query, Result> { * * @hide */ @Deprecated public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { this(parseProperty(propertyName).maxEntries(maxEntries), propertyName, null); this(argsFromProperty(propertyName).maxEntries(maxEntries), propertyName, null); } /** Loading @@ -1399,9 +1407,10 @@ public class PropertyInvalidatedCache<Query, Result> { * @param cacheName Name of this cache in debug and dumpsys * @hide */ @Deprecated public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName, @NonNull String cacheName) { this(parseProperty(propertyName).maxEntries(maxEntries), cacheName, null); this(argsFromProperty(propertyName).maxEntries(maxEntries), cacheName, null); } /** Loading Loading @@ -1856,6 +1865,14 @@ public class PropertyInvalidatedCache<Query, Result> { invalidateCache(createPropertyName(module, api)); } /** * Invalidate caches in all processes that have the module and api specified in the args. * @hide */ public static void invalidateCache(@NonNull Args args) { invalidateCache(createPropertyName(args.mModule, args.mApi)); } /** * Invalidate PropertyInvalidatedCache caches in all processes that are keyed on * {@var name}. This function is synchronous: caches are invalidated upon return. Loading core/java/android/os/IpcDataCache.java +34 −32 Original line number Diff line number Diff line Loading @@ -400,10 +400,11 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, } /** * This is a convenience class that encapsulates configuration information for a * cache. It may be supplied to the cache constructors in lieu of the other * parameters. The class captures maximum entry count, the module, the key, and the * api. * This is a convenience class that encapsulates configuration information for a cache. It * may be supplied to the cache constructors in lieu of the other parameters. The class * captures maximum entry count, the module, the key, and the api. The key is used to * invalidate the cache and may be shared by different caches. The api is a user-visible (in * debug) name for the cache. * * There are three specific use cases supported by this class. * Loading @@ -430,11 +431,8 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * @hide */ public static class Config { private final int mMaxEntries; @IpcDataCacheModule private final String mModule; private final String mApi; private final String mName; final Args mArgs; final String mName; /** * The list of cache names that were created extending this Config. If Loading @@ -452,12 +450,20 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, */ private boolean mDisabled = false; /** * Fully construct a config. */ private Config(@NonNull Args args, @NonNull String name) { mArgs = args; mName = name; } /** * */ public Config(int maxEntries, @NonNull @IpcDataCacheModule String module, @NonNull String api, @NonNull String name) { mMaxEntries = maxEntries; mModule = module; mApi = api; mName = name; this(new Args(module).api(api).maxEntries(maxEntries), name); } /** Loading @@ -473,7 +479,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * the parameter list. */ public Config(@NonNull Config root, @NonNull String api, @NonNull String name) { this(root.maxEntries(), root.module(), api, name); this(root.mArgs.api(api), name); } /** Loading @@ -481,7 +487,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * the parameter list. */ public Config(@NonNull Config root, @NonNull String api) { this(root.maxEntries(), root.module(), api, api); this(root.mArgs.api(api), api); } /** Loading @@ -490,26 +496,23 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * current process. */ public Config child(@NonNull String name) { final Config result = new Config(this, api(), name); final Config result = new Config(mArgs, name); registerChild(name); return result; } public final int maxEntries() { return mMaxEntries; } @IpcDataCacheModule public final @NonNull String module() { return mModule; } public final @NonNull String api() { return mApi; /** * Set the cacheNull behavior. */ public Config cacheNulls(boolean enable) { return new Config(mArgs.cacheNulls(enable), mName); } public final @NonNull String name() { return mName; /** * Set the isolateUidss behavior. */ public Config isolateUids(boolean enable) { return new Config(mArgs.isolateUids(enable), mName); } /** Loading @@ -532,7 +535,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * Invalidate all caches that share this Config's module and api. */ public void invalidateCache() { IpcDataCache.invalidateCache(mModule, mApi); IpcDataCache.invalidateCache(mArgs); } /** Loading Loading @@ -564,8 +567,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * @hide */ public IpcDataCache(@NonNull Config config, @NonNull QueryHandler<Query, Result> computer) { super(new Args(config.module()).maxEntries(config.maxEntries()).api(config.api()), config.name(), computer); super(config.mArgs, config.mName, computer); } /** Loading core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +15 −0 Original line number Diff line number Diff line Loading @@ -740,5 +740,20 @@ public class PropertyInvalidatedCacheTests { assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); // Verify that the default is not to cache nulls. cache = new TestCache(new Args(MODULE_TEST) .maxEntries(4).api("testCachingNulls"), new TestQuery()); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); } } core/tests/coretests/src/android/os/IpcDataCacheTest.java +64 −18 Original line number Diff line number Diff line Loading @@ -16,13 +16,21 @@ package android.os; import static android.app.Flags.FLAG_PIC_CACHE_NULLS; import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import android.app.PropertyInvalidatedCache; import android.app.PropertyInvalidatedCache.Args; import android.multiuser.Flags; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.ravenwood.RavenwoodRule; import android.os.IpcDataCache; import androidx.test.filters.SmallTest; Loading @@ -43,6 +51,10 @@ import org.junit.Test; @SmallTest public class IpcDataCacheTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); // Configuration for creating caches private static final String MODULE = IpcDataCache.MODULE_TEST; private static final String API = "testApi"; Loading Loading @@ -287,8 +299,13 @@ public class IpcDataCacheTest { @Override public String apply(Integer qv) { mRecomputeCount += 1; // Special case for testing caches of nulls. Integers in the range 30-40 return null. if (qv >= 30 && qv < 40) { return null; } else { return "foo" + qv.toString(); } } int getRecomputeCount() { return mRecomputeCount; Loading Loading @@ -406,31 +423,16 @@ public class IpcDataCacheTest { } @Test public void testConfig() { public void testConfigDisable() { // Create a set of caches based on a set of chained configs. IpcDataCache.Config a = new IpcDataCache.Config(8, MODULE, "apiA"); TestCache ac = new TestCache(a); assertEquals(8, a.maxEntries()); assertEquals(MODULE, a.module()); assertEquals("apiA", a.api()); assertEquals("apiA", a.name()); IpcDataCache.Config b = new IpcDataCache.Config(a, "apiB"); TestCache bc = new TestCache(b); assertEquals(8, b.maxEntries()); assertEquals(MODULE, b.module()); assertEquals("apiB", b.api()); assertEquals("apiB", b.name()); IpcDataCache.Config c = new IpcDataCache.Config(a, "apiC", "nameC"); TestCache cc = new TestCache(c); assertEquals(8, c.maxEntries()); assertEquals(MODULE, c.module()); assertEquals("apiC", c.api()); assertEquals("nameC", c.name()); IpcDataCache.Config d = a.child("nameD"); TestCache dc = new TestCache(d); assertEquals(8, d.maxEntries()); assertEquals(MODULE, d.module()); assertEquals("apiA", d.api()); assertEquals("nameD", d.name()); a.disableForCurrentProcess(); assertEquals(ac.isDisabled(), true); Loading @@ -449,6 +451,7 @@ public class IpcDataCacheTest { assertEquals(ec.isDisabled(), true); } // Verify that invalidating the cache from an app process would fail due to lack of permissions. @Test @android.platform.test.annotations.DisabledOnRavenwood( Loading Loading @@ -507,4 +510,47 @@ public class IpcDataCacheTest { // Re-enable test mode (so that the cleanup for the test does not throw). IpcDataCache.setTestMode(true); } @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS) @Test public void testCachingNulls() { IpcDataCache.Config c = new IpcDataCache.Config(4, IpcDataCache.MODULE_TEST, "testCachingNulls"); TestCache cache; cache = new TestCache(c.cacheNulls(true)); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); cache = new TestCache(c.cacheNulls(false)); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); // Verify that the default is not to cache nulls. cache = new TestCache(c); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); } } Loading
core/java/android/app/PropertyInvalidatedCache.java +23 −6 Original line number Diff line number Diff line Loading @@ -1294,6 +1294,13 @@ public class PropertyInvalidatedCache<Query, Result> { public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries, boolean mIsolateUids, boolean mTestMode, boolean mCacheNulls) { /** * Default values for fields. */ public static final int DEFAULT_MAX_ENTRIES = 32; public static final boolean DEFAULT_ISOLATE_UIDS = true; public static final boolean DEFAULT_CACHE_NULLS = false; // Validation: the module must be one of the known module strings and the maxEntries must // be positive. public Args { Loading @@ -1308,10 +1315,10 @@ public class PropertyInvalidatedCache<Query, Result> { public Args(@NonNull String module) { this(module, null, // api 32, // maxEntries true, // isolateUids DEFAULT_MAX_ENTRIES, DEFAULT_ISOLATE_UIDS, false, // testMode true // allowNulls DEFAULT_CACHE_NULLS ); } Loading Loading @@ -1361,7 +1368,7 @@ public class PropertyInvalidatedCache<Query, Result> { * Burst a property name into module and api. Throw if the key is invalid. This method is * used in to transition legacy cache constructors to the args constructor. */ private static Args parseProperty(@NonNull String name) { private static Args argsFromProperty(@NonNull String name) { throwIfInvalidCacheKey(name); // Strip off the leading well-known prefix. String base = name.substring(CACHE_KEY_PREFIX.length() + 1); Loading @@ -1384,8 +1391,9 @@ public class PropertyInvalidatedCache<Query, Result> { * * @hide */ @Deprecated public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { this(parseProperty(propertyName).maxEntries(maxEntries), propertyName, null); this(argsFromProperty(propertyName).maxEntries(maxEntries), propertyName, null); } /** Loading @@ -1399,9 +1407,10 @@ public class PropertyInvalidatedCache<Query, Result> { * @param cacheName Name of this cache in debug and dumpsys * @hide */ @Deprecated public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName, @NonNull String cacheName) { this(parseProperty(propertyName).maxEntries(maxEntries), cacheName, null); this(argsFromProperty(propertyName).maxEntries(maxEntries), cacheName, null); } /** Loading Loading @@ -1856,6 +1865,14 @@ public class PropertyInvalidatedCache<Query, Result> { invalidateCache(createPropertyName(module, api)); } /** * Invalidate caches in all processes that have the module and api specified in the args. * @hide */ public static void invalidateCache(@NonNull Args args) { invalidateCache(createPropertyName(args.mModule, args.mApi)); } /** * Invalidate PropertyInvalidatedCache caches in all processes that are keyed on * {@var name}. This function is synchronous: caches are invalidated upon return. Loading
core/java/android/os/IpcDataCache.java +34 −32 Original line number Diff line number Diff line Loading @@ -400,10 +400,11 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, } /** * This is a convenience class that encapsulates configuration information for a * cache. It may be supplied to the cache constructors in lieu of the other * parameters. The class captures maximum entry count, the module, the key, and the * api. * This is a convenience class that encapsulates configuration information for a cache. It * may be supplied to the cache constructors in lieu of the other parameters. The class * captures maximum entry count, the module, the key, and the api. The key is used to * invalidate the cache and may be shared by different caches. The api is a user-visible (in * debug) name for the cache. * * There are three specific use cases supported by this class. * Loading @@ -430,11 +431,8 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * @hide */ public static class Config { private final int mMaxEntries; @IpcDataCacheModule private final String mModule; private final String mApi; private final String mName; final Args mArgs; final String mName; /** * The list of cache names that were created extending this Config. If Loading @@ -452,12 +450,20 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, */ private boolean mDisabled = false; /** * Fully construct a config. */ private Config(@NonNull Args args, @NonNull String name) { mArgs = args; mName = name; } /** * */ public Config(int maxEntries, @NonNull @IpcDataCacheModule String module, @NonNull String api, @NonNull String name) { mMaxEntries = maxEntries; mModule = module; mApi = api; mName = name; this(new Args(module).api(api).maxEntries(maxEntries), name); } /** Loading @@ -473,7 +479,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * the parameter list. */ public Config(@NonNull Config root, @NonNull String api, @NonNull String name) { this(root.maxEntries(), root.module(), api, name); this(root.mArgs.api(api), name); } /** Loading @@ -481,7 +487,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * the parameter list. */ public Config(@NonNull Config root, @NonNull String api) { this(root.maxEntries(), root.module(), api, api); this(root.mArgs.api(api), api); } /** Loading @@ -490,26 +496,23 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * current process. */ public Config child(@NonNull String name) { final Config result = new Config(this, api(), name); final Config result = new Config(mArgs, name); registerChild(name); return result; } public final int maxEntries() { return mMaxEntries; } @IpcDataCacheModule public final @NonNull String module() { return mModule; } public final @NonNull String api() { return mApi; /** * Set the cacheNull behavior. */ public Config cacheNulls(boolean enable) { return new Config(mArgs.cacheNulls(enable), mName); } public final @NonNull String name() { return mName; /** * Set the isolateUidss behavior. */ public Config isolateUids(boolean enable) { return new Config(mArgs.isolateUids(enable), mName); } /** Loading @@ -532,7 +535,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * Invalidate all caches that share this Config's module and api. */ public void invalidateCache() { IpcDataCache.invalidateCache(mModule, mApi); IpcDataCache.invalidateCache(mArgs); } /** Loading Loading @@ -564,8 +567,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * @hide */ public IpcDataCache(@NonNull Config config, @NonNull QueryHandler<Query, Result> computer) { super(new Args(config.module()).maxEntries(config.maxEntries()).api(config.api()), config.name(), computer); super(config.mArgs, config.mName, computer); } /** Loading
core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +15 −0 Original line number Diff line number Diff line Loading @@ -740,5 +740,20 @@ public class PropertyInvalidatedCacheTests { assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); // Verify that the default is not to cache nulls. cache = new TestCache(new Args(MODULE_TEST) .maxEntries(4).api("testCachingNulls"), new TestQuery()); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); } }
core/tests/coretests/src/android/os/IpcDataCacheTest.java +64 −18 Original line number Diff line number Diff line Loading @@ -16,13 +16,21 @@ package android.os; import static android.app.Flags.FLAG_PIC_CACHE_NULLS; import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import android.app.PropertyInvalidatedCache; import android.app.PropertyInvalidatedCache.Args; import android.multiuser.Flags; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.ravenwood.RavenwoodRule; import android.os.IpcDataCache; import androidx.test.filters.SmallTest; Loading @@ -43,6 +51,10 @@ import org.junit.Test; @SmallTest public class IpcDataCacheTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); // Configuration for creating caches private static final String MODULE = IpcDataCache.MODULE_TEST; private static final String API = "testApi"; Loading Loading @@ -287,8 +299,13 @@ public class IpcDataCacheTest { @Override public String apply(Integer qv) { mRecomputeCount += 1; // Special case for testing caches of nulls. Integers in the range 30-40 return null. if (qv >= 30 && qv < 40) { return null; } else { return "foo" + qv.toString(); } } int getRecomputeCount() { return mRecomputeCount; Loading Loading @@ -406,31 +423,16 @@ public class IpcDataCacheTest { } @Test public void testConfig() { public void testConfigDisable() { // Create a set of caches based on a set of chained configs. IpcDataCache.Config a = new IpcDataCache.Config(8, MODULE, "apiA"); TestCache ac = new TestCache(a); assertEquals(8, a.maxEntries()); assertEquals(MODULE, a.module()); assertEquals("apiA", a.api()); assertEquals("apiA", a.name()); IpcDataCache.Config b = new IpcDataCache.Config(a, "apiB"); TestCache bc = new TestCache(b); assertEquals(8, b.maxEntries()); assertEquals(MODULE, b.module()); assertEquals("apiB", b.api()); assertEquals("apiB", b.name()); IpcDataCache.Config c = new IpcDataCache.Config(a, "apiC", "nameC"); TestCache cc = new TestCache(c); assertEquals(8, c.maxEntries()); assertEquals(MODULE, c.module()); assertEquals("apiC", c.api()); assertEquals("nameC", c.name()); IpcDataCache.Config d = a.child("nameD"); TestCache dc = new TestCache(d); assertEquals(8, d.maxEntries()); assertEquals(MODULE, d.module()); assertEquals("apiA", d.api()); assertEquals("nameD", d.name()); a.disableForCurrentProcess(); assertEquals(ac.isDisabled(), true); Loading @@ -449,6 +451,7 @@ public class IpcDataCacheTest { assertEquals(ec.isDisabled(), true); } // Verify that invalidating the cache from an app process would fail due to lack of permissions. @Test @android.platform.test.annotations.DisabledOnRavenwood( Loading Loading @@ -507,4 +510,47 @@ public class IpcDataCacheTest { // Re-enable test mode (so that the cleanup for the test does not throw). IpcDataCache.setTestMode(true); } @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS) @Test public void testCachingNulls() { IpcDataCache.Config c = new IpcDataCache.Config(4, IpcDataCache.MODULE_TEST, "testCachingNulls"); TestCache cache; cache = new TestCache(c.cacheNulls(true)); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); cache = new TestCache(c.cacheNulls(false)); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); // Verify that the default is not to cache nulls. cache = new TestCache(c); cache.invalidateCache(); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); assertEquals(3, cache.getRecomputeCount()); assertEquals("foo1", cache.query(1)); assertEquals("foo2", cache.query(2)); assertEquals(null, cache.query(30)); // The recompute is 4 because nulls were not cached. assertEquals(4, cache.getRecomputeCount()); } }