Add Cache
This commit is contained in:
parent
d2e8dbb3ca
commit
7a77fc43b8
6 changed files with 260 additions and 0 deletions
12
app/core/src/main/java/com/fsck/k9/cache/Cache.kt
vendored
Normal file
12
app/core/src/main/java/com/fsck/k9/cache/Cache.kt
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
interface Cache<KEY : Any, VALUE : Any> {
|
||||
|
||||
operator fun get(key: KEY): VALUE?
|
||||
|
||||
operator fun set(key: KEY, value: VALUE)
|
||||
|
||||
fun hasKey(key: KEY): Boolean
|
||||
|
||||
fun clear()
|
||||
}
|
45
app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt
vendored
Normal file
45
app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
import com.fsck.k9.Clock
|
||||
|
||||
internal class ExpiringCache<KEY : Any, VALUE : Any>(
|
||||
private val clock: Clock,
|
||||
private val delegateCache: Cache<KEY, VALUE> = InMemoryCache(),
|
||||
private var lastClearTime: Long = clock.time,
|
||||
private val cacheTimeValidity: Long = CACHE_TIME_VALIDITY_IN_MILLIS
|
||||
) : Cache<KEY, VALUE> {
|
||||
|
||||
override fun get(key: KEY): VALUE? {
|
||||
recycle()
|
||||
return delegateCache[key]
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
recycle()
|
||||
delegateCache[key] = value
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
recycle()
|
||||
return delegateCache.hasKey(key)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
lastClearTime = clock.time
|
||||
delegateCache.clear()
|
||||
}
|
||||
|
||||
private fun recycle() {
|
||||
if (isExpired()) {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isExpired(): Boolean {
|
||||
return (clock.time - lastClearTime) >= cacheTimeValidity
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CACHE_TIME_VALIDITY_IN_MILLIS = 30_000L
|
||||
}
|
||||
}
|
21
app/core/src/main/java/com/fsck/k9/cache/InMemoryCache.kt
vendored
Normal file
21
app/core/src/main/java/com/fsck/k9/cache/InMemoryCache.kt
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
internal class InMemoryCache<KEY : Any, VALUE : Any>(
|
||||
private val cache: MutableMap<KEY, VALUE> = mutableMapOf()
|
||||
) : Cache<KEY, VALUE> {
|
||||
override fun get(key: KEY): VALUE? {
|
||||
return cache[key]
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
cache[key] = value
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
return cache.containsKey(key)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
30
app/core/src/main/java/com/fsck/k9/cache/SynchronizedCache.kt
vendored
Normal file
30
app/core/src/main/java/com/fsck/k9/cache/SynchronizedCache.kt
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
internal class SynchronizedCache<KEY : Any, VALUE : Any>(
|
||||
private val delegateCache: Cache<KEY, VALUE>
|
||||
) : Cache<KEY, VALUE> {
|
||||
|
||||
override fun get(key: KEY): VALUE? {
|
||||
synchronized(delegateCache) {
|
||||
return delegateCache[key]
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
synchronized(delegateCache) {
|
||||
delegateCache[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
synchronized(delegateCache) {
|
||||
return delegateCache.hasKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
synchronized(delegateCache) {
|
||||
delegateCache.clear()
|
||||
}
|
||||
}
|
||||
}
|
84
app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt
vendored
Normal file
84
app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
import com.fsck.k9.TestClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
data class CacheTestData<KEY : Any, VALUE : Any>(
|
||||
val name: String,
|
||||
val createCache: () -> Cache<KEY, VALUE>
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CacheTest(data: CacheTestData<Any, Any>) {
|
||||
|
||||
private val testSubject = data.createCache()
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun data(): Collection<CacheTestData<Any, Any>> {
|
||||
return listOf(
|
||||
CacheTestData("InMemoryCache") { InMemoryCache() },
|
||||
CacheTestData("ExpiringCache") { ExpiringCache(TestClock(), InMemoryCache()) },
|
||||
CacheTestData("SynchronizedCache") { SynchronizedCache(InMemoryCache()) }
|
||||
)
|
||||
}
|
||||
|
||||
const val KEY = "key"
|
||||
const val VALUE = "value"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get should return null with empty cache`() {
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get should return entry when present`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo(VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should add entry with empty cache`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo(VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should overwrite entry when already present`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
testSubject[KEY] = "$VALUE changed"
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo("$VALUE changed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer no with empty cache`() {
|
||||
assertThat(testSubject.hasKey(KEY)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer yes when cache has entry`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
assertThat(testSubject.hasKey(KEY)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear should empty cache`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
testSubject.clear()
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
}
|
68
app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt
vendored
Normal file
68
app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
package com.fsck.k9.cache
|
||||
|
||||
import com.fsck.k9.TestClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.test.Test
|
||||
|
||||
class ExpiringCacheTest {
|
||||
|
||||
private val clock = TestClock()
|
||||
|
||||
private val testSubject: Cache<String, String> = ExpiringCache(clock, InMemoryCache())
|
||||
|
||||
@Test
|
||||
fun `get should return null when entry present and cache expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)
|
||||
|
||||
val result = testSubject[KEY]
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should clear cache and add new entry when cache expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)
|
||||
|
||||
testSubject[KEY + 1] = "$VALUE changed"
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
assertThat(testSubject[KEY + 1]).isEqualTo("$VALUE changed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer no when cache has entry and validity expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)
|
||||
|
||||
assertThat(testSubject.hasKey(KEY)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should keep cache when time progresses within expiration`() {
|
||||
testSubject[KEY] = VALUE
|
||||
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS - 1)
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo(VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should empty cache after time progresses to expiration`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
|
||||
private fun advanceClockBy(timeInMillis: Long) {
|
||||
clock.time = clock.time + timeInMillis
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY = "key"
|
||||
const val VALUE = "value"
|
||||
const val CACHE_TIME_VALIDITY_IN_MILLIS = 30_000L
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue