Skip to content

Commit

Permalink
Add Cache
Browse files Browse the repository at this point in the history
  • Loading branch information
wmontwe committed Feb 10, 2023
1 parent d2e8dbb commit b23376d
Show file tree
Hide file tree
Showing 6 changed files with 384 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()
}
}
}
175 changes: 175 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,175 @@
package com.fsck.k9.cache

import com.fsck.k9.TestClock
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

class ExpiringCacheTest {

private val clock = TestClock()

@Test
fun implements_cache_interface() {
assertIs<Cache<String, String>>(ExpiringCache<String, String>(clock = clock))
}

@Test
fun get_withEmptyCache() {
val testSubject = emptyCache()

val result = testSubject["key"]

assertNull(result)
}

@Test
fun get_withFilledCache() {
val testSubject = filledCache()

val result = testSubject["key"]

assertSame(
expected = VALUE,
actual = result
)
}

@Test
fun get_withFilledCacheExpired() {
val testSubject = filledCache()
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)

val result = testSubject["key"]

assertNull(result)
}

@Test
fun set_withEmptyCache() {
val testSubject = emptyCache()

testSubject[KEY] = VALUE

assertSame(
expected = VALUE,
actual = testSubject[KEY]
)
}

@Test
fun set_withFilledCache() {
val testSubject = filledCache()

testSubject[KEY] = "$VALUE changed"

assertSame(
expected = "$VALUE changed",
actual = testSubject[KEY]
)
}

@Test
fun set_withFilledCacheExpired() {
val testSubject = filledCache()
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)

testSubject[KEY + 1] = "$VALUE changed"

assertNull(testSubject[KEY])
assertSame(
expected = "$VALUE changed",
actual = testSubject[KEY + 1]
)
}

@Test
fun hasKey_withEmptyCache() {
val testSubject = emptyCache()

val result = testSubject.hasKey(KEY)

assertFalse(
actual = result
)
}

@Test
fun hasKey_withFilledCache() {
val testSubject = filledCache()

val result = testSubject.hasKey(KEY)

assertTrue(
actual = result
)
}

@Test
fun hasKey_withFilledCacheExpired() {
val testSubject = filledCache()
advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS)

val result = testSubject.hasKey(KEY)

assertFalse(
actual = result
)
}

@Test
fun clear_withFilledCache() {
val testSubject = filledCache()

testSubject.clear()

assertNull(testSubject[KEY])
}

@Test
fun clear_withFilledCacheAndTimeProgression() {
val testSubject = filledCache()

testSubject.clear()
testSubject[KEY] = VALUE

assertSame(
expected = VALUE,
actual = testSubject[KEY]
)

advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS - 1)

assertSame(
expected = VALUE,
actual = testSubject[KEY]
)

advanceClockBy(1)

assertNull(testSubject[KEY])
}

private fun emptyCache() = ExpiringCache(
clock = clock,
delegateCache = InMemoryCache(cache = mutableMapOf())
)

private fun filledCache() = ExpiringCache(
clock = clock,
delegateCache = InMemoryCache(cache = mutableMapOf((KEY to VALUE)))
)

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
}
}
101 changes: 101 additions & 0 deletions app/core/src/test/java/com/fsck/k9/cache/InMemoryCacheTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.fsck.k9.cache

import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

class InMemoryCacheTest {

@Test
fun implements_cache_interface() {
assertIs<Cache<String, String>>(InMemoryCache<String, String>())
}

@Test
fun get_withEmptyCache() {
val testSubject = emptyCache()

val result = testSubject["key"]

assertNull(result)
}

@Test
fun get_withFilledCache() {
val testSubject = filledCache()

val result = testSubject["key"]

assertSame(
expected = VALUE,
actual = result
)
}

@Test
fun set_withEmptyCache() {
val testSubject = emptyCache()

testSubject[KEY] = VALUE

assertSame(
expected = VALUE,
actual = testSubject[KEY]
)
}

@Test
fun set_withFilledCache() {
val testSubject = filledCache()

testSubject[KEY] = "$VALUE changed"

assertSame(
expected = "$VALUE changed",
actual = testSubject[KEY]
)
}

@Test
fun hasKey_withEmptyCache() {
val testSubject = emptyCache()

val result = testSubject.hasKey(KEY)

assertFalse(
actual = result
)
}

@Test
fun hasKey_withFilledCache() {
val testSubject = filledCache()

val result = testSubject.hasKey(KEY)

assertTrue(
actual = result
)
}

@Test
fun clear_withFilledCache() {
val testSubject = filledCache()

testSubject.clear()

assertNull(testSubject[KEY])
}

private fun emptyCache(): InMemoryCache<String, String> = InMemoryCache(cache = mutableMapOf())

private fun filledCache(): InMemoryCache<String, String> = InMemoryCache(cache = mutableMapOf((KEY to VALUE)))

private companion object {
const val KEY = "key"
const val VALUE = "value"
}
}

0 comments on commit b23376d

Please sign in to comment.