Add Cache

This commit is contained in:
Wolf Montwe 2023-02-08 14:28:19 +01:00 committed by Wolf Montwé
parent d2e8dbb3ca
commit 7a77fc43b8
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
6 changed files with 260 additions and 0 deletions

View 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()
}

View 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
}
}

View 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()
}
}

View 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()
}
}
}

View 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()
}
}

View 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
}
}