-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
app/core/src/main/java/com/fsck/k9/cache/SynchronizedCache.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
68
app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |