From 613a5c8296da175400fdcb7592272d3107ea369b Mon Sep 17 00:00:00 2001 From: Yang Date: Wed, 29 Jan 2020 04:02:28 +1100 Subject: [PATCH] Add support for purging all store entries (#79) * Fix typos. * Gradle 6.1, AGP 4.0.0-alpha09, lifecycle 2.2.0. * Add experimental invalidateAll() support and @ExperimentalStoreAPI annotation. * Update sample with deleteAll function. * Update README.md with deleteAll function. * Add a section to README.md on clearing store entries. * Rewrite tests without mocking. Move test utils / helpers to a single package. * Code formatting and cleanups. * Use StoreResponse.Data instead of DataWithOrigin in ClearAllStoreTest and ClearStoreByKeyTest. * Simplified samples. Refactor tests. * Gradle 6.1.1. --- README.md | 59 +++++- .../java/com/dropbox/android/sample/Graph.kt | 70 ++++--- .../android/sample/data/model/DbModel.kt | 7 +- build.gradle | 2 +- buildsystem/dependencies.gradle | 2 +- cache/README.md | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- store/build.gradle | 3 + .../external/store4/ExperimentalStoreApi.kt | 10 + .../dropbox/android/external/store4/Store.kt | 16 +- .../android/external/store4/StoreBuilder.kt | 34 ++-- .../android/external/store4/impl/RealStore.kt | 7 + .../external/store4/impl/SourceOfTruth.kt | 20 +- .../store4/impl/SourceOfTruthWithBarrier.kt | 4 + .../external/store3/ClearStoreMemoryTest.kt | 63 ------- .../android/external/store3/ClearStoreTest.kt | 100 ---------- .../android/external/store3/Clearable.kt | 11 -- .../external/store3/ClearingPersister.kt | 18 -- .../external/store3/TestStoreBuilder.kt | 3 - .../store4/SourceOfTruthWithBarrierTest.kt | 6 +- .../external/store4/impl/ClearAllStoreTest.kt | 169 +++++++++++++++++ .../store4/impl/ClearStoreByKeyTest.kt | 171 ++++++++++++++++++ .../external/store4/impl/FlowStoreTest.kt | 28 +-- .../external/store4/impl/KeyTrackerTest.kt | 15 +- .../impl/StreamWithoutSourceOfTruthTest.kt | 2 + .../store4/{impl => testutil}/FakeFetcher.kt | 2 +- .../{impl => testutil}/FlowSubjectTest.kt | 2 +- .../store4/{impl => testutil}/FlowTestExt.kt | 2 +- .../store4/testutil/InMemoryPersister.kt | 30 +++ .../SimplePersisterAsFlowable.kt | 16 +- .../SimplePersisterAsFlowableTest.kt | 40 ++-- .../external/store4/testutil/TestStoreExt.kt | 20 ++ 32 files changed, 628 insertions(+), 310 deletions(-) create mode 100644 store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt delete mode 100644 store/src/test/java/com/dropbox/android/external/store3/ClearStoreMemoryTest.kt delete mode 100644 store/src/test/java/com/dropbox/android/external/store3/ClearStoreTest.kt delete mode 100644 store/src/test/java/com/dropbox/android/external/store3/Clearable.kt delete mode 100644 store/src/test/java/com/dropbox/android/external/store3/ClearingPersister.kt create mode 100644 store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt create mode 100644 store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt rename store/src/test/java/com/dropbox/android/external/store4/{impl => testutil}/FakeFetcher.kt (95%) rename store/src/test/java/com/dropbox/android/external/store4/{impl => testutil}/FlowSubjectTest.kt (98%) rename store/src/test/java/com/dropbox/android/external/store4/{impl => testutil}/FlowTestExt.kt (98%) create mode 100644 store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt rename store/src/test/java/com/dropbox/android/external/store4/{impl => testutil}/SimplePersisterAsFlowable.kt (89%) rename store/src/test/java/com/dropbox/android/external/store4/{impl => testutil}/SimplePersisterAsFlowableTest.kt (80%) create mode 100644 store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt diff --git a/README.md b/README.md index 00a870f..edadb5f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ StoreBuilder }.persister( reader = db.postDao()::loadPosts, writer = db.postDao()::insertPosts, - delete = db.postDao()::clearFeed + delete = db.postDao()::clearFeed, + deleteAll = db.postDao()::clearAllFeeds ).build() ``` @@ -219,7 +220,8 @@ StoreBuilder ).persister( reader = db.postDao()::loadPosts, writer = db.postDao()::insertPosts, - delete = db.postDao()::clearFeed + delete = db.postDao()::clearFeed, + deleteAll = db.postDao()::clearAllFeeds ).build() ``` @@ -229,3 +231,56 @@ StoreBuilder * `setExpireAfterTimeUnit(expireAfterTimeUnit: TimeUnit)` sets the time unit used when setting `expireAfterAccess` or `expireAfterWrite`. Default unit is `TimeUnit.SECONDS`. Note that `setExpireAfterAccess` and `setExpireAfterWrite` **cannot** both be set at the same time. + +### Clearing store entries + +You can delete a specific entry by key from a store, or clear all entries in a store. + +#### Store with no persister + +```kotlin +val store = StoreBuilder + .fromNonFlow { key: String -> + api.fetchData(key) + }.build() +``` + +The following will clear the entry associated with the key from the in-memory cache: + +```kotlin +store.clear("10") +``` + +The following will clear all entries from the in-memory cache: + +```kotlin +store.clearAll() +``` + +#### Store with persister + +When store has a persister (source of truth), you'll need to provide the `delete` and `deleteAll` functions for `clear(key)` and `clearAll()` to work: + +```kotlin +StoreBuilder + .fromNonFlow { key: String -> + api.fetchData(key) + }.persister( + reader = dao::loadData, + writer = dao::writeData, + delete = dao::clearDataByKey, + deleteAll = dao::clearAllData + ).build() +``` + +The following will clear the entry associated with the key from both the in-memory cache and the persister (source of truth): + +```kotlin +store.clear("10") +``` + +The following will clear all entries from both the in-memory cache and the persister (source of truth): + +```kotlin +store.clearAll() +``` \ No newline at end of file diff --git a/app/src/main/java/com/dropbox/android/sample/Graph.kt b/app/src/main/java/com/dropbox/android/sample/Graph.kt index 875aa2d..8ae8d75 100644 --- a/app/src/main/java/com/dropbox/android/sample/Graph.kt +++ b/app/src/main/java/com/dropbox/android/sample/Graph.kt @@ -38,23 +38,28 @@ object Graph { .fromNonFlow { key: String -> provideRetrofit().fetchSubreddit(key, 10).data.children.map(::toPosts) } - .persister(reader = db.postDao()::loadPosts, + .persister( + reader = db.postDao()::loadPosts, writer = db.postDao()::insertPosts, - delete = db.postDao()::clearFeed) + delete = db.postDao()::clearFeedBySubredditName, + deleteAll = db.postDao()::clearAllFeeds + ) .build() } fun provideRoomStoreMultiParam(context: SampleApp): Store, List> { val db = provideRoom(context) return StoreBuilder - .fromNonFlow, List> { (query, config) -> - provideRetrofit().fetchSubreddit(query, config.limit) - .data.children.map(::toPosts) - } - .persister(reader = { (query, _) -> db.postDao().loadPosts(query) }, - writer = { (query, _), posts -> db.postDao().insertPosts(query, posts) }, - delete = { (query, _) -> db.postDao().clearFeed(query) }) - .build() + .fromNonFlow, List> { (query, config) -> + provideRetrofit().fetchSubreddit(query, config.limit) + .data.children.map(::toPosts) + } + .persister(reader = { (query, _) -> db.postDao().loadPosts(query) }, + writer = { (query, _), posts -> db.postDao().insertPosts(query, posts) }, + delete = { (query, _) -> db.postDao().clearFeedBySubredditName(query) }, + deleteAll = db.postDao()::clearAllFeeds + ) + .build() } private fun provideRoom(context: SampleApp): RedditDb { @@ -72,29 +77,34 @@ object Graph { fun provideConfigStore(context: Context): Store { val fileSystem = FileSystemFactory.create(context.cacheDir) - val fileSystemPersister = FileSystemPersister.create(fileSystem, object : PathResolver { - override fun resolve(key: Unit) = "config.json" - }) + val fileSystemPersister = + FileSystemPersister.create(fileSystem, object : PathResolver { + override fun resolve(key: Unit) = "config.json" + }) val adapter = moshi.adapter(RedditConfig::class.java) return StoreBuilder - .fromNonFlow { - delay(500) - RedditConfig(10) + .fromNonFlow { + delay(500) + RedditConfig(10) + } + .nonFlowingPersister( + reader = { + runCatching { + val source = fileSystemPersister.read(Unit) + source?.let { adapter.fromJson(it) } + }.getOrNull() + }, + writer = { _, _ -> + val buffer = Buffer() + fileSystemPersister.write(Unit, buffer) } - .nonFlowingPersister( - reader = { - runCatching { - val source = fileSystemPersister.read(Unit) - source?.let { adapter.fromJson(it) } - }.getOrNull() - }, - writer = { _, _ -> - val buffer = Buffer() - fileSystemPersister.write(Unit, buffer) - } - ) - .cachePolicy(MemoryPolicy.builder().setExpireAfterWrite(10).setExpireAfterTimeUnit(TimeUnit.SECONDS).build()) - .build() + ) + .cachePolicy( + MemoryPolicy.builder().setExpireAfterWrite(10).setExpireAfterTimeUnit( + TimeUnit.SECONDS + ).build() + ) + .build() } private fun provideRetrofit(): Api { diff --git a/app/src/main/java/com/dropbox/android/sample/data/model/DbModel.kt b/app/src/main/java/com/dropbox/android/sample/data/model/DbModel.kt index 6c83ba3..406df3d 100644 --- a/app/src/main/java/com/dropbox/android/sample/data/model/DbModel.kt +++ b/app/src/main/java/com/dropbox/android/sample/data/model/DbModel.kt @@ -57,7 +57,7 @@ abstract class PostDao { @Transaction open suspend fun insertPosts(subredditName: String, posts: List) { // first clear the feed - clearFeed(subredditName) + clearFeedBySubredditName(subredditName) // convert them into database models val feedEntities = posts.mapIndexed { index: Int, post: Post -> FeedEntity( @@ -76,7 +76,10 @@ abstract class PostDao { } @Query("DELETE FROM FeedEntity WHERE subredditName = :subredditName") - abstract suspend fun clearFeed(subredditName: String) + abstract suspend fun clearFeedBySubredditName(subredditName: String) + + @Query("DELETE FROM FeedEntity") + abstract suspend fun clearAllFeeds() @Insert(onConflict = OnConflictStrategy.REPLACE) protected abstract suspend fun insertPosts( diff --git a/build.gradle b/build.gradle index 1a4bed5..5fc7059 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } ext.versions = [ - androidGradlePlugin : '4.0.0-alpha08', + androidGradlePlugin : '4.0.0-alpha09', dexcountGradlePlugin: '1.0.2', kotlin : '1.3.61', dokkaGradlePlugin : '0.10.0', diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 13fb69f..96c8fef 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -18,7 +18,7 @@ ext.versions = [ javax : '1', room : '2.2.2', coreKtx : '1.1.0', - lifecycle : '2.2.0-rc03', + lifecycle : '2.2.0', // Testing. junit : '4.13', diff --git a/cache/README.md b/cache/README.md index 27391bb..bd7395f 100644 --- a/cache/README.md +++ b/cache/README.md @@ -69,7 +69,7 @@ cache.get(1) // returns "bird" ### Cache Loader -**Cache** provides an API for get cached value by key and using the provided `loader: () -> Value` lambda to compute and cache the value automatically if none exists. +**Cache** provides an API for getting cached value by key and using the provided `loader: () -> Value` lambda to compute and cache the value automatically if none exists. ```kotlin val cache = Cache.Builder.newBuilder().build() @@ -156,7 +156,7 @@ cache.invalidateAll() ### Unit Testing Cache Expirations -To make it easier for testing logics that depend on cache expirations, `Cache.Builder` provides an API for setting a fake implementation of [Clock] for controlling (virtual) time in tests. +To make it easier for testing logics that depend on cache expirations, `Cache.Builder` provides an API for setting a fake implementation of `Clock` for controlling (virtual) time in tests. First define a custom `Clock` implementation: diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0472461..b9442c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-rc-2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/store/build.gradle b/store/build.gradle index f3e1a22..45dc44b 100644 --- a/store/build.gradle +++ b/store/build.gradle @@ -38,6 +38,9 @@ repositories { compileKotlin { kotlinOptions { jvmTarget = "1.8" + freeCompilerArgs += [ + '-Xuse-experimental=kotlin.Experimental', + ] } } compileTestKotlin { diff --git a/store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt b/store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt new file mode 100644 index 0000000..024be3c --- /dev/null +++ b/store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt @@ -0,0 +1,10 @@ +package com.dropbox.android.external.store4 + +/** + * Marks declarations that are still **experimental** in store API. + * Declarations marked with this annotation are unstable and subject to change. + */ +@MustBeDocumented +@Retention(value = AnnotationRetention.BINARY) +@Experimental(level = Experimental.Level.WARNING) +annotation class ExperimentalStoreApi diff --git a/store/src/main/java/com/dropbox/android/external/store4/Store.kt b/store/src/main/java/com/dropbox/android/external/store4/Store.kt index 01ce034..aacec01 100644 --- a/store/src/main/java/com/dropbox/android/external/store4/Store.kt +++ b/store/src/main/java/com/dropbox/android/external/store4/Store.kt @@ -21,14 +21,16 @@ import kotlinx.coroutines.flow.transform * } * .persister(reader = { (query, _) -> db.postDao().loadData(query) }, * writer = { (query, _), posts -> db.dataDAO().insertData(query, posts) }, - * delete = { (query, _) -> db.dataDAO().clearData(query) }) + * delete = { (query, _) -> db.dataDAO().clearData(query) }, + * deleteAll = db.postDao()::clearAllFeeds) * .build() - * //single shot response + * + * // single shot response * viewModelScope.launch { * val data = store.fresh(key) * } * - * //get cached data and collect future emissions as well + * // get cached data and collect future emissions as well * viewModelScope.launch { * val data = store.cached(key, refresh=true) * .collect{data.value=it } @@ -49,6 +51,14 @@ interface Store { * [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store]. */ suspend fun clear(key: Key) + + /** + * Purge all entries from memory and disk cache. + * Persistent storage will only be cleared if a deleteAll function was passed to + * [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store]. + */ + @ExperimentalStoreApi + suspend fun clearAll() } @ExperimentalCoroutinesApi diff --git a/store/src/main/java/com/dropbox/android/external/store4/StoreBuilder.kt b/store/src/main/java/com/dropbox/android/external/store4/StoreBuilder.kt index 3221cec..c868e07 100644 --- a/store/src/main/java/com/dropbox/android/external/store4/StoreBuilder.kt +++ b/store/src/main/java/com/dropbox/android/external/store4/StoreBuilder.kt @@ -55,15 +55,16 @@ interface StoreBuilder { fun disableCache(): StoreBuilder /** - * Connects a (non-[Flow]) source of truth that is accessible via [reader], [writer] and - * [delete]. + * Connects a (non-[Flow]) source of truth that is accessible via [reader], [writer], + * [delete], and [deleteAll]. * * @see persister */ fun nonFlowingPersister( reader: suspend (Key) -> NewOutput?, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? = null + delete: (suspend (Key) -> Unit)? = null, + deleteAll: (suspend () -> Unit)? = null ): StoreBuilder /** @@ -93,13 +94,15 @@ interface StoreBuilder { * @param reader reads records from the source of truth * @param writer writes records **coming in from the fetcher (network)** to the source of truth. * Writing local user updates to the source of truth via [Store] is currently not supported. - * @param delete deletes records in the source of truth + * @param delete deletes records in the source of truth for the give key + * @param deleteAll deletes all records in the source of truth * */ fun persister( reader: (Key) -> Flow, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? = null + delete: (suspend (Key) -> Unit)? = null, + deleteAll: (suspend () -> Unit)? = null ): StoreBuilder companion object { @@ -187,13 +190,15 @@ private class BuilderImpl( override fun nonFlowingPersister( reader: suspend (Key) -> NewOutput?, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? + delete: (suspend (Key) -> Unit)?, + deleteAll: (suspend () -> Unit)? ): BuilderWithSourceOfTruth { return withSourceOfTruth( PersistentNonFlowingSourceOfTruth( realReader = reader, realWriter = writer, - realDelete = delete + realDelete = delete, + realDeleteAll = deleteAll ) ) } @@ -205,7 +210,8 @@ private class BuilderImpl( PersistentNonFlowingSourceOfTruth( realReader = { key -> persister.read(key) }, realWriter = { key, input -> persister.write(key, input) }, - realDelete = { error("Delete is not implemented in legacy persisters") } + realDelete = { error("Delete is not implemented in legacy persisters") }, + realDeleteAll = { error("Delete all is not implemented in legacy persisters") } ) return withLegacySourceOfTruth(sourceOfTruth) } @@ -213,13 +219,15 @@ private class BuilderImpl( override fun persister( reader: (Key) -> Flow, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? + delete: (suspend (Key) -> Unit)?, + deleteAll: (suspend () -> Unit)? ): BuilderWithSourceOfTruth { return withSourceOfTruth( PersistentSourceOfTruth( realReader = reader, realWriter = writer, - realDelete = delete + realDelete = delete, + realDeleteAll = deleteAll ) ) } @@ -266,13 +274,15 @@ private class BuilderWithSourceOfTruth( override fun persister( reader: (Key) -> Flow, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? + delete: (suspend (Key) -> Unit)?, + deleteAll: (suspend () -> Unit)? ): StoreBuilder = error("Multiple persisters are not supported") override fun nonFlowingPersister( reader: suspend (Key) -> NewOutput?, writer: suspend (Key, Output) -> Unit, - delete: (suspend (Key) -> Unit)? + delete: (suspend (Key) -> Unit)?, + deleteAll: (suspend () -> Unit)? ): StoreBuilder = error("Multiple persisters are not supported") override fun nonFlowingPersisterLegacy(persister: Persister): StoreBuilder = diff --git a/store/src/main/java/com/dropbox/android/external/store4/impl/RealStore.kt b/store/src/main/java/com/dropbox/android/external/store4/impl/RealStore.kt index 331a610..d82bf77 100644 --- a/store/src/main/java/com/dropbox/android/external/store4/impl/RealStore.kt +++ b/store/src/main/java/com/dropbox/android/external/store4/impl/RealStore.kt @@ -17,6 +17,7 @@ package com.dropbox.android.external.store4.impl import com.dropbox.android.external.cache4.Cache import com.dropbox.android.external.store4.CacheType +import com.dropbox.android.external.store4.ExperimentalStoreApi import com.dropbox.android.external.store4.MemoryPolicy import com.dropbox.android.external.store4.ResponseOrigin import com.dropbox.android.external.store4.Store @@ -120,6 +121,12 @@ internal class RealStore( sourceOfTruth?.delete(key) } + @ExperimentalStoreApi + override suspend fun clearAll() { + memCache?.invalidateAll() + sourceOfTruth?.deleteAll() + } + /** * We want to stream from disk but also want to refresh. If requested or necessary. * diff --git a/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruth.kt b/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruth.kt index 565d460..f256740 100644 --- a/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruth.kt +++ b/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruth.kt @@ -30,6 +30,7 @@ internal interface SourceOfTruth { fun reader(key: Key): Flow suspend fun write(key: Key, value: Input) suspend fun delete(key: Key) + suspend fun deleteAll() // for testing suspend fun getSize(): Int } @@ -37,15 +38,23 @@ internal interface SourceOfTruth { internal class PersistentSourceOfTruth( private val realReader: (Key) -> Flow, private val realWriter: suspend (Key, Input) -> Unit, - private val realDelete: (suspend (Key) -> Unit)? = null + private val realDelete: (suspend (Key) -> Unit)? = null, + private val realDeleteAll: (suspend () -> Unit)? = null ) : SourceOfTruth { override val defaultOrigin = ResponseOrigin.Persister + override fun reader(key: Key): Flow = realReader(key) + override suspend fun write(key: Key, value: Input) = realWriter(key, value) + override suspend fun delete(key: Key) { realDelete?.invoke(key) } + override suspend fun deleteAll() { + realDeleteAll?.invoke() + } + // for testing override suspend fun getSize(): Int { throw UnsupportedOperationException("not supported for persistent") @@ -55,18 +64,25 @@ internal class PersistentSourceOfTruth( internal class PersistentNonFlowingSourceOfTruth( private val realReader: suspend (Key) -> Output?, private val realWriter: suspend (Key, Input) -> Unit, - private val realDelete: (suspend (Key) -> Unit)? = null + private val realDelete: (suspend (Key) -> Unit)? = null, + private val realDeleteAll: (suspend () -> Unit)? ) : SourceOfTruth { override val defaultOrigin = ResponseOrigin.Persister + override fun reader(key: Key): Flow = flow { emit(realReader(key)) } override suspend fun write(key: Key, value: Input) = realWriter(key, value) + override suspend fun delete(key: Key) { realDelete?.invoke(key) } + override suspend fun deleteAll() { + realDeleteAll?.invoke() + } + // for testing override suspend fun getSize(): Int { throw UnsupportedOperationException("not supported for persistent") diff --git a/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruthWithBarrier.kt b/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruthWithBarrier.kt index a102ed7..4e35fd8 100644 --- a/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruthWithBarrier.kt +++ b/store/src/main/java/com/dropbox/android/external/store4/impl/SourceOfTruthWithBarrier.kt @@ -103,6 +103,10 @@ internal class SourceOfTruthWithBarrier( delegate.delete(key) } + suspend fun deleteAll() { + delegate.deleteAll() + } + private sealed class BarrierMsg( val version: Long ) { diff --git a/store/src/test/java/com/dropbox/android/external/store3/ClearStoreMemoryTest.kt b/store/src/test/java/com/dropbox/android/external/store3/ClearStoreMemoryTest.kt deleted file mode 100644 index e605783..0000000 --- a/store/src/test/java/com/dropbox/android/external/store3/ClearStoreMemoryTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.dropbox.android.external.store3 - -import com.dropbox.android.external.store4.get -import com.dropbox.android.external.store4.legacy.BarCode -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@FlowPreview -@ExperimentalCoroutinesApi -@RunWith(Parameterized::class) -class ClearStoreMemoryTest( - storeType: TestStoreType -) { - private val testScope = TestCoroutineScope() - private var networkCalls = 0 - private val store = TestStoreBuilder.from(testScope) { - networkCalls++ - }.build(storeType) - - @Test - fun testClearSingleBarCode() = testScope.runBlockingTest { - // one request should produce one call - val barcode = BarCode("type", "key") - store.get(barcode) - assertThat(networkCalls).isEqualTo(1) - - // after clearing the memory another call should be made - store.clear(barcode) - store.get(barcode) - assertThat(networkCalls).isEqualTo(2) - } - - @Test - fun testClearAllBarCodes() = testScope.runBlockingTest { - val b1 = BarCode("type1", "key1") - val b2 = BarCode("type2", "key2") - - // each request should produce one call - store.get(b1) - store.get(b2) - assertThat(networkCalls).isEqualTo(2) - - store.clear(b1) - store.clear(b2) - - // after everything is cleared each request should produce another 2 calls - store.get(b1) - store.get(b2) - assertThat(networkCalls).isEqualTo(4) - } - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun params() = TestStoreType.values() - } -} diff --git a/store/src/test/java/com/dropbox/android/external/store3/ClearStoreTest.kt b/store/src/test/java/com/dropbox/android/external/store3/ClearStoreTest.kt deleted file mode 100644 index b7760cd..0000000 --- a/store/src/test/java/com/dropbox/android/external/store3/ClearStoreTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.dropbox.android.external.store3 - -import com.dropbox.android.external.store4.get -import com.dropbox.android.external.store4.legacy.BarCode -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.mockito.Mockito.verify -import java.util.concurrent.atomic.AtomicInteger - -@FlowPreview -@ExperimentalCoroutinesApi -@RunWith(Parameterized::class) -class ClearStoreTest( - storeType: TestStoreType -) { - private val testScope = TestCoroutineScope() - - private val persister: ClearingPersister = mock() - private val networkCalls = AtomicInteger(0) - - private val store = TestStoreBuilder.from( - scope = testScope, - fetcher = { - networkCalls.incrementAndGet() - }, - persister = persister - ).build(storeType) - - @Test - fun testClearSingleBarCode() = testScope.runBlockingTest { - // one request should produce one call - val barcode = BarCode("type", "key") - - whenever(persister.read(barcode)) - .thenReturn(null) // read from disk on get - .thenReturn(1) // read from disk after fetching from network - .thenReturn(null) // read from disk after clearing - .thenReturn(1) // read from disk after making additional network call - whenever(persister.write(barcode, 1)).thenReturn(true) - whenever(persister.write(barcode, 2)).thenReturn(true) - - store.get(barcode) - assertThat(networkCalls.toInt()).isEqualTo(1) - - // after clearing the memory another call should be made - store.clear(barcode) - store.get(barcode) - verify(persister).clear(barcode) - assertThat(networkCalls.toInt()).isEqualTo(2) - } - - @Test - fun testClearAllBarCodes() = testScope.runBlockingTest { - val barcode1 = BarCode("type1", "key1") - val barcode2 = BarCode("type2", "key2") - - whenever(persister.read(barcode1)) - .thenReturn(null) // read from disk - .thenReturn(1) // read from disk after fetching from network - .thenReturn(null) // read from disk after clearing disk cache - .thenReturn(1) // read from disk after making additional network call - whenever(persister.write(barcode1, 1)).thenReturn(true) - whenever(persister.write(barcode1, 2)).thenReturn(true) - - whenever(persister.read(barcode2)) - .thenReturn(null) // read from disk - .thenReturn(1) // read from disk after fetching from network - .thenReturn(null) // read from disk after clearing disk cache - .thenReturn(1) // read from disk after making additional network call - - whenever(persister.write(barcode2, 1)).thenReturn(true) - whenever(persister.write(barcode2, 2)).thenReturn(true) - - // each request should produce one call - store.get(barcode1) - store.get(barcode2) - assertThat(networkCalls.toInt()).isEqualTo(2) - - store.clear(barcode1) - store.clear(barcode2) - // after everything is cleared each request should produce another 2 calls - store.get(barcode1) - store.get(barcode2) - assertThat(networkCalls.toInt()).isEqualTo(4) - } - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun params() = TestStoreType.values() - } -} diff --git a/store/src/test/java/com/dropbox/android/external/store3/Clearable.kt b/store/src/test/java/com/dropbox/android/external/store3/Clearable.kt deleted file mode 100644 index 8718f9b..0000000 --- a/store/src/test/java/com/dropbox/android/external/store3/Clearable.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.dropbox.android.external.store3 - -/** - * Persisters should implement Clearable if they want store.clear(key) to also clear the persister - * - * @param Type of key/request param in store - */ -@Deprecated("") // used in tests -interface Clearable { - fun clear(key: T) -} diff --git a/store/src/test/java/com/dropbox/android/external/store3/ClearingPersister.kt b/store/src/test/java/com/dropbox/android/external/store3/ClearingPersister.kt deleted file mode 100644 index 5b99a2f..0000000 --- a/store/src/test/java/com/dropbox/android/external/store3/ClearingPersister.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.dropbox.android.external.store3 - -import com.dropbox.android.external.store4.Persister -import com.dropbox.android.external.store4.legacy.BarCode - -open class ClearingPersister : Persister, Clearable { - override suspend fun read(key: BarCode): Int? { - throw RuntimeException() - } - - override suspend fun write(key: BarCode, raw: Int): Boolean { - throw RuntimeException() - } - - override fun clear(key: BarCode) { - throw RuntimeException() - } -} diff --git a/store/src/test/java/com/dropbox/android/external/store3/TestStoreBuilder.kt b/store/src/test/java/com/dropbox/android/external/store3/TestStoreBuilder.kt index 041a2e0..3a24f3c 100644 --- a/store/src/test/java/com/dropbox/android/external/store3/TestStoreBuilder.kt +++ b/store/src/test/java/com/dropbox/android/external/store3/TestStoreBuilder.kt @@ -144,9 +144,6 @@ data class TestStoreBuilder( }, realWriter = { key, value -> persister.write(key, value) - }, - realDelete = { key -> - (persister as? Clearable)?.clear(key) } ) } diff --git a/store/src/test/java/com/dropbox/android/external/store4/SourceOfTruthWithBarrierTest.kt b/store/src/test/java/com/dropbox/android/external/store4/SourceOfTruthWithBarrierTest.kt index e44adb6..c8daf70 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/SourceOfTruthWithBarrierTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/SourceOfTruthWithBarrierTest.kt @@ -15,11 +15,12 @@ */ package com.dropbox.android.external.store4 -import com.dropbox.android.external.store4.impl.FlowStoreTest import com.dropbox.android.external.store4.impl.DataWithOrigin import com.dropbox.android.external.store4.impl.PersistentSourceOfTruth import com.dropbox.android.external.store4.impl.SourceOfTruth import com.dropbox.android.external.store4.impl.SourceOfTruthWithBarrier +import com.dropbox.android.external.store4.testutil.InMemoryPersister +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -29,14 +30,13 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest -import com.google.common.truth.Truth.assertThat import org.junit.Test @FlowPreview @ExperimentalCoroutinesApi class SourceOfTruthWithBarrierTest { private val testScope = TestCoroutineScope() - private val persister = FlowStoreTest.InMemoryPersister() + private val persister = InMemoryPersister() private val delegate: SourceOfTruth = PersistentSourceOfTruth( realReader = { key -> diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt b/store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt new file mode 100644 index 0000000..df10530 --- /dev/null +++ b/store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt @@ -0,0 +1,169 @@ +package com.dropbox.android.external.store4.impl + +import com.dropbox.android.external.store4.ExperimentalStoreApi +import com.dropbox.android.external.store4.ResponseOrigin +import com.dropbox.android.external.store4.StoreBuilder +import com.dropbox.android.external.store4.StoreResponse.Data +import com.dropbox.android.external.store4.testutil.InMemoryPersister +import com.dropbox.android.external.store4.testutil.getData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@FlowPreview +@ExperimentalCoroutinesApi +@ExperimentalStoreApi +@RunWith(JUnit4::class) +class ClearAllStoreTest { + + private val testScope = TestCoroutineScope() + + private val key1 = "key1" + private val key2 = "key2" + private val value1 = 1 + private val value2 = 2 + + private val fetcher: suspend (key: String) -> Int = { key: String -> + when (key) { + key1 -> value1 + key2 -> value2 + else -> throw IllegalStateException("Unknown key") + } + } + + private val persister = InMemoryPersister() + + @Test + fun `calling clearAll() on store with persister (no in-memory cache) deletes all entries from the persister`() = + testScope.runBlockingTest { + val store = StoreBuilder.fromNonFlow( + fetcher = fetcher + ).scope(testScope) + .disableCache() + .nonFlowingPersister( + reader = persister::read, + writer = persister::write, + deleteAll = persister::deleteAll + ) + .build() + + // should receive data from network first time + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value2 + ) + ) + + // should receive data from persister + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Persister, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Persister, + value = value2 + ) + ) + + // clear all entries in store + store.clearAll() + assertThat(persister.peekEntry(key1)) + .isNull() + assertThat(persister.peekEntry(key2)) + .isNull() + + // should fetch data from network again + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value2 + ) + ) + } + + @Test + fun `calling clearAll() on store with in-memory cache (no persister) deletes all entries from the in-memory cache`() = + testScope.runBlockingTest { + val store = StoreBuilder.fromNonFlow( + fetcher = fetcher + ).scope(testScope).build() + + // should receive data from network first time + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value2 + ) + ) + + // should receive data from cache + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Cache, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Cache, + value = value2 + ) + ) + + // clear all entries in store + store.clearAll() + + // should fetch data from network again + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value1 + ) + ) + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value2 + ) + ) + } +} diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt b/store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt new file mode 100644 index 0000000..3b21c14 --- /dev/null +++ b/store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt @@ -0,0 +1,171 @@ +package com.dropbox.android.external.store4.impl + +import com.dropbox.android.external.store4.ResponseOrigin +import com.dropbox.android.external.store4.StoreBuilder +import com.dropbox.android.external.store4.StoreResponse.Data +import com.dropbox.android.external.store4.testutil.InMemoryPersister +import com.dropbox.android.external.store4.testutil.getData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@FlowPreview +@ExperimentalCoroutinesApi +@RunWith(JUnit4::class) +class ClearStoreByKeyTest { + + private val testScope = TestCoroutineScope() + + private val persister = InMemoryPersister() + + @Test + fun `calling clear(key) on store with persister (no in-memory cache) deletes the entry associated with the key from the persister`() = + testScope.runBlockingTest { + val key = "key" + val value = 1 + val store = StoreBuilder.fromNonFlow( + fetcher = { value } + ).scope(testScope) + .disableCache() + .nonFlowingPersister( + reader = persister::read, + writer = persister::write, + delete = persister::deleteByKey + ) + .build() + + // should receive data from network first time + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value + ) + ) + + // should receive data from persister + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Persister, + value = value + ) + ) + + // clear store entry by key + store.clear(key) + assertThat(persister.peekEntry(key)) + .isNull() + + // should fetch data from network again + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value + ) + ) + } + + @Test + fun `calling clear(key) on store with in-memory cache (no persister) deletes the entry associated with the key from the in-memory cache`() = + testScope.runBlockingTest { + val key = "key" + val value = 1 + val store = StoreBuilder.fromNonFlow( + fetcher = { value } + ).scope(testScope).build() + + // should receive data from network first time + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value + ) + ) + + // should receive data from cache + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Cache, + value = value + ) + ) + + // clear store entry by key + store.clear(key) + + // should fetch data from network again + assertThat(store.getData(key)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value + ) + ) + } + + @Test + fun `calling clear(key) on store has no effect on existing entries associated with other keys in the in-memory cache or persister`() = + testScope.runBlockingTest { + val key1 = "key1" + val key2 = "key2" + val value1 = 1 + val value2 = 2 + val store = StoreBuilder.fromNonFlow( + fetcher = { key -> + when (key) { + key1 -> value1 + key2 -> value2 + else -> throw IllegalStateException("Unknown key") + } + } + ).scope(testScope) + .nonFlowingPersister( + reader = persister::read, + writer = persister::write, + delete = persister::deleteByKey + ) + .build() + + // get data for both keys + store.getData(key1) + store.getData(key2) + + // clear store entry for key1 + store.clear(key1) + + // entry for key1 is gone + assertThat(persister.peekEntry(key1)) + .isNull() + + // entry for key2 should still exists + assertThat(persister.peekEntry(key2)) + .isEqualTo(value2) + + // getting data for key1 should hit the network again + assertThat(store.getData(key1)) + .isEqualTo( + Data( + origin = ResponseOrigin.Fetcher, + value = value1 + ) + ) + + // getting data for key2 should not hit the network + assertThat(store.getData(key2)) + .isEqualTo( + Data( + origin = ResponseOrigin.Cache, + value = value2 + ) + ) + } +} diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowStoreTest.kt b/store/src/test/java/com/dropbox/android/external/store4/impl/FlowStoreTest.kt index 0cf4375..6489adf 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowStoreTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/impl/FlowStoreTest.kt @@ -25,6 +25,10 @@ import com.dropbox.android.external.store4.StoreResponse import com.dropbox.android.external.store4.StoreResponse.Data import com.dropbox.android.external.store4.StoreResponse.Loading import com.dropbox.android.external.store4.fresh +import com.dropbox.android.external.store4.testutil.FakeFetcher +import com.dropbox.android.external.store4.testutil.InMemoryPersister +import com.dropbox.android.external.store4.testutil.asFlowable +import com.dropbox.android.external.store4.testutil.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -42,7 +46,6 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import kotlin.collections.set @FlowPreview @ExperimentalCoroutinesApi @@ -327,7 +330,7 @@ class FlowStoreTest { @Test fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runBlockingTest { - val persister = InMemoryPersister().asObservable() + val persister = InMemoryPersister().asFlowable() val pipeline = build( flowingFetcher = { flow { @@ -356,7 +359,7 @@ class FlowStoreTest { @Test fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runBlockingTest { - val persister = InMemoryPersister().asObservable() + val persister = InMemoryPersister().asFlowable() val pipeline = build( flowingFetcher = { flow { @@ -403,7 +406,7 @@ class FlowStoreTest { @Test fun errorTest() = testScope.runBlockingTest { val exception = IllegalArgumentException("wow") - val persister = InMemoryPersister().asObservable() + val persister = InMemoryPersister().asFlowable() val pipeline = build( nonFlowingFetcher = { throw exception @@ -662,23 +665,6 @@ class FlowStoreTest { } } - class InMemoryPersister { - private val data = mutableMapOf() - - @Suppress("RedundantSuspendModifier") // for function reference - suspend fun read(key: Key) = data[key] - - @Suppress("RedundantSuspendModifier") // for function reference - suspend fun write(key: Key, output: Output) { - data[key] = output - } - - suspend fun asObservable() = SimplePersisterAsFlowable( - reader = this::read, - writer = this::write - ) - } - private fun build( nonFlowingFetcher: (suspend (Key) -> Input)? = null, flowingFetcher: ((Key) -> Flow)? = null, diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/KeyTrackerTest.kt b/store/src/test/java/com/dropbox/android/external/store4/impl/KeyTrackerTest.kt index c392809..c7c30f7 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/KeyTrackerTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/impl/KeyTrackerTest.kt @@ -15,6 +15,8 @@ */ package com.dropbox.android.external.store4.impl +import com.dropbox.android.external.store4.testutil.KeyTracker +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.cancelAndJoin @@ -23,7 +25,6 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest -import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -40,8 +41,8 @@ class KeyTrackerTest { fun dontSkipInvalidations() = scope1.runBlockingTest { val collection = scope2.async { subject.keyFlow('b') - .take(2) - .toList() + .take(2) + .toList() } scope2.advanceUntilIdle() assertThat(subject.activeKeyCount()).isEqualTo(1) @@ -60,8 +61,8 @@ class KeyTrackerTest { val collections = keys.associate { key -> key to scope2.async { subject.keyFlow(key) - .take(2) - .toList() + .take(2) + .toList() } } scope2.advanceUntilIdle() @@ -83,8 +84,8 @@ class KeyTrackerTest { val collections = (0..4).map { scope2.async { subject.keyFlow('b') - .take(2) - .toList() + .take(2) + .toList() } } scope2.advanceUntilIdle() diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/StreamWithoutSourceOfTruthTest.kt b/store/src/test/java/com/dropbox/android/external/store4/impl/StreamWithoutSourceOfTruthTest.kt index 37febf1..bfc8b0d 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/StreamWithoutSourceOfTruthTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/impl/StreamWithoutSourceOfTruthTest.kt @@ -19,6 +19,8 @@ import com.dropbox.android.external.store4.ResponseOrigin import com.dropbox.android.external.store4.StoreBuilder import com.dropbox.android.external.store4.StoreRequest import com.dropbox.android.external.store4.StoreResponse +import com.dropbox.android.external.store4.testutil.FakeFetcher +import com.dropbox.android.external.store4.testutil.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/FakeFetcher.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/FakeFetcher.kt similarity index 95% rename from store/src/test/java/com/dropbox/android/external/store4/impl/FakeFetcher.kt rename to store/src/test/java/com/dropbox/android/external/store4/testutil/FakeFetcher.kt index 571820d..cfe53fe 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/FakeFetcher.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/FakeFetcher.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.dropbox.android.external.store4.impl +package com.dropbox.android.external.store4.testutil import com.google.common.truth.Truth.assertThat diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowSubjectTest.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/FlowSubjectTest.kt similarity index 98% rename from store/src/test/java/com/dropbox/android/external/store4/impl/FlowSubjectTest.kt rename to store/src/test/java/com/dropbox/android/external/store4/testutil/FlowSubjectTest.kt index 0d66dd0..eed647d 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowSubjectTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/FlowSubjectTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.dropbox.android.external.store4.impl +package com.dropbox.android.external.store4.testutil import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowTestExt.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/FlowTestExt.kt similarity index 98% rename from store/src/test/java/com/dropbox/android/external/store4/impl/FlowTestExt.kt rename to store/src/test/java/com/dropbox/android/external/store4/testutil/FlowTestExt.kt index 99ca663..3d7208b 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/FlowTestExt.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/FlowTestExt.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.dropbox.android.external.store4.impl +package com.dropbox.android.external.store4.testutil import com.google.common.truth.FailureMetadata import com.google.common.truth.Subject diff --git a/store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt new file mode 100644 index 0000000..81267a0 --- /dev/null +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt @@ -0,0 +1,30 @@ +package com.dropbox.android.external.store4.testutil + +/** + * An in-memory non-flowing persister for testing. + */ +class InMemoryPersister { + private val data = mutableMapOf() + + @Suppress("RedundantSuspendModifier") // for function reference + suspend fun read(key: Key) = data[key] + + @Suppress("RedundantSuspendModifier") // for function reference + suspend fun write(key: Key, output: Output) { + data[key] = output + } + + @Suppress("RedundantSuspendModifier") // for function reference + suspend fun deleteByKey(key: Key) { + data.remove(key) + } + + @Suppress("RedundantSuspendModifier") // for function reference + suspend fun deleteAll() { + data.clear() + } + + fun peekEntry(key: Key): Output? { + return data[key] + } +} diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowable.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowable.kt similarity index 89% rename from store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowable.kt rename to store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowable.kt index 80b67b6..bc79ea5 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowable.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowable.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.dropbox.android.external.store4.impl +package com.dropbox.android.external.store4.testutil import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel @@ -89,10 +89,10 @@ internal class KeyTracker { val keyChannel = lock.withLock { channels.getOrPut(key) { KeyChannel( - channel = BroadcastChannel(Channel.CONFLATED).apply { - // start w/ an initial value. - offer(Unit) - } + channel = BroadcastChannel(Channel.CONFLATED).apply { + // start w/ an initial value. + offer(Unit) + } ) }.also { it.acquire() // refcount @@ -130,3 +130,9 @@ internal class KeyTracker { } } } + +@ExperimentalCoroutinesApi +suspend fun InMemoryPersister.asFlowable() = SimplePersisterAsFlowable( + reader = this::read, + writer = this::write +) diff --git a/store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowableTest.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowableTest.kt similarity index 80% rename from store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowableTest.kt rename to store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowableTest.kt index d95377e..56e3f4b 100644 --- a/store/src/test/java/com/dropbox/android/external/store4/impl/SimplePersisterAsFlowableTest.kt +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/SimplePersisterAsFlowableTest.kt @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.dropbox.android.external.store4.impl +package com.dropbox.android.external.store4.testutil import com.dropbox.android.external.store4.legacy.BarCode +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect @@ -26,7 +27,6 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest -import com.google.common.truth.Truth.assertThat import org.junit.Test @ExperimentalCoroutinesApi @@ -49,14 +49,14 @@ class SimplePersisterAsFlowableTest { val collectedValues = CompletableDeferred>() otherScope.launch { collectedValues.complete(flowable - .flowReader(barcode) - .onEach { - if (collectedFirst.isActive) { - collectedFirst.complete(Unit) - } + .flowReader(barcode) + .onEach { + if (collectedFirst.isActive) { + collectedFirst.complete(Unit) } - .take(2) - .toList()) + } + .take(2) + .toList()) } collectedFirst.await() flowable.flowWriter(barcode, "x") @@ -84,18 +84,18 @@ class SimplePersisterAsFlowableTest { var readIndex = 0 val written = mutableListOf() return SimplePersisterAsFlowable( - reader = { - if (readIndex >= values.size) { - throw AssertionError("should not've read this many") - } - values[readIndex++] - }, - writer = { _: BarCode, value: String -> - written.add(value) - }, - delete = { - TODO("not implemented") + reader = { + if (readIndex >= values.size) { + throw AssertionError("should not've read this many") } + values[readIndex++] + }, + writer = { _: BarCode, value: String -> + written.add(value) + }, + delete = { + TODO("not implemented") + } ) to written } } diff --git a/store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt b/store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt new file mode 100644 index 0000000..e747453 --- /dev/null +++ b/store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt @@ -0,0 +1,20 @@ +package com.dropbox.android.external.store4.testutil + +import com.dropbox.android.external.store4.Store +import com.dropbox.android.external.store4.StoreRequest +import com.dropbox.android.external.store4.StoreResponse +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first + +/** + * Helper factory that will return [StoreResponse.Data] for [key] + * if it is cached otherwise will return fresh/network data (updating your caches) + */ +suspend fun Store.getData(key: Key) = + stream( + StoreRequest.cached(key, refresh = false) + ).filterNot { + it is StoreResponse.Loading + }.first().let { + StoreResponse.Data(it.requireData(), it.origin) + }