Add Store RxJava3 module (#171)

This commit is contained in:
Mez Pahlan 2020-05-31 00:47:14 +01:00 committed by GitHub
parent 0afefdb0b0
commit 10da116ea6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 779 additions and 3 deletions

View file

@ -35,6 +35,7 @@ ext.versions = [
navigation : '2.2.1',
constraintLayout : '1.1.3',
rx2 : '2.2.19',
rx3 : '3.0.3',
// Testing.
junit : '4.13',
@ -69,6 +70,7 @@ ext.libraries = [
roomRuntime : "androidx.room:room-ktx:$versions.room",
coreKtx : "androidx.core:core-ktx:$versions.coreKtx",
rx2 : "io.reactivex.rxjava2:rxjava:$versions.rx2",
rx3 : "io.reactivex.rxjava3:rxjava:$versions.rx3",
// Testing.
junit : "junit:junit:$versions.junit",
@ -76,7 +78,8 @@ ext.libraries = [
mockito : "org.mockito:mockito-core:$versions.mockito",
mockitoKotlin : "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockitoKotlin",
coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines",
coroutinesRx : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$versions.coroutines",
coroutinesRx2 : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$versions.coroutines",
coroutinesRx3 : "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$versions.coroutines",
coroutinesReactive : "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$versions.coroutines",
coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines",
coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines",

View file

@ -1 +1 @@
include ':app', ':store', ':cache', ':filesystem', ':multicast',':store-rx2'
include ':app', ':store', ':cache', ':filesystem', ':multicast',':store-rx2', ':store-rx3'

View file

@ -6,7 +6,7 @@ plugins {
dependencies {
implementation libraries.kotlinStdLib
implementation libraries.coroutinesCore
implementation libraries.coroutinesRx
implementation libraries.coroutinesRx2
implementation libraries.coroutinesReactive
implementation project(path: ':store')
implementation libraries.rx2

1
store-rx3/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,26 @@
public final class com/dropbox/store/rx3/RxFetcherKt {
public static final fun flowableFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
public static final fun flowableValueFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
public static final fun singleFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
public static final fun singleValueFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
}
public final class com/dropbox/store/rx3/RxSourceOfTruthKt {
public static final fun fromFlowable (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/dropbox/android/external/store4/SourceOfTruth;
public static synthetic fun fromFlowable$default (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/dropbox/android/external/store4/SourceOfTruth;
public static final fun fromMaybe (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/dropbox/android/external/store4/SourceOfTruth;
public static synthetic fun fromMaybe$default (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/dropbox/android/external/store4/SourceOfTruth;
}
public final class com/dropbox/store/rx3/RxStoreBuilderKt {
public static final fun withScheduler (Lcom/dropbox/android/external/store4/StoreBuilder;Lio/reactivex/rxjava3/core/Scheduler;)Lcom/dropbox/android/external/store4/StoreBuilder;
}
public final class com/dropbox/store/rx3/RxStoreKt {
public static final fun freshSingle (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Single;
public static final fun getSingle (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Single;
public static final fun observe (Lcom/dropbox/android/external/store4/Store;Lcom/dropbox/android/external/store4/StoreRequest;)Lio/reactivex/rxjava3/core/Flowable;
public static final fun observeClear (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Completable;
public static final fun observeClearAll (Lcom/dropbox/android/external/store4/Store;)Lio/reactivex/rxjava3/core/Completable;
}

39
store-rx3/build.gradle Normal file
View file

@ -0,0 +1,39 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.dokka'
}
dependencies {
implementation libraries.kotlinStdLib
implementation libraries.coroutinesCore
implementation libraries.coroutinesRx3
implementation libraries.coroutinesReactive
implementation project(path: ':store')
implementation libraries.rx3
testImplementation libraries.junit
testImplementation libraries.truth
testImplementation libraries.mockito
testImplementation libraries.coroutinesTest
}
group = GROUP
version = VERSION_NAME
apply from: rootProject.file("gradle/maven-push.gradle")
apply from: rootProject.file("gradle/jacoco.gradle")
targetCompatibility = 1.8
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [
'-Xopt-in=kotlin.RequiresOptIn',
]
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2020 Dropbox LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
POM_NAME=com.dropbox.mobile.store
POM_ARTIFACT_ID=store-rx3
POM_PACKAGING=jar

View file

@ -0,0 +1,71 @@
package com.dropbox.store.rx3
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.FetcherResult
import com.dropbox.android.external.store4.Store
import com.dropbox.android.external.store4.valueFetcher
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.reactive.asFlow
/**
* Creates a new [Fetcher] from a [flowableFactory].
*
* [Store] does not catch exception thrown in [flowableFactory] or in the returned [Flowable]. These
* exception will be propagated to the caller.
*
* Use when creating a [Store] that fetches objects in a multiple responses per request
* network protocol (e.g Web Sockets).
*
* @param flowableFactory a factory for a [Flowable] source of network records.
*/
fun <Key : Any, Output : Any> flowableFetcher(
flowableFactory: (key: Key) -> Flowable<FetcherResult<Output>>
): Fetcher<Key, Output> = { key: Key -> flowableFactory(key).asFlow() }
/**
* "Creates" a [Fetcher] from a [singleFactory].
*
* [Store] does not catch exception thrown in [singleFactory] or in the returned [Single]. These
* exception will be propagated to the caller.
*
* Use when creating a [Store] that fetches objects in a single response per request network
* protocol (e.g Http).
*
* @param singleFactory a factory for a [Single] source of network records.
*/
fun <Key : Any, Output : Any> singleFetcher(
singleFactory: (key: Key) -> Single<FetcherResult<Output>>
): Fetcher<Key, Output> = { key: Key -> singleFactory(key).toFlowable().asFlow() }
/**
* "Creates" a [Fetcher] from a [flowableFactory] and translate the results to a [FetcherResult].
*
* Emitted values will be wrapped in [FetcherResult.Data]. if an exception disrupts the stream then
* it will be wrapped in [FetcherResult.Error]. Exceptions thrown in [flowableFactory] itself are
* not caught and will be returned to the caller.
*
* Use when creating a [Store] that fetches objects in a multiple responses per request
* network protocol (e.g Web Sockets).
*
* @param flowFactory a factory for a [Flowable] source of network records.
*/
fun <Key : Any, Output : Any> flowableValueFetcher(
flowableFactory: (key: Key) -> Flowable<Output>
): Fetcher<Key, Output> = valueFetcher { key: Key -> flowableFactory(key).asFlow() }
/**
* Creates a new [Fetcher] from a [singleFactory] and translate the results to a [FetcherResult].
*
* The emitted value will be wrapped in [FetcherResult.Data]. if an exception is returned then
* it will be wrapped in [FetcherResult.Error]. Exceptions thrown in [singleFactory] itself are
* not caught and will be returned to the caller.
*
* Use when creating a [Store] that fetches objects in a single response per request network
* protocol (e.g Http).
*
* @param singleFactory a factory for a [Single] source of network records.
*/
fun <Key : Any, Output : Any> singleValueFetcher(
singleFactory: (key: Key) -> Single<Output>
): Fetcher<Key, Output> = flowableValueFetcher { key: Key -> singleFactory(key).toFlowable() }

View file

@ -0,0 +1,62 @@
package com.dropbox.store.rx3
import com.dropbox.android.external.store4.SourceOfTruth
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.rx3.await
/**
* Creates a [Maybe] source of truth that is accessible via [reader], [writer], [delete] and
* [deleteAll].
*
* @param reader function for reading records from the source of truth
* @param writer function for writing updates to the backing source of truth
* @param delete function for deleting records in the source of truth for the given key
* @param deleteAll function for deleting all records in the source of truth
*
*/
fun <Key : Any, Input : Any, Output : Any> SourceOfTruth.Companion.fromMaybe(
reader: (Key) -> Maybe<Output>,
writer: (Key, Input) -> Completable,
delete: ((Key) -> Completable)? = null,
deleteAll: (() -> Completable)? = null
): SourceOfTruth<Key, Input, Output> {
val deleteFun: (suspend (Key) -> Unit)? =
if (delete != null) { key -> delete(key).await() } else null
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
return fromNonFlow(
reader = { key -> reader.invoke(key).await() },
writer = { key, output -> writer.invoke(key, output).await() },
delete = deleteFun,
deleteAll = deleteAllFun
)
}
/**
* Creates a ([Flowable]) source of truth that is accessed via [reader], [writer], [delete] and
* [deleteAll].
*
* @param reader function for reading records from the source of truth
* @param writer function for writing updates to the backing source of truth
* @param delete function for deleting records in the source of truth for the given key
* @param deleteAll function for deleting all records in the source of truth
*
*/
fun <Key : Any, Input : Any, Output : Any> SourceOfTruth.Companion.fromFlowable(
reader: (Key) -> Flowable<Output>,
writer: (Key, Input) -> Completable,
delete: ((Key) -> Completable)? = null,
deleteAll: (() -> Completable)? = null
): SourceOfTruth<Key, Input, Output> {
val deleteFun: (suspend (Key) -> Unit)? =
if (delete != null) { key -> delete(key).await() } else null
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
return from(
reader = { key -> reader.invoke(key).asFlow() },
writer = { key, output -> writer.invoke(key, output).await() },
delete = deleteFun,
deleteAll = deleteAllFun
)
}

View file

@ -0,0 +1,52 @@
package com.dropbox.store.rx3
import com.dropbox.android.external.store4.ExperimentalStoreApi
import com.dropbox.android.external.store4.Store
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.fresh
import com.dropbox.android.external.store4.get
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.rx3.asFlowable
import kotlinx.coroutines.rx3.rxCompletable
import kotlinx.coroutines.rx3.rxSingle
/**
* Return a [Flowable] for the given key
* @param request - see [StoreRequest] for configurations
*/
@ExperimentalCoroutinesApi
fun <Key : Any, Output : Any> Store<Key, Output>.observe(request: StoreRequest<Key>): Flowable<StoreResponse<Output>> =
stream(request).asFlowable()
/**
* Purge a particular entry from memory and disk cache.
* Persistent storage will only be cleared if a delete function was passed to
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
*/
fun <Key : Any, Output : Any> Store<Key, Output>.observeClear(key: Key): Completable =
rxCompletable { clear(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
fun <Key : Any, Output : Any> Store<Key, Output>.observeClearAll(): Completable =
rxCompletable { clearAll() }
/**
* Helper factory that will return data as a [Single] for [key] if it is cached otherwise will return fresh/network data (updating your caches)
*/
fun <Key : Any, Output : Any> Store<Key, Output>.getSingle(key: Key) =
rxSingle { this@getSingle.get(key) }
/**
* Helper factory that will return fresh data as a [Single] for [key] while updating your caches
*/
fun <Key : Any, Output : Any> Store<Key, Output>.freshSingle(key: Key) =
rxSingle { this@freshSingle.fresh(key) }

View file

@ -0,0 +1,21 @@
package com.dropbox.store.rx3
import com.dropbox.android.external.store4.StoreBuilder
import io.reactivex.rxjava3.core.Scheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.rx3.asCoroutineDispatcher
/**
* Define what scheduler fetcher requests will be called on,
* if a scheduler is not set Store will use [GlobalScope]
*/
@FlowPreview
@ExperimentalCoroutinesApi
fun <Key : Any, Output : Any> StoreBuilder<Key, Output>.withScheduler(
scheduler: Scheduler
): StoreBuilder<Key, Output> {
return scope(CoroutineScope(scheduler.asCoroutineDispatcher()))
}

View file

@ -0,0 +1,86 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.dropbox.store.rx3.test
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.TestCoroutineScope
@OptIn(ExperimentalCoroutinesApi::class)
internal fun <T> TestCoroutineScope.assertThat(flow: Flow<T>): FlowSubject<T> {
return Truth.assertAbout(
FlowSubject.Factory<T>(
this
)
).that(flow)
}
@OptIn(ExperimentalCoroutinesApi::class)
internal class FlowSubject<T> constructor(
failureMetadata: FailureMetadata,
private val testCoroutineScope: TestCoroutineScope,
private val actual: Flow<T>
) : Subject(failureMetadata, actual) {
/**
* Takes all items in the flow that are available by collecting on it as long as there are
* active jobs in the given [TestCoroutineScope].
*
* It ensures all expected items are dispatched as well as no additional unexpected items are
* dispatched.
*/
suspend fun emitsExactly(vararg expected: T) {
val collectedSoFar = mutableListOf<T>()
val collectionCoroutine = testCoroutineScope.async {
actual.collect {
collectedSoFar.add(it)
if (collectedSoFar.size > expected.size) {
assertWithMessage("Too many emissions in the flow (only first additional item is shown)")
.that(collectedSoFar)
.isEqualTo(expected)
}
}
}
testCoroutineScope.advanceUntilIdle()
if (!collectionCoroutine.isActive) {
collectionCoroutine.getCompletionExceptionOrNull()?.let {
throw it
}
}
collectionCoroutine.cancelAndJoin()
assertWithMessage("Flow didn't exactly emit expected items")
.that(collectedSoFar)
.isEqualTo(expected.toList())
}
class Factory<T>(
private val testCoroutineScope: TestCoroutineScope
) : Subject.Factory<FlowSubject<T>, Flow<T>> {
override fun createSubject(metadata: FailureMetadata, actual: Flow<T>): FlowSubject<T> {
return FlowSubject(
failureMetadata = metadata,
actual = actual,
testCoroutineScope = testCoroutineScope
)
}
}
}

View file

@ -0,0 +1,83 @@
package com.dropbox.store.rx3.test
import com.dropbox.android.external.store4.FetcherResult
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.store.rx3.singleFetcher
import com.google.common.truth.Truth.assertThat
import io.reactivex.rxjava3.core.Single
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
@RunWith(JUnit4::class)
@FlowPreview
@ExperimentalCoroutinesApi
class HotRxSingleStoreTest {
private val testScope = TestCoroutineScope()
@Test
fun `GIVEN a hot fetcher WHEN two cached and one fresh call THEN fetcher is only called twice`() =
testScope.runBlockingTest {
val fetcher: FakeRxFetcher<Int, FetcherResult<String>> =
FakeRxFetcher(
3 to FetcherResult.Data("three-1"),
3 to FetcherResult.Data("three-2")
)
val pipeline = StoreBuilder.from(singleFetcher<Int, String> { fetcher.fetch(it) })
.scope(testScope)
.build()
assertThat(pipeline.stream(StoreRequest.cached(3, refresh = false)))
.emitsExactly(
StoreResponse.Loading<String>(
origin = ResponseOrigin.Fetcher
), StoreResponse.Data(
value = "three-1",
origin = ResponseOrigin.Fetcher
)
)
assertThat(
pipeline.stream(StoreRequest.cached(3, refresh = false))
).emitsExactly(
StoreResponse.Data(
value = "three-1",
origin = ResponseOrigin.Cache
)
)
assertThat(pipeline.stream(StoreRequest.fresh(3)))
.emitsExactly(
StoreResponse.Loading<String>(
origin = ResponseOrigin.Fetcher
),
StoreResponse.Data(
value = "three-2",
origin = ResponseOrigin.Fetcher
)
)
}
}
class FakeRxFetcher<Key, Output>(
vararg val responses: Pair<Key, Output>
) {
private var index = 0
@Suppress("RedundantSuspendModifier") // needed for function reference
fun fetch(key: Key): Single<Output> {
// will throw if fetcher called more than twice
if (index >= responses.size) {
throw AssertionError("unexpected fetch request")
}
val pair = responses[index++]
assertThat(pair.first).isEqualTo(key)
return Single.just(pair.second)
}
}

View file

@ -0,0 +1,109 @@
package com.dropbox.store.rx3.test
import com.dropbox.android.external.store4.FetcherResult
import com.dropbox.android.external.store4.ResponseOrigin
import com.dropbox.android.external.store4.SourceOfTruth
import com.dropbox.android.external.store4.StoreBuilder
import com.dropbox.android.external.store4.StoreRequest
import com.dropbox.android.external.store4.StoreResponse
import com.dropbox.store.rx3.flowableFetcher
import com.dropbox.store.rx3.fromFlowable
import com.dropbox.store.rx3.observe
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.schedulers.TestScheduler
import io.reactivex.rxjava3.subscribers.TestSubscriber
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.atomic.AtomicInteger
@RunWith(JUnit4::class)
@FlowPreview
@ExperimentalCoroutinesApi
class RxFlowableStoreTest {
private val testScheduler = TestScheduler()
private val atomicInteger = AtomicInteger(0)
private val fakeDisk = mutableMapOf<Int, String>()
private val store =
StoreBuilder.from<Int, String, String>(
flowableFetcher {
Flowable.create({ emitter ->
emitter.onNext(
FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence")
)
emitter.onNext(
FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence")
)
emitter.onComplete()
}, BackpressureStrategy.LATEST)
},
sourceOfTruth = SourceOfTruth.fromFlowable(
reader = {
if (fakeDisk[it] != null)
Flowable.fromCallable { fakeDisk[it]!! }
else
Flowable.empty<String>()
},
writer = { key, value ->
Completable.fromAction { fakeDisk[key] = value }
}
))
.build()
@Test
fun simpleTest() {
var testSubscriber = TestSubscriber<StoreResponse<String>>()
store.observe(StoreRequest.fresh(3))
.subscribeOn(testScheduler)
.subscribe(testSubscriber)
testScheduler.triggerActions()
testSubscriber
.awaitCount(3)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 1 occurrence", ResponseOrigin.Fetcher),
StoreResponse.Data("3 2 occurrence", ResponseOrigin.Fetcher)
)
testSubscriber = TestSubscriber<StoreResponse<String>>()
store.observe(StoreRequest.cached(3, false))
.subscribeOn(testScheduler)
.subscribe(testSubscriber)
testScheduler.triggerActions()
testSubscriber
.awaitCount(2)
.assertValues(
StoreResponse.Data("3 2 occurrence", ResponseOrigin.Cache),
StoreResponse.Data("3 2 occurrence", ResponseOrigin.SourceOfTruth)
)
testSubscriber = TestSubscriber<StoreResponse<String>>()
store.observe(StoreRequest.fresh(3))
.subscribeOn(testScheduler)
.subscribe(testSubscriber)
testScheduler.triggerActions()
testSubscriber
.awaitCount(3)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 3 occurrence", ResponseOrigin.Fetcher),
StoreResponse.Data("3 4 occurrence", ResponseOrigin.Fetcher)
)
testSubscriber = TestSubscriber<StoreResponse<String>>()
store.observe(StoreRequest.cached(3, false))
.subscribeOn(testScheduler)
.subscribe(testSubscriber)
testScheduler.triggerActions()
testSubscriber
.awaitCount(2)
.assertValues(
StoreResponse.Data("3 4 occurrence", ResponseOrigin.Cache),
StoreResponse.Data("3 4 occurrence", ResponseOrigin.SourceOfTruth)
)
}
}

View file

@ -0,0 +1,77 @@
package com.dropbox.store.rx3.test
import com.dropbox.android.external.store4.ExperimentalStoreApi
import com.dropbox.android.external.store4.FetcherResult
import com.dropbox.android.external.store4.SourceOfTruth
import com.dropbox.android.external.store4.StoreBuilder
import com.dropbox.store.rx3.freshSingle
import com.dropbox.store.rx3.fromMaybe
import com.dropbox.store.rx3.getSingle
import com.dropbox.store.rx3.singleFetcher
import com.dropbox.store.rx3.withScheduler
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.atomic.AtomicInteger
@ExperimentalStoreApi
@RunWith(JUnit4::class)
@FlowPreview
@ExperimentalCoroutinesApi
class RxSingleStoreExtensionsTest {
private val atomicInteger = AtomicInteger(0)
private var fakeDisk = mutableMapOf<Int, String>()
private val store =
StoreBuilder.from<Int, String, String>(
fetcher = singleFetcher {
Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") }
},
sourceOfTruth = SourceOfTruth.fromMaybe(
reader = { Maybe.fromCallable<String> { fakeDisk[it] } },
writer = { key, value ->
Completable.fromAction { fakeDisk[key] = value }
},
delete = { key ->
Completable.fromAction { fakeDisk.remove(key) }
},
deleteAll = {
Completable.fromAction { fakeDisk.clear() }
}
)
)
.withScheduler(Schedulers.trampoline())
.build()
@Test
fun `store rx extension tests`() {
// Return from cache - after initial fetch
store.getSingle(3)
.test()
.await()
.assertValue("3 1")
// Return from cache
store.getSingle(3)
.test()
.await()
.assertValue("3 1")
// Return from fresh - forcing a new fetch
store.freshSingle(3)
.test()
.await()
.assertValue("3 2")
// Return from cache - different to initial
store.getSingle(3)
.test()
.await()
.assertValue("3 2")
}
}

View file

@ -0,0 +1,128 @@
package com.dropbox.store.rx3.test
import com.dropbox.android.external.store4.ExperimentalStoreApi
import com.dropbox.android.external.store4.FetcherResult
import com.dropbox.android.external.store4.ResponseOrigin
import com.dropbox.android.external.store4.SourceOfTruth
import com.dropbox.android.external.store4.StoreBuilder
import com.dropbox.android.external.store4.StoreRequest
import com.dropbox.android.external.store4.StoreResponse
import com.dropbox.store.rx3.fromMaybe
import com.dropbox.store.rx3.observe
import com.dropbox.store.rx3.observeClear
import com.dropbox.store.rx3.observeClearAll
import com.dropbox.store.rx3.singleFetcher
import com.dropbox.store.rx3.withScheduler
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.atomic.AtomicInteger
@ExperimentalStoreApi
@RunWith(JUnit4::class)
@FlowPreview
@ExperimentalCoroutinesApi
class RxSingleStoreTest {
private val atomicInteger = AtomicInteger(0)
private var fakeDisk = mutableMapOf<Int, String>()
private val store =
StoreBuilder.from<Int, String, String>(
fetcher = singleFetcher {
Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") }
},
sourceOfTruth = SourceOfTruth.fromMaybe(
reader = { Maybe.fromCallable<String> { fakeDisk[it] } },
writer = { key, value ->
Completable.fromAction { fakeDisk[key] = value }
},
delete = { key ->
Completable.fromAction { fakeDisk.remove(key) }
},
deleteAll = {
Completable.fromAction { fakeDisk.clear() }
}
)
)
.withScheduler(Schedulers.trampoline())
.build()
@Test
fun simpleTest() {
store.observe(StoreRequest.cached(3, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
)
store.observe(StoreRequest.cached(3, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Data("3 1", ResponseOrigin.Cache),
StoreResponse.Data("3 1", ResponseOrigin.SourceOfTruth)
)
store.observe(StoreRequest.fresh(3))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 2", ResponseOrigin.Fetcher)
)
store.observe(StoreRequest.cached(3, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Data("3 2", ResponseOrigin.Cache),
StoreResponse.Data("3 2", ResponseOrigin.SourceOfTruth)
)
}
@Test
fun `GIVEN a store with persister values WHEN observeClear is Called THEN next Store get hits network`() {
fakeDisk[3] = "seeded occurrence"
store.observeClear(3).blockingAwait()
store.observe(StoreRequest.cached(3, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
)
}
@Test
fun `GIVEN a store with persister values WHEN observeClearAll is called THEN next Store get calls both hit network`() {
fakeDisk[3] = "seeded occurrence"
fakeDisk[4] = "another seeded occurrence"
store.observeClearAll().blockingAwait()
store.observe(StoreRequest.cached(3, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
)
store.observe(StoreRequest.cached(4, false))
.test()
.awaitCount(2)
.assertValues(
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
StoreResponse.Data("4 2", ResponseOrigin.Fetcher)
)
}
}