From 2ce7c6e412d0835386241f8a0f52629158aacd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf=20Montwe=CC=81?= Date: Tue, 28 Feb 2023 16:57:33 +0100 Subject: [PATCH 1/2] Add core testing with TestClock using Kotlinx datetime --- core/testing/build.gradle.kts | 7 +++++ .../app/k9mail/core/testing/TestClock.kt | 14 +++++++++ .../app/k9mail/core/testing/TestClockTest.kt | 30 +++++++++++++++++++ gradle/libs.versions.toml | 3 ++ settings.gradle.kts | 1 + 5 files changed, 55 insertions(+) create mode 100644 core/testing/build.gradle.kts create mode 100644 core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt create mode 100644 core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts new file mode 100644 index 0000000000..ef4b49ac3c --- /dev/null +++ b/core/testing/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id(ThunderbirdPlugins.Library.jvm) +} + +dependencies { + implementation(libs.kotlinx.datetime) +} diff --git a/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt b/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt new file mode 100644 index 0000000000..acac6d88af --- /dev/null +++ b/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt @@ -0,0 +1,14 @@ +package app.k9mail.core.testing + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +class TestClock( + private var currentTime: Instant = Clock.System.now(), +) : Clock { + override fun now(): Instant = currentTime + + fun changeTimeTo(time: Instant) { + currentTime = time + } +} diff --git a/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt b/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt new file mode 100644 index 0000000000..eb0e029825 --- /dev/null +++ b/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt @@ -0,0 +1,30 @@ +package app.k9mail.core.testing + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import kotlinx.datetime.Instant +import kotlin.test.Test + +internal class TestClockTest { + + @Test + fun `should return the current time`() { + val testClock = TestClock() + + val currentTime = testClock.now() + + assertThat(currentTime).isNotNull() + } + + @Test + fun `should return the changed time`() { + val testClock = TestClock() + + testClock.changeTimeTo(Instant.DISTANT_FUTURE) + + val currentTime = testClock.now() + + assertThat(currentTime).isEqualTo(Instant.DISTANT_FUTURE) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3126f73fa8..0a4dc30171 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinCoroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutines" } +kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } androidx-activity = { module = "androidx.activity:activity", version.ref = "androidxActivity" } @@ -128,6 +129,7 @@ truth = "com.google.truth:truth:1.1.3" turbine = "app.cash.turbine:turbine:0.12.1" jdom2 = "org.jdom:jdom2:2.0.6.1" icu4j-charset = "com.ibm.icu:icu4j-charset:72.1" +assertk = "com.willowtreeapps.assertk:assertk-jvm:0.25" leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.9.1" @@ -137,6 +139,7 @@ detekt-plugin-compose = "io.nlopez.compose.rules:detekt:0.1.1" shared-jvm-test = [ "junit", "truth", + "assertk", "mockito-inline", "mockito-kotlin", "koin-test", diff --git a/settings.gradle.kts b/settings.gradle.kts index c0dfe8471d..374dda89ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,6 +42,7 @@ include( ":core:ui:compose:demo", ":core:ui:compose:designsystem", ":core:ui:compose:theme", + ":core:testing", ) include( From 28e3f62a4e3017aaa3de5fb296a7bab6d9eec527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf=20Montwe=CC=81?= Date: Wed, 1 Mar 2023 17:27:34 +0100 Subject: [PATCH 2/2] Change Clock to kotlinx.datetime.Clock --- app/core/build.gradle.kts | 1 + app/core/src/main/java/com/fsck/k9/Clock.kt | 13 ------------ app/core/src/main/java/com/fsck/k9/K9.kt | 1 + .../src/main/java/com/fsck/k9/KoinModule.kt | 3 ++- .../java/com/fsck/k9/QuietTimeChecker.java | 4 +++- .../java/com/fsck/k9/cache/ExpiringCache.kt | 9 ++++---- .../com/fsck/k9/job/MailSyncWorkerManager.kt | 9 +++++--- .../com/fsck/k9/mailstore/LocalStore.java | 2 +- .../k9/mailstore/OutboxStateRepository.kt | 11 ++++++---- .../NewMailNotificationManager.kt | 4 ++-- .../fsck/k9/preferences/SettingsImporter.java | 4 ++-- .../java/com/fsck/k9/QuietTimeCheckerTest.kt | 4 +++- .../test/java/com/fsck/k9/cache/CacheTest.kt | 2 +- .../com/fsck/k9/cache/ExpiringCacheTest.kt | 19 +++++++---------- .../NewMailNotificationManagerTest.kt | 7 ++++--- .../SummaryNotificationDataCreatorTest.kt | 4 ++-- .../src/main/java/com/fsck/k9/TestClock.kt | 5 ----- app/ui/legacy/build.gradle.kts | 1 + .../k9/ui/helper/RelativeDateTimeFormatter.kt | 16 +++++++++++--- .../k9/ui/messagelist/MessageListFragment.kt | 2 +- .../helper/RelativeDateTimeFormatterTest.kt | 5 +++-- .../ui/messagelist/MessageListAdapterTest.kt | 2 +- .../thunderbird.library.android.gradle.kts | 1 + .../kotlin/thunderbird.library.jvm.gradle.kts | 1 + config/detekt/baseline.xml | 3 +-- core/testing/build.gradle.kts | 4 ---- .../app/k9mail/core/testing/TestClock.kt | 5 +++++ .../app/k9mail/core/testing/TestClockTest.kt | 21 +++++++++++++------ gradle/libs.versions.toml | 3 +++ 29 files changed, 93 insertions(+), 73 deletions(-) delete mode 100644 app/core/src/main/java/com/fsck/k9/Clock.kt delete mode 100644 app/testing/src/main/java/com/fsck/k9/TestClock.kt diff --git a/app/core/build.gradle.kts b/app/core/build.gradle.kts index 9245c58ffa..38541aa5a6 100644 --- a/app/core/build.gradle.kts +++ b/app/core/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(libs.mime4j.core) implementation(libs.mime4j.dom) + testApi(projects.core.testing) testImplementation(projects.mail.testing) testImplementation(projects.backend.imap) testImplementation(projects.mail.protocols.smtp) diff --git a/app/core/src/main/java/com/fsck/k9/Clock.kt b/app/core/src/main/java/com/fsck/k9/Clock.kt deleted file mode 100644 index 755102351c..0000000000 --- a/app/core/src/main/java/com/fsck/k9/Clock.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.fsck.k9 - -/** - * An interface to provide the current time. - */ -interface Clock { - val time: Long -} - -internal class RealClock : Clock { - override val time: Long - get() = System.currentTimeMillis() -} diff --git a/app/core/src/main/java/com/fsck/k9/K9.kt b/app/core/src/main/java/com/fsck/k9/K9.kt index 95f7fe1cc3..c47101cc81 100644 --- a/app/core/src/main/java/com/fsck/k9/K9.kt +++ b/app/core/src/main/java/com/fsck/k9/K9.kt @@ -9,6 +9,7 @@ import com.fsck.k9.mailstore.LocalStore import com.fsck.k9.preferences.RealGeneralSettingsManager import com.fsck.k9.preferences.Storage import com.fsck.k9.preferences.StorageEditor +import kotlinx.datetime.Clock import timber.log.Timber import timber.log.Timber.DebugTree diff --git a/app/core/src/main/java/com/fsck/k9/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/KoinModule.kt index a9a5267ed9..b5921c0892 100644 --- a/app/core/src/main/java/com/fsck/k9/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/KoinModule.kt @@ -10,6 +10,7 @@ import com.fsck.k9.mailstore.LocalStoreProvider import com.fsck.k9.setup.ServerNameSuggester import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope +import kotlinx.datetime.Clock import org.koin.core.qualifier.named import org.koin.dsl.module @@ -30,7 +31,7 @@ val mainModule = module { single { TrustManagerFactory.createInstance(get()) } single { LocalKeyStoreManager(get()) } single { DefaultTrustedSocketFactory(get(), get()) } - single { RealClock() } + single { Clock.System } factory { ServerNameSuggester() } factory { EmailAddressValidator() } factory { ServerSettingsSerializer() } diff --git a/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java b/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java index 8db00d3e81..6844b08d5f 100644 --- a/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java +++ b/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java @@ -3,6 +3,8 @@ package com.fsck.k9; import java.util.Calendar; +import kotlinx.datetime.Clock; + class QuietTimeChecker { private final Clock clock; @@ -31,7 +33,7 @@ class QuietTimeChecker { } Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(clock.getTime()); + calendar.setTimeInMillis(clock.now().toEpochMilliseconds()); int minutesSinceMidnight = (calendar.get(Calendar.HOUR_OF_DAY) * 60) + calendar.get(Calendar.MINUTE); diff --git a/app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt b/app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt index 2c45f12dc0..ad8d3459b9 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt +++ b/app/core/src/main/java/com/fsck/k9/cache/ExpiringCache.kt @@ -1,11 +1,12 @@ package com.fsck.k9.cache -import com.fsck.k9.Clock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant internal class ExpiringCache( private val clock: Clock, private val delegateCache: Cache = InMemoryCache(), - private var lastClearTime: Long = clock.time, + private var lastClearTime: Instant = clock.now(), private val cacheTimeValidity: Long = CACHE_TIME_VALIDITY_IN_MILLIS, ) : Cache { @@ -25,7 +26,7 @@ internal class ExpiringCache( } override fun clear() { - lastClearTime = clock.time + lastClearTime = clock.now() delegateCache.clear() } @@ -36,7 +37,7 @@ internal class ExpiringCache( } private fun isExpired(): Boolean { - return (clock.time - lastClearTime) >= cacheTimeValidity + return (clock.now() - lastClearTime).inWholeMilliseconds >= cacheTimeValidity } private companion object { diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt index 77ea8abbc1..cff8f598da 100644 --- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt +++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt @@ -8,12 +8,15 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import com.fsck.k9.Account -import com.fsck.k9.Clock import com.fsck.k9.K9 import java.util.concurrent.TimeUnit +import kotlinx.datetime.Clock import timber.log.Timber -class MailSyncWorkerManager(private val workManager: WorkManager, val clock: Clock) { +class MailSyncWorkerManager( + private val workManager: WorkManager, + val clock: Clock, +) { fun cancelMailSync(account: Account) { Timber.v("Canceling mail sync worker for %s", account) @@ -66,7 +69,7 @@ class MailSyncWorkerManager(private val workManager: WorkManager, val clock: Clo } private fun calculateInitialDelay(lastSyncTime: Long, syncIntervalMinutes: Long): Long { - val now = clock.time + val now = clock.now().toEpochMilliseconds() val nextSyncTime = lastSyncTime + (syncIntervalMinutes * 60L * 1000L) return if (lastSyncTime > now || nextSyncTime <= now) { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 27e3e11736..517bef2c38 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -27,7 +27,6 @@ import android.text.TextUtils; import androidx.core.database.CursorKt; import com.fsck.k9.Account; -import com.fsck.k9.Clock; import com.fsck.k9.DI; import com.fsck.k9.Preferences; import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand; @@ -52,6 +51,7 @@ import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchField; import com.fsck.k9.search.SqlQueryBuilder; +import kotlinx.datetime.Clock; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt index 792c3195af..628250b58c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt @@ -1,13 +1,16 @@ package com.fsck.k9.mailstore import android.content.ContentValues -import com.fsck.k9.Clock import com.fsck.k9.helper.getIntOrThrow import com.fsck.k9.helper.getLongOrThrow import com.fsck.k9.helper.getStringOrNull import com.fsck.k9.helper.getStringOrThrow +import kotlinx.datetime.Clock -class OutboxStateRepository(private val database: LockableDatabase, private val clock: Clock) { +class OutboxStateRepository( + private val database: LockableDatabase, + private val clock: Clock, +) { fun getOutboxState(messageId: Long): OutboxState { return database.execute(false) { db -> @@ -76,7 +79,7 @@ class OutboxStateRepository(private val database: LockableDatabase, private val } fun setSendAttemptError(messageId: Long, errorMessage: String) { - val sendErrorTimestamp = clock.time + val sendErrorTimestamp = clock.now().toEpochMilliseconds() database.execute(false) { db -> val contentValues = ContentValues().apply { @@ -90,7 +93,7 @@ class OutboxStateRepository(private val database: LockableDatabase, private val } fun setSendAttemptsExceeded(messageId: Long) { - val sendErrorTimestamp = clock.time + val sendErrorTimestamp = clock.now().toEpochMilliseconds() database.execute(false) { db -> val contentValues = ContentValues().apply { diff --git a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt index 0a8e411a54..d883297cd0 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt @@ -1,9 +1,9 @@ package com.fsck.k9.notification import com.fsck.k9.Account -import com.fsck.k9.Clock import com.fsck.k9.controller.MessageReference import com.fsck.k9.mailstore.LocalMessage +import kotlinx.datetime.Clock /** * Manages notifications for new messages @@ -130,5 +130,5 @@ internal class NewMailNotificationManager( } } - private fun now(): Long = clock.time + private fun now(): Long = clock.now().toEpochMilliseconds() } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java index e00d161dde..4ed1e02512 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java @@ -18,7 +18,6 @@ import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import com.fsck.k9.Account; import com.fsck.k9.AccountPreferenceSerializer; -import com.fsck.k9.Clock; import com.fsck.k9.Core; import com.fsck.k9.DI; import com.fsck.k9.Identity; @@ -30,6 +29,7 @@ import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mailstore.SpecialLocalFoldersCreator; import com.fsck.k9.preferences.Settings.InvalidSettingValueException; +import kotlinx.datetime.Clock; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -469,7 +469,7 @@ public class SettingsImporter { // To avoid reusing a previously existing notification channel ID, we need to make sure to use a unique value // for `messagesNotificationChannelVersion`. Clock clock = DI.get(Clock.class); - String messageNotificationChannelVersion = Long.toString(clock.getTime() / 1000); + String messageNotificationChannelVersion = Long.toString(clock.now().getEpochSeconds()); putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion); AccountDescription imported = new AccountDescription(accountName, uuid); diff --git a/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt b/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt index 19c67aced5..854f3dc2f8 100644 --- a/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt @@ -1,6 +1,8 @@ package com.fsck.k9 +import app.k9mail.core.testing.TestClock import java.util.Calendar +import kotlinx.datetime.Instant import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -111,6 +113,6 @@ class QuietTimeCheckerTest { calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) calendar.set(Calendar.MINUTE, minute) - clock.time = calendar.timeInMillis + clock.changeTimeTo(Instant.fromEpochMilliseconds(calendar.timeInMillis)) } } diff --git a/app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt b/app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt index c4c83ffacb..4660edddf5 100644 --- a/app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt +++ b/app/core/src/test/java/com/fsck/k9/cache/CacheTest.kt @@ -1,6 +1,6 @@ package com.fsck.k9.cache -import com.fsck.k9.TestClock +import app.k9mail.core.testing.TestClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt b/app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt index 123892dbf7..d69d96c057 100644 --- a/app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt +++ b/app/core/src/test/java/com/fsck/k9/cache/ExpiringCacheTest.kt @@ -1,8 +1,9 @@ package com.fsck.k9.cache -import com.fsck.k9.TestClock +import app.k9mail.core.testing.TestClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds class ExpiringCacheTest { @@ -13,7 +14,7 @@ class ExpiringCacheTest { @Test fun `get should return null when entry present and cache expired`() { testSubject[KEY] = VALUE - advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS) + clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION) val result = testSubject[KEY] @@ -23,7 +24,7 @@ class ExpiringCacheTest { @Test fun `set should clear cache and add new entry when cache expired`() { testSubject[KEY] = VALUE - advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS) + clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION) testSubject[KEY + 1] = "$VALUE changed" @@ -34,7 +35,7 @@ class ExpiringCacheTest { @Test fun `hasKey should answer no when cache has entry and validity expired`() { testSubject[KEY] = VALUE - advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS) + clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION) assertThat(testSubject.hasKey(KEY)).isFalse() } @@ -42,7 +43,7 @@ class ExpiringCacheTest { @Test fun `should keep cache when time progresses within expiration`() { testSubject[KEY] = VALUE - advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS - 1) + clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION.minus(1L.milliseconds)) assertThat(testSubject[KEY]).isEqualTo(VALUE) } @@ -51,18 +52,14 @@ class ExpiringCacheTest { fun `should empty cache after time progresses to expiration`() { testSubject[KEY] = VALUE - advanceClockBy(CACHE_TIME_VALIDITY_IN_MILLIS) + clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION) 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 + val CACHE_TIME_VALIDITY_DURATION = 30_000L.milliseconds } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt index 49ea41c227..a9dbffe2de 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt @@ -1,7 +1,7 @@ package com.fsck.k9.notification +import app.k9mail.core.testing.TestClock import com.fsck.k9.Account -import com.fsck.k9.TestClock import com.fsck.k9.controller.MessageReference import com.fsck.k9.mailstore.LocalMessage import com.fsck.k9.mailstore.LocalStore @@ -10,6 +10,7 @@ import com.fsck.k9.mailstore.MessageStoreManager import com.fsck.k9.mailstore.NotificationMessage import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull +import kotlinx.datetime.Instant import org.junit.Test import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn @@ -27,7 +28,7 @@ class NewMailNotificationManagerTest { private val account = createAccount() private val notificationContentCreator = mock() private val localStoreProvider = createLocalStoreProvider() - private val clock = TestClock(TIMESTAMP) + private val clock = TestClock(Instant.fromEpochMilliseconds(TIMESTAMP)) private val manager = NewMailNotificationManager( notificationContentCreator, createNotificationRepository(), @@ -82,7 +83,7 @@ class NewMailNotificationManagerTest { ) manager.addNewMailNotification(account, messageOne, silent = false) val timestamp = TIMESTAMP + 1000 - clock.time = timestamp + clock.changeTimeTo(Instant.fromEpochMilliseconds(timestamp)) val result = manager.addNewMailNotification(account, messageTwo, silent = false) diff --git a/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt index 2b5c64ceed..16dfd32bad 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt @@ -1,11 +1,11 @@ package com.fsck.k9.notification +import app.k9mail.core.testing.TestClock import com.fsck.k9.Account -import com.fsck.k9.Clock import com.fsck.k9.K9 -import com.fsck.k9.TestClock import com.fsck.k9.controller.MessageReference import com.google.common.truth.Truth.assertThat +import kotlinx.datetime.Clock import org.junit.After import org.junit.Before import org.junit.Test diff --git a/app/testing/src/main/java/com/fsck/k9/TestClock.kt b/app/testing/src/main/java/com/fsck/k9/TestClock.kt deleted file mode 100644 index 3421c3e24e..0000000000 --- a/app/testing/src/main/java/com/fsck/k9/TestClock.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.fsck.k9 - -class TestClock(initialTime: Long = 0L) : Clock { - override var time: Long = initialTime -} diff --git a/app/ui/legacy/build.gradle.kts b/app/ui/legacy/build.gradle.kts index 95ab9401e2..46f1efd945 100644 --- a/app/ui/legacy/build.gradle.kts +++ b/app/ui/legacy/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.glide) annotationProcessor(libs.glide.compiler) + testImplementation(projects.core.testing) testImplementation(projects.mail.testing) testImplementation(projects.app.storage) testImplementation(projects.app.testing) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatter.kt index 689f0155d5..14911766be 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatter.kt @@ -9,18 +9,22 @@ import android.text.format.DateUtils.FORMAT_SHOW_DATE import android.text.format.DateUtils.FORMAT_SHOW_TIME import android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY import android.text.format.DateUtils.FORMAT_SHOW_YEAR -import com.fsck.k9.Clock import java.util.Calendar import java.util.Calendar.DAY_OF_WEEK import java.util.Calendar.YEAR +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant /** * Formatter to describe timestamps as a time relative to now. */ -class RelativeDateTimeFormatter(private val context: Context, private val clock: Clock) { +class RelativeDateTimeFormatter( + private val context: Context, + private val clock: Clock, +) { fun formatDate(timestamp: Long): String { - val now = clock.time.toCalendar() + val now = clock.now().toCalendar() val date = timestamp.toCalendar() val format = when { date.isToday() -> FORMAT_SHOW_TIME @@ -38,6 +42,12 @@ private fun Long.toCalendar(): Calendar { return calendar } +private fun Instant.toCalendar(): Calendar { + val calendar = Calendar.getInstance() + calendar.timeInMillis = this.toEpochMilliseconds() + return calendar +} + private fun Calendar.isToday() = DateUtils.isToday(this.timeInMillis) private fun Calendar.isWithinPastSevenDaysOf(other: Calendar) = this.before(other) && diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt index 99182918d2..cc116d1e81 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -28,7 +28,6 @@ import app.k9mail.ui.utils.linearlayoutmanager.LinearLayoutManager import com.fsck.k9.Account import com.fsck.k9.Account.Expunge import com.fsck.k9.Account.SortType -import com.fsck.k9.Clock import com.fsck.k9.K9 import com.fsck.k9.Preferences import com.fsck.k9.SwipeAction @@ -59,6 +58,7 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar import java.util.concurrent.Future +import kotlinx.datetime.Clock import net.jcip.annotations.GuardedBy import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt index 0da1ccbc7e..f281d902e1 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt @@ -2,12 +2,13 @@ package com.fsck.k9.ui.helper import android.os.Build import android.os.SystemClock +import app.k9mail.core.testing.TestClock import com.fsck.k9.RobolectricTest -import com.fsck.k9.TestClock import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId import java.util.TimeZone +import kotlinx.datetime.Instant import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -125,7 +126,7 @@ class RelativeDateTimeFormatterTest : RobolectricTest() { val dateTime = LocalDateTime.parse(time) val timeInMillis = dateTime.toEpochMillis() SystemClock.setCurrentTimeMillis(timeInMillis) // Is handled by ShadowSystemClock - clock.time = timeInMillis + clock.changeTimeTo(Instant.fromEpochMilliseconds(timeInMillis)) } private fun String.toEpochMillis() = LocalDateTime.parse(this).toEpochMillis() diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt index f2af4b87ca..398380715f 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt @@ -11,12 +11,12 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible +import app.k9mail.core.testing.TestClock import com.fsck.k9.Account import com.fsck.k9.FontSizes import com.fsck.k9.FontSizes.FONT_DEFAULT import com.fsck.k9.FontSizes.LARGE import com.fsck.k9.RobolectricTest -import com.fsck.k9.TestClock import com.fsck.k9.UiDensity import com.fsck.k9.contacts.ContactPictureLoader import com.fsck.k9.mail.Address diff --git a/build-plugin/src/main/kotlin/thunderbird.library.android.gradle.kts b/build-plugin/src/main/kotlin/thunderbird.library.android.gradle.kts index 51e743e10e..7ebbf674a4 100644 --- a/build-plugin/src/main/kotlin/thunderbird.library.android.gradle.kts +++ b/build-plugin/src/main/kotlin/thunderbird.library.android.gradle.kts @@ -26,5 +26,6 @@ android { } dependencies { + implementation(libs.bundles.shared.jvm.main) testImplementation(libs.bundles.shared.jvm.test) } diff --git a/build-plugin/src/main/kotlin/thunderbird.library.jvm.gradle.kts b/build-plugin/src/main/kotlin/thunderbird.library.jvm.gradle.kts index db8bc6c1e2..ac22661273 100644 --- a/build-plugin/src/main/kotlin/thunderbird.library.jvm.gradle.kts +++ b/build-plugin/src/main/kotlin/thunderbird.library.jvm.gradle.kts @@ -9,5 +9,6 @@ java { } dependencies { + implementation(libs.bundles.shared.jvm.main) testImplementation(libs.bundles.shared.jvm.test) } diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 91d0a4725b..0ac46bc699 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -174,7 +174,7 @@ LongParameterList:MessageDatabaseHelpers.kt$( type: Int = 0, root: Int? = null, parent: Int = -1, seq: Int = 0, mimeType: String = "text/plain", decodedBodySize: Int = 0, displayName: String? = null, header: String? = null, encoding: String = "7bit", charset: String? = null, dataLocation: Int = 0, data: ByteArray? = null, preamble: String? = null, epilogue: String? = null, boundary: String? = null, contentId: String? = null, serverExtra: String? = null, directory: File? = null, ) LongParameterList:MessageDetailsViewModel.kt$MessageDetailsViewModel$( private val resources: Resources, private val messageRepository: MessageRepository, private val folderRepository: FolderRepository, private val contactSettingsProvider: ContactSettingsProvider, private val contacts: Contacts, private val clipboardManager: ClipboardManager, private val accountManager: AccountManager, private val participantFormatter: MessageDetailsParticipantFormatter, private val folderNameFormatter: FolderNameFormatter, ) LongParameterList:MessageListAdapterTest.kt$MessageListAdapterTest$( account: Account = Account(SOME_ACCOUNT_UUID), subject: String? = "irrelevant", threadCount: Int = 0, messageDate: Long = 0L, internalDate: Long = 0L, displayName: CharSequence = "irrelevant", displayAddress: Address? = Address.parse("irrelevant@domain.example").first(), previewText: String = "irrelevant", isMessageEncrypted: Boolean = false, isRead: Boolean = false, isStarred: Boolean = false, isAnswered: Boolean = false, isForwarded: Boolean = false, hasAttachments: Boolean = false, uniqueId: Long = 0L, folderId: Long = 0L, messageUid: String = "irrelevant", databaseId: Long = 0L, threadRoot: Long = 0L, ) - LongParameterList:MessageListAdapterTest.kt$MessageListAdapterTest$( fontSizes: FontSizes = createFontSizes(), previewLines: Int = 0, stars: Boolean = true, senderAboveSubject: Boolean = false, showContactPicture: Boolean = true, showingThreadedList: Boolean = true, backGroundAsReadIndicator: Boolean = false, showAccountChip: Boolean = false, ) + LongParameterList:MessageListAdapterTest.kt$MessageListAdapterTest$( fontSizes: FontSizes = createFontSizes(), previewLines: Int = 0, stars: Boolean = true, senderAboveSubject: Boolean = false, showContactPicture: Boolean = true, showingThreadedList: Boolean = true, backGroundAsReadIndicator: Boolean = false, showAccountChip: Boolean = false, density: UiDensity = UiDensity.Default, ) LongParameterList:MessagePartDatabaseHelpers.kt$( type: Int = MessagePartType.UNKNOWN, root: Long? = null, parent: Long = -1, seq: Int = 0, mimeType: String? = null, decodedBodySize: Int? = null, displayName: String? = null, header: String? = null, encoding: String? = null, charset: String? = null, dataLocation: Int = DataLocation.MISSING, data: ByteArray? = null, preamble: String? = null, epilogue: String? = null, boundary: String? = null, contentId: String? = null, serverExtra: String? = null, ) LongParameterList:PasswordPromptDialogFragment.kt$PasswordPromptDialogFragment.Companion$( accountUuid: String, accountName: String, inputIncomingServerPassword: Boolean, incomingServerName: String?, inputOutgoingServerPassword: Boolean, outgoingServerName: String?, targetFragment: Fragment, requestCode: Int, ) LongParameterList:PushController.kt$PushController$( private val preferences: Preferences, private val generalSettingsManager: GeneralSettingsManager, private val backendManager: BackendManager, private val pushServiceManager: PushServiceManager, private val bootCompleteManager: BootCompleteManager, private val autoSyncManager: AutoSyncManager, private val pushNotificationManager: PushNotificationManager, private val connectivityManager: ConnectivityManager, private val accountPushControllerFactory: AccountPushControllerFactory, private val coroutineScope: CoroutineScope = GlobalScope, private val coroutineDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(), ) @@ -197,7 +197,6 @@ MagicNumber:Account.kt$Account$6 MagicNumber:Account.kt$Account$84 MagicNumber:Account.kt$Account.DeletePolicy.MARK_AS_READ$3 - MagicNumber:AccountChip.kt$AccountChip$16 MagicNumber:AccountCreator.kt$AccountCreator$110 MagicNumber:AccountCreator.kt$AccountCreator$143 MagicNumber:AccountCreator.kt$AccountCreator$443 diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index ef4b49ac3c..063b640132 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -1,7 +1,3 @@ plugins { id(ThunderbirdPlugins.Library.jvm) } - -dependencies { - implementation(libs.kotlinx.datetime) -} diff --git a/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt b/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt index acac6d88af..508b2c424b 100644 --- a/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt +++ b/core/testing/src/main/kotlin/app/k9mail/core/testing/TestClock.kt @@ -1,5 +1,6 @@ package app.k9mail.core.testing +import kotlin.time.Duration import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -11,4 +12,8 @@ class TestClock( fun changeTimeTo(time: Instant) { currentTime = time } + + fun advanceTimeBy(duration: Duration) { + currentTime += duration + } } diff --git a/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt b/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt index eb0e029825..980133f3ae 100644 --- a/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt +++ b/core/testing/src/test/kotlin/app/k9mail/core/testing/TestClockTest.kt @@ -2,29 +2,38 @@ package app.k9mail.core.testing import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isNotNull -import kotlinx.datetime.Instant import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.datetime.Instant internal class TestClockTest { @Test fun `should return the current time`() { - val testClock = TestClock() + val testClock = TestClock(Instant.DISTANT_PAST) val currentTime = testClock.now() - assertThat(currentTime).isNotNull() + assertThat(currentTime).isEqualTo(Instant.DISTANT_PAST) } @Test fun `should return the changed time`() { - val testClock = TestClock() - + val testClock = TestClock(Instant.DISTANT_PAST) testClock.changeTimeTo(Instant.DISTANT_FUTURE) val currentTime = testClock.now() assertThat(currentTime).isEqualTo(Instant.DISTANT_FUTURE) } + + @Test + fun `should advance time by duration`() { + val testClock = TestClock(Instant.DISTANT_PAST) + testClock.advanceTimeBy(1L.milliseconds) + + val currentTime = testClock.now() + + assertThat(currentTime).isEqualTo(Instant.DISTANT_PAST + 1L.milliseconds) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a4dc30171..3f348f33d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -136,6 +136,9 @@ leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.9.1" detekt-plugin-compose = "io.nlopez.compose.rules:detekt:0.1.1" [bundles] +shared-jvm-main = [ + "kotlinx-datetime", +] shared-jvm-test = [ "junit", "truth",