Skip to content

Commit

Permalink
Add Cache
Browse files Browse the repository at this point in the history
  • Loading branch information
wmontwe committed Feb 13, 2023
1 parent d2e8dbb commit 7a77fc4
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 0 deletions.
12 changes: 12 additions & 0 deletions app/core/src/main/java/com/fsck/k9/cache/Cache.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 45 additions & 0 deletions app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions app/core/src/main/java/com/fsck/k9/cache/InMemoryCache.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions app/core/src/main/java/com/fsck/k9/cache/SynchronizedCache.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 68 additions & 0 deletions app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 7a77fc4

Please sign in to comment.