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.
This commit is contained in:
parent
86f7b1b060
commit
613a5c8296
32 changed files with 628 additions and 310 deletions
59
README.md
59
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<String, Int> { 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<String, Int> { 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()
|
||||
```
|
|
@ -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<Pair<String, RedditConfig>, List<Post>> {
|
||||
val db = provideRoom(context)
|
||||
return StoreBuilder
|
||||
.fromNonFlow<Pair<String, RedditConfig>, List<Post>> { (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<Pair<String, RedditConfig>, List<Post>> { (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<Unit, RedditConfig> {
|
||||
val fileSystem = FileSystemFactory.create(context.cacheDir)
|
||||
val fileSystemPersister = FileSystemPersister.create(fileSystem, object : PathResolver<Unit> {
|
||||
override fun resolve(key: Unit) = "config.json"
|
||||
})
|
||||
val fileSystemPersister =
|
||||
FileSystemPersister.create(fileSystem, object : PathResolver<Unit> {
|
||||
override fun resolve(key: Unit) = "config.json"
|
||||
})
|
||||
val adapter = moshi.adapter<RedditConfig>(RedditConfig::class.java)
|
||||
return StoreBuilder
|
||||
.fromNonFlow<Unit, RedditConfig> {
|
||||
delay(500)
|
||||
RedditConfig(10)
|
||||
.fromNonFlow<Unit, RedditConfig> {
|
||||
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 {
|
||||
|
|
|
@ -57,7 +57,7 @@ abstract class PostDao {
|
|||
@Transaction
|
||||
open suspend fun insertPosts(subredditName: String, posts: List<Post>) {
|
||||
// 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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
4
cache/README.md
vendored
4
cache/README.md
vendored
|
@ -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<Long, User>()
|
||||
|
@ -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:
|
||||
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
|
@ -38,6 +38,9 @@ repositories {
|
|||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += [
|
||||
'-Xuse-experimental=kotlin.Experimental',
|
||||
]
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
|
|
10
store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt
vendored
Normal file
10
store/src/main/java/com/dropbox/android/external/store4/ExperimentalStoreApi.kt
vendored
Normal file
|
@ -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
|
|
@ -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<Key : Any, Output : Any> {
|
|||
* [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
|
||||
|
|
|
@ -55,15 +55,16 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
|||
fun disableCache(): StoreBuilder<Key, Output>
|
||||
|
||||
/**
|
||||
* 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 <NewOutput : Any> 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<Key, NewOutput>
|
||||
|
||||
/**
|
||||
|
@ -93,13 +94,15 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
|||
* @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 <NewOutput : Any> persister(
|
||||
reader: (Key) -> Flow<NewOutput?>,
|
||||
writer: suspend (Key, Output) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)? = null
|
||||
delete: (suspend (Key) -> Unit)? = null,
|
||||
deleteAll: (suspend () -> Unit)? = null
|
||||
): StoreBuilder<Key, NewOutput>
|
||||
|
||||
companion object {
|
||||
|
@ -187,13 +190,15 @@ private class BuilderImpl<Key : Any, Output : Any>(
|
|||
override fun <NewOutput : Any> nonFlowingPersister(
|
||||
reader: suspend (Key) -> NewOutput?,
|
||||
writer: suspend (Key, Output) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)?
|
||||
delete: (suspend (Key) -> Unit)?,
|
||||
deleteAll: (suspend () -> Unit)?
|
||||
): BuilderWithSourceOfTruth<Key, Output, NewOutput> {
|
||||
return withSourceOfTruth(
|
||||
PersistentNonFlowingSourceOfTruth(
|
||||
realReader = reader,
|
||||
realWriter = writer,
|
||||
realDelete = delete
|
||||
realDelete = delete,
|
||||
realDeleteAll = deleteAll
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -205,7 +210,8 @@ private class BuilderImpl<Key : Any, Output : Any>(
|
|||
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<Key : Any, Output : Any>(
|
|||
override fun <NewOutput : Any> persister(
|
||||
reader: (Key) -> Flow<NewOutput?>,
|
||||
writer: suspend (Key, Output) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)?
|
||||
delete: (suspend (Key) -> Unit)?,
|
||||
deleteAll: (suspend () -> Unit)?
|
||||
): BuilderWithSourceOfTruth<Key, Output, NewOutput> {
|
||||
return withSourceOfTruth(
|
||||
PersistentSourceOfTruth(
|
||||
realReader = reader,
|
||||
realWriter = writer,
|
||||
realDelete = delete
|
||||
realDelete = delete,
|
||||
realDeleteAll = deleteAll
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -266,13 +274,15 @@ private class BuilderWithSourceOfTruth<Key : Any, Input : Any, Output : Any>(
|
|||
override fun <NewOutput : Any> persister(
|
||||
reader: (Key) -> Flow<NewOutput?>,
|
||||
writer: suspend (Key, Output) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)?
|
||||
delete: (suspend (Key) -> Unit)?,
|
||||
deleteAll: (suspend () -> Unit)?
|
||||
): StoreBuilder<Key, NewOutput> = error("Multiple persisters are not supported")
|
||||
|
||||
override fun <NewOutput : Any> nonFlowingPersister(
|
||||
reader: suspend (Key) -> NewOutput?,
|
||||
writer: suspend (Key, Output) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)?
|
||||
delete: (suspend (Key) -> Unit)?,
|
||||
deleteAll: (suspend () -> Unit)?
|
||||
): StoreBuilder<Key, NewOutput> = error("Multiple persisters are not supported")
|
||||
|
||||
override fun nonFlowingPersisterLegacy(persister: Persister<Output, Key>): StoreBuilder<Key, Output> =
|
||||
|
|
|
@ -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<Key : Any, Input : Any, Output : Any>(
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -30,6 +30,7 @@ internal interface SourceOfTruth<Key, Input, Output> {
|
|||
fun reader(key: Key): Flow<Output?>
|
||||
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<Key, Input, Output> {
|
|||
internal class PersistentSourceOfTruth<Key, Input, Output>(
|
||||
private val realReader: (Key) -> Flow<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)? = null
|
||||
) : SourceOfTruth<Key, Input, Output> {
|
||||
override val defaultOrigin = ResponseOrigin.Persister
|
||||
|
||||
override fun reader(key: Key): Flow<Output?> = 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<Key, Input, Output>(
|
|||
internal class PersistentNonFlowingSourceOfTruth<Key, Input, Output>(
|
||||
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<Key, Input, Output> {
|
||||
override val defaultOrigin = ResponseOrigin.Persister
|
||||
|
||||
override fun reader(key: Key): Flow<Output?> = 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")
|
||||
|
|
|
@ -103,6 +103,10 @@ internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
|||
delegate.delete(key)
|
||||
}
|
||||
|
||||
suspend fun deleteAll() {
|
||||
delegate.deleteAll()
|
||||
}
|
||||
|
||||
private sealed class BarrierMsg(
|
||||
val version: Long
|
||||
) {
|
||||
|
|
|
@ -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<BarCode, Int>(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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 <T> Type of key/request param in store
|
||||
*/
|
||||
@Deprecated("") // used in tests
|
||||
interface Clearable<T> {
|
||||
fun clear(key: T)
|
||||
}
|
|
@ -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<Int, BarCode>, Clearable<BarCode> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -144,9 +144,6 @@ data class TestStoreBuilder<Key : Any, Output : Any>(
|
|||
},
|
||||
realWriter = { key, value ->
|
||||
persister.write(key, value)
|
||||
},
|
||||
realDelete = { key ->
|
||||
(persister as? Clearable<Key>)?.clear(key)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<Int, String>()
|
||||
private val persister = InMemoryPersister<Int, String>()
|
||||
private val delegate: SourceOfTruth<Int, String, String> =
|
||||
PersistentSourceOfTruth(
|
||||
realReader = { key ->
|
||||
|
|
169
store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt
vendored
Normal file
169
store/src/test/java/com/dropbox/android/external/store4/impl/ClearAllStoreTest.kt
vendored
Normal file
|
@ -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<String, Int>()
|
||||
|
||||
@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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
171
store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt
vendored
Normal file
171
store/src/test/java/com/dropbox/android/external/store4/impl/ClearStoreByKeyTest.kt
vendored
Normal file
|
@ -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<String, Int>()
|
||||
|
||||
@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<String, Int>(
|
||||
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<String, Int>(
|
||||
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<String, Int>(
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<Int, String>().asObservable()
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = build(
|
||||
flowingFetcher = {
|
||||
flow {
|
||||
|
@ -356,7 +359,7 @@ class FlowStoreTest {
|
|||
|
||||
@Test
|
||||
fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runBlockingTest {
|
||||
val persister = InMemoryPersister<Int, String>().asObservable()
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = build(
|
||||
flowingFetcher = {
|
||||
flow {
|
||||
|
@ -403,7 +406,7 @@ class FlowStoreTest {
|
|||
@Test
|
||||
fun errorTest() = testScope.runBlockingTest {
|
||||
val exception = IllegalArgumentException("wow")
|
||||
val persister = InMemoryPersister<Int, String>().asObservable()
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = build(
|
||||
nonFlowingFetcher = {
|
||||
throw exception
|
||||
|
@ -662,23 +665,6 @@ class FlowStoreTest {
|
|||
}
|
||||
}
|
||||
|
||||
class InMemoryPersister<Key, Output> {
|
||||
private val data = mutableMapOf<Key, Output>()
|
||||
|
||||
@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 <Key : Any, Input : Any, Output : Any> build(
|
||||
nonFlowingFetcher: (suspend (Key) -> Input)? = null,
|
||||
flowingFetcher: ((Key) -> Flow<Input>)? = null,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
30
store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt
vendored
Normal file
30
store/src/test/java/com/dropbox/android/external/store4/testutil/InMemoryPersister.kt
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
package com.dropbox.android.external.store4.testutil
|
||||
|
||||
/**
|
||||
* An in-memory non-flowing persister for testing.
|
||||
*/
|
||||
class InMemoryPersister<Key, Output> {
|
||||
private val data = mutableMapOf<Key, Output>()
|
||||
|
||||
@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]
|
||||
}
|
||||
}
|
|
@ -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<Key> {
|
|||
val keyChannel = lock.withLock {
|
||||
channels.getOrPut(key) {
|
||||
KeyChannel(
|
||||
channel = BroadcastChannel<Unit>(Channel.CONFLATED).apply {
|
||||
// start w/ an initial value.
|
||||
offer(Unit)
|
||||
}
|
||||
channel = BroadcastChannel<Unit>(Channel.CONFLATED).apply {
|
||||
// start w/ an initial value.
|
||||
offer(Unit)
|
||||
}
|
||||
)
|
||||
}.also {
|
||||
it.acquire() // refcount
|
||||
|
@ -130,3 +130,9 @@ internal class KeyTracker<Key> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
suspend fun <Key, Output> InMemoryPersister<Key, Output>.asFlowable() = SimplePersisterAsFlowable(
|
||||
reader = this::read,
|
||||
writer = this::write
|
||||
)
|
|
@ -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<List<String?>>()
|
||||
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<String>()
|
||||
return SimplePersisterAsFlowable<BarCode, String, String>(
|
||||
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
|
||||
}
|
||||
}
|
20
store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt
vendored
Normal file
20
store/src/test/java/com/dropbox/android/external/store4/testutil/TestStoreExt.kt
vendored
Normal file
|
@ -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 <Key : Any, Output : Any> Store<Key, Output>.getData(key: Key) =
|
||||
stream(
|
||||
StoreRequest.cached(key, refresh = false)
|
||||
).filterNot {
|
||||
it is StoreResponse.Loading
|
||||
}.first().let {
|
||||
StoreResponse.Data(it.requireData(), it.origin)
|
||||
}
|
Loading…
Reference in a new issue