Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,7 @@ dependencies {
implementation(libs.bundles.room)
ksp(libs.androidx.room.compiler)
detektPlugins(libs.compose.detekt)

// Testing dependencies
testImplementation(libs.junit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
}
}
}
11 changes: 10 additions & 1 deletion app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions app/src/test/README.md
Original file line number Diff line number Diff line change
@@ -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/reports/tests/testFossDebugUnitTest/index.html`
- `build/reports/tests/testCoreDebugUnitTest/index.html`
Original file line number Diff line number Diff line change
@@ -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
)
}
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading