Skip to content

Commit d6559c1

Browse files
committed
chore: update documentation, fix unit tests, move files
1 parent 8505557 commit d6559c1

4 files changed

Lines changed: 176 additions & 128 deletions

File tree

cache/src/main/java/com/kroger/cache/internal/CacheFlowWrapper.kt renamed to cache/src/main/java/com/kroger/cache/CacheFlowWrapper.kt

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
package com.kroger.cache.internal
2-
3-
import com.kroger.cache.SnapshotPersistentCache
4-
import kotlinx.coroutines.CoroutineScope
5-
import kotlinx.coroutines.Job
6-
import kotlinx.coroutines.flow.MutableStateFlow
7-
import kotlinx.coroutines.flow.StateFlow
8-
import kotlinx.coroutines.flow.asStateFlow
9-
import kotlinx.coroutines.flow.drop
10-
import kotlinx.coroutines.flow.launchIn
11-
import kotlinx.coroutines.flow.onEach
12-
import kotlinx.coroutines.launch
13-
141
/**
152
* MIT License
163
*
@@ -34,12 +21,28 @@ import kotlinx.coroutines.launch
3421
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3522
* SOFTWARE.
3623
*/
24+
package com.kroger.cache
25+
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.ExperimentalCoroutinesApi
28+
import kotlinx.coroutines.Job
29+
import kotlinx.coroutines.flow.FlowCollector
30+
import kotlinx.coroutines.flow.MutableSharedFlow
31+
import kotlinx.coroutines.flow.MutableStateFlow
32+
import kotlinx.coroutines.flow.StateFlow
33+
import kotlinx.coroutines.flow.asStateFlow
34+
import kotlinx.coroutines.flow.drop
35+
import kotlinx.coroutines.flow.launchIn
36+
import kotlinx.coroutines.flow.onEach
37+
import kotlinx.coroutines.launch
38+
3739
/**
38-
* A Wrapper class for a SnapshotPersistentCache that exposes changes to the cache via a flow.
40+
* A Wrapper class for a SnapshotPersistentCache that persists changes made to the value of the state flow
3941
*
40-
* **Note this works best when used as a singleton
42+
* Only emits values on the flow that are set via the same instance.
43+
* If two flow wrappers exist for the same SnapshotPersistentCache, and one gets updated, the second will be out of sync.
4144
*
42-
* @param cache the [com.kroger.cache.SnapshotPersistentCache] holding the value(s) on disk
45+
* @param cache the [SnapshotPersistentCache] holding the value(s) on disk
4346
* @param scope the [kotlinx.coroutines.CoroutineScope] to run the flow on
4447
*
4548
*/

cache/src/main/java/com/kroger/cache/internal/MemoryCache.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import com.kroger.cache.Cache
2929
* A wrapper around a [Map] to work as an in-memory cache.
3030
* This class is not thread-safe and callers must ensure access is synchronized.
3131
*
32-
* @param initialCapacity the initial capacity to use when creatig the [Map].
32+
* @param initialCapacity the initial capacity to use when creating the [Map].
3333
*/
3434
internal class MemoryCache<K, V>(
3535
initialCapacity: Int,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2023 The Kroger Co. All rights reserved.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
package com.kroger.cache
25+
26+
import app.cash.turbine.test
27+
import com.google.common.truth.Truth.assertThat
28+
import io.mockk.coEvery
29+
import io.mockk.coVerifySequence
30+
import io.mockk.just
31+
import io.mockk.mockk
32+
import io.mockk.runs
33+
import kotlinx.coroutines.CoroutineName
34+
import kotlinx.coroutines.CoroutineScope
35+
import kotlinx.coroutines.Dispatchers
36+
import kotlinx.coroutines.ExperimentalCoroutinesApi
37+
import kotlinx.coroutines.delay
38+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
39+
import kotlinx.coroutines.test.advanceTimeBy
40+
import kotlinx.coroutines.test.resetMain
41+
import kotlinx.coroutines.test.runTest
42+
import kotlinx.coroutines.test.setMain
43+
import org.junit.jupiter.api.AfterEach
44+
import org.junit.jupiter.api.BeforeEach
45+
import org.junit.jupiter.api.Test
46+
47+
@OptIn(ExperimentalCoroutinesApi::class)
48+
class CacheFlowWrapperTest {
49+
private val testDispatcher = UnconfinedTestDispatcher()
50+
val testScope = CoroutineScope(CoroutineName("CacheFlowWrapperTest") + testDispatcher)
51+
val fileCache: SnapshotPersistentCache<String> = mockk()
52+
53+
lateinit var cacheWrapper: CacheFlowWrapper<String>
54+
55+
private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null
56+
57+
@BeforeEach
58+
fun setup() {
59+
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
60+
Thread.setDefaultUncaughtExceptionHandler { _, e -> throw e }
61+
Dispatchers.setMain(testDispatcher)
62+
}
63+
64+
@AfterEach
65+
fun afterEach() {
66+
Thread.setDefaultUncaughtExceptionHandler(defaultUncaughtExceptionHandler)
67+
Dispatchers.resetMain()
68+
}
69+
70+
@OptIn(ExperimentalCoroutinesApi::class)
71+
@Test
72+
fun `GIVEN cache is still reading WHEN new value is set THEN set value will wait for read to finish`() =
73+
runTest {
74+
val fileCacheValue = "File cache value"
75+
val newValue = "new value"
76+
coEvery { fileCache.read() } coAnswers {
77+
delay(1000)
78+
fileCacheValue
79+
}
80+
coEvery { fileCache.save(any()) } just runs
81+
cacheWrapper = CacheFlowWrapper(fileCache, testScope)
82+
cacheWrapper.cacheValueFlow.test {
83+
assertThat(awaitItem()).isEqualTo(null)
84+
cacheWrapper.setValue(newValue)
85+
assertThat(awaitItem()).isEqualTo(fileCacheValue)
86+
advanceTimeBy(1000)
87+
assertThat(awaitItem()).isEqualTo(newValue)
88+
cancelAndIgnoreRemainingEvents()
89+
}
90+
91+
coVerifySequence {
92+
fileCache.read()
93+
fileCache.save(eq(newValue))
94+
}
95+
}
96+
97+
@OptIn(ExperimentalCoroutinesApi::class)
98+
@Test
99+
fun `GIVEN cache is done reading WHEN new value is set THEN set value will happen immediately`() =
100+
runTest {
101+
val fileCacheValue = "File cache value"
102+
val newValue = "new value"
103+
coEvery { fileCache.read() } coAnswers {
104+
fileCacheValue
105+
}
106+
coEvery { fileCache.save(any()) } just runs
107+
cacheWrapper = CacheFlowWrapper(fileCache, testScope)
108+
cacheWrapper.cacheValueFlow.test {
109+
assertThat(awaitItem()).isEqualTo(fileCacheValue)
110+
advanceTimeBy(1000)
111+
cacheWrapper.setValue(newValue)
112+
assertThat(awaitItem()).isEqualTo(newValue)
113+
cancelAndIgnoreRemainingEvents()
114+
}
115+
116+
coVerifySequence {
117+
fileCache.read()
118+
fileCache.save(eq(newValue))
119+
}
120+
}
121+
122+
@OptIn(ExperimentalCoroutinesApi::class)
123+
@Test
124+
fun `GIVEN cache is writing values slowly WHEN new values are set in quick succession THEN all values are emitted on flow, and last value is saved to disk`() =
125+
runTest {
126+
val firstValue = "first new value"
127+
val secondValue = "second new value"
128+
val thirdValue = "third new value"
129+
val fourthValue = "Fourth new value"
130+
coEvery { fileCache.read() } returns null
131+
coEvery { fileCache.save(any()) } coAnswers {
132+
delay(1000)
133+
}
134+
cacheWrapper = CacheFlowWrapper(fileCache, testScope)
135+
cacheWrapper.cacheValueFlow.test {
136+
assertThat(awaitItem()).isEqualTo(null)
137+
cacheWrapper.setValue(firstValue)
138+
cacheWrapper.setValue(secondValue)
139+
cacheWrapper.setValue(thirdValue)
140+
cacheWrapper.setValue(fourthValue)
141+
advanceTimeBy(2000)
142+
assertThat(awaitItem()).isEqualTo(firstValue)
143+
assertThat(awaitItem()).isEqualTo(secondValue)
144+
assertThat(awaitItem()).isEqualTo(thirdValue)
145+
assertThat(awaitItem()).isEqualTo(fourthValue)
146+
cancelAndIgnoreRemainingEvents()
147+
}
148+
149+
coVerifySequence {
150+
fileCache.read()
151+
fileCache.save(eq(firstValue)) // start writing the first value
152+
// second and third should be skipped since first isn't done writing yet
153+
fileCache.save(eq(fourthValue)) // fourth and final value is written
154+
}
155+
}
156+
}

cache/src/test/java/com/kroger/cache/internal/CacheFlowWrapperTest.kt

Lines changed: 0 additions & 111 deletions
This file was deleted.

0 commit comments

Comments
 (0)