From 7430ba3af4e5775a8884ccf37e4a2594a516c6d2 Mon Sep 17 00:00:00 2001 From: AshBash Date: Sun, 4 Jan 2026 23:19:41 +0000 Subject: [PATCH 1/4] Added unit tests to reproduce behavior described in bug report https://github.com/FossifyOrg/Clock/issues/293 Refactored AlarmController to move logic that causes bug to separate testable function with optional parameter for passing in currentDayMinutes Refactored updateNonRecurringAlarmDay in Constants.kt with overloaded function, allowing testability with optional parameter currentDayMinutes Added unit test class AlarmControllerTest.kt under app/src/test folder Added readme explaining how to run tests --- app/build.gradle.kts | 5 ++ .../fossify/clock/helpers/AlarmController.kt | 15 +++- .../org/fossify/clock/helpers/Constants.kt | 11 ++- app/src/test/README.md | 25 ++++++ .../clock/helpers/AlarmControllerTest.kt | 86 +++++++++++++++++++ 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 app/src/test/README.md create mode 100644 app/src/test/kotlin/org/fossify/clock/helpers/AlarmControllerTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ff571ef0a..9e19fdb14 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,4 +152,9 @@ dependencies { implementation(libs.bundles.room) ksp(libs.androidx.room.compiler) detektPlugins(libs.compose.detekt) + + // Testing dependencies + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:5.8.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt index eb887cf90..ebc5d1945 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt @@ -34,7 +34,7 @@ class AlarmController( fun rescheduleEnabledAlarms() { db.getEnabledAlarms().forEach { // TODO: Skipped upcoming alarms are being *rescheduled* here. - if (!it.isToday() || it.timeInMinutes > getCurrentDayMinutes()) { + if (shouldRescheduleAlarm(it)) { scheduleNextOccurrence(it, false) } } @@ -240,5 +240,18 @@ class AlarmController( ).also { instance = it } } } + + /** + * Testable function that determines if an alarm should be rescheduled. + * This encapsulates the core logic from rescheduleEnabledAlarms(). + * + * @param alarm The alarm to check + * @param currentDayMinutes Optional current time for testing, null uses system time + * @return true if the alarm should be scheduled + */ + fun shouldRescheduleAlarm(alarm: Alarm, currentDayMinutes: Int? = null): Boolean { + val currentMinutes = currentDayMinutes ?: getCurrentDayMinutes() + return !alarm.isToday() || alarm.timeInMinutes > currentMinutes + } } } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt index 2d812c03d..55676eed3 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt @@ -304,8 +304,17 @@ fun getTimeOfNextAlarm(alarmTimeInMinutes: Int, days: Int): Calendar? { } fun updateNonRecurringAlarmDay(alarm: Alarm) { + updateNonRecurringAlarmDay(alarm, null) +} + +/** + * Testable version that accepts an optional current time for testing purposes. + * @param currentDayMinutes If provided, uses this as the current time instead of the system time. + */ +fun updateNonRecurringAlarmDay(alarm: Alarm, currentDayMinutes: Int?) { if (alarm.isRecurring()) return - alarm.days = if (alarm.timeInMinutes > getCurrentDayMinutes()) { + val currentMinutes = currentDayMinutes ?: getCurrentDayMinutes() + alarm.days = if (alarm.timeInMinutes > currentMinutes) { TODAY_BIT } else { TOMORROW_BIT diff --git a/app/src/test/README.md b/app/src/test/README.md new file mode 100644 index 000000000..a791bc86f --- /dev/null +++ b/app/src/test/README.md @@ -0,0 +1,25 @@ +# Unit Tests for Fossify Clock + +## Overview + +Unit tests for alarm scheduling logic. + +## Test Files + +### AlarmControllerTest.kt + +Tests alarm controller reschedule logic. + +## Running Tests + +```bash +# Run all tests +./gradlew test + +# Run specific test variant +./gradlew testFossDebugUnitTest +``` + +Test reports are generated at: +- `build-outputs/gradle/reports/tests/testFossDebugUnitTest/index.html` +- `build-outputs/gradle/reports/tests/testCoreDebugUnitTest/index.html` diff --git a/app/src/test/kotlin/org/fossify/clock/helpers/AlarmControllerTest.kt b/app/src/test/kotlin/org/fossify/clock/helpers/AlarmControllerTest.kt new file mode 100644 index 000000000..39fce2fa6 --- /dev/null +++ b/app/src/test/kotlin/org/fossify/clock/helpers/AlarmControllerTest.kt @@ -0,0 +1,86 @@ +package org.fossify.clock.helpers + +import org.fossify.clock.models.Alarm +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for AlarmController reschedule logic. + * Tests the actual functions used in AlarmController. + */ +class AlarmControllerTest { + + /** + * Test basic behavior: current time is 7 AM, alarm is 8 AM + * Should be marked as TODAY since alarm time hasn't passed yet + */ + @Test + fun testAlarmBeforeCurrentTime_ShouldBeToday() { + val alarm = createAlarm(timeInMinutes = 480) // 8:00 AM + val currentTime = 420 // 7:00 AM + + updateNonRecurringAlarmDay(alarm, currentTime) + + assertEquals(TODAY_BIT, alarm.days) + } + + /** + * Test basic behavior: current time is 9 AM, alarm is 8 AM + * Should be marked as TOMORROW since alarm time has passed + */ + @Test + fun testAlarmAfterCurrentTime_ShouldBeTomorrow() { + val alarm = createAlarm(timeInMinutes = 480) // 8:00 AM + val currentTime = 540 // 9:00 AM + + updateNonRecurringAlarmDay(alarm, currentTime) + + assertEquals(TOMORROW_BIT, alarm.days) + } + + /** + * BUG REPRODUCTION TEST using actual AlarmController.shouldRescheduleAlarm(): + * + * Scenario: User sets alarm for 8 AM tomorrow, saves to DB with TOMORROW_BIT. + * Next day arrives, user restarts phone at 9 AM (after alarm time). + * AlarmController.rescheduleEnabledAlarms() reads alarm from DB and calls shouldRescheduleAlarm(). + * + * Expected: Alarm should NOT be rescheduled (time has passed for today's intended alarm) + * Actual Bug: shouldRescheduleAlarm returns TRUE because alarm still has TOMORROW_BIT! + * + * This test asserts the CORRECT behavior, so it FAILS now (demonstrating the bug exists) + * and will PASS once we implement the fix. + */ + @Test + fun testBugScenario_StaleAlarmFromDBGetsRescheduledIncorrectly() { + // Alarm was set yesterday for "tomorrow" at 8:00 AM and saved to DB with TOMORROW_BIT + val alarm = createAlarm(timeInMinutes = 480, initialDays = TOMORROW_BIT) // 8:00 AM + + // Next day: it's now 9:00 AM (after the alarm time) + // The alarm in DB still has TOMORROW_BIT (stale data) + val currentTime = 540 // 9:00 AM + + // AlarmController.rescheduleEnabledAlarms() calls shouldRescheduleAlarm + val shouldSchedule = AlarmController.shouldRescheduleAlarm(alarm, currentTime) + + // Assert CORRECT behavior: should be FALSE (don't reschedule stale alarms) + // This will FAIL now because the bug makes it return TRUE + assertFalse("Stale alarm should NOT be rescheduled - time has passed", shouldSchedule) + } + + private fun createAlarm(timeInMinutes: Int, initialDays: Int = 0): Alarm { + return Alarm( + id = 1, + timeInMinutes = timeInMinutes, + days = initialDays, + isEnabled = true, + vibrate = true, + soundTitle = "Test", + soundUri = "", + label = "", + oneShot = false + ) + } +} From 7ab9425c8b40f102c147d8d48d9a6f5472d9793c Mon Sep 17 00:00:00 2001 From: AshBash Date: Sun, 4 Jan 2026 23:20:11 +0000 Subject: [PATCH 2/4] Corrected folder path in readme --- app/src/test/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/README.md b/app/src/test/README.md index a791bc86f..2c4e88bb0 100644 --- a/app/src/test/README.md +++ b/app/src/test/README.md @@ -21,5 +21,5 @@ Tests alarm controller reschedule logic. ``` Test reports are generated at: -- `build-outputs/gradle/reports/tests/testFossDebugUnitTest/index.html` -- `build-outputs/gradle/reports/tests/testCoreDebugUnitTest/index.html` +- `build/reports/tests/testFossDebugUnitTest/index.html` +- `build/reports/tests/testCoreDebugUnitTest/index.html` From ec18a1753a09159d0984c30f546544d689d88084 Mon Sep 17 00:00:00 2001 From: AshBash Date: Sun, 4 Jan 2026 23:42:04 +0000 Subject: [PATCH 3/4] Removing unused dependencies as we don't need mockito for this --- app/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9e19fdb14..531a04136 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,6 +155,4 @@ dependencies { // Testing dependencies testImplementation("junit:junit:4.13.2") - testImplementation("org.mockito:mockito-core:5.8.0") - testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") } From accf2b886801623a14845cecbe7fcc7ef5ba37e5 Mon Sep 17 00:00:00 2001 From: AshBash Date: Mon, 5 Jan 2026 00:13:16 +0000 Subject: [PATCH 4/4] Moved JUnit to libs version toml for consistency --- app/build.gradle.kts | 2 +- gradle/libs.versions.toml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 531a04136..65ddf6ebc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,5 +154,5 @@ dependencies { detektPlugins(libs.compose.detekt) // Testing dependencies - testImplementation("junit:junit:4.13.2") + testImplementation(libs.junit) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69c6d0748..7801df754 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,8 @@ kotlinx-coroutines = "1.10.2" numberpicker = "2.4.13" #Room room = "2.8.4" +#JUnit +junit = "4.13.2" #Fossify commons = "5.13.1" #Gradle @@ -55,6 +57,8 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } #Compose compose-detekt = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } +#JUnit +junit = { module = "junit:junit", version.ref = "junit" } #Fossify fossify-commons = { module = "org.fossify:commons", version.ref = "commons" } [bundles]