-
-
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
384 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() | ||
} | ||
} | ||
} |
175 changes: 175 additions & 0 deletions
175
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,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
101
app/core/src/test/java/com/fsck/k9/cache/InMemoryCacheTest.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,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" | ||
} | ||
} |