Initial commit - v1.1.9
This commit is contained in:
135
app/src/test/java/com/example/shiftalarm/CrossVersionTest.kt
Normal file
135
app/src/test/java/com/example/shiftalarm/CrossVersionTest.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
package com.example.shiftalarm
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDate
|
||||
import java.time.DayOfWeek
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class CrossVersionTest {
|
||||
|
||||
// ==========================================
|
||||
// 1. Simulation Configuration
|
||||
// ==========================================
|
||||
val START_DATE = LocalDate.of(2026, 2, 1)
|
||||
val END_DATE = LocalDate.of(2026, 3, 31)
|
||||
|
||||
// Target Android Versions
|
||||
val TARGET_VERSIONS = listOf(
|
||||
26 to "Android 8.0 (Oreo)",
|
||||
29 to "Android 10 (Q)",
|
||||
31 to "Android 12 (S)",
|
||||
33 to "Android 13 (T)",
|
||||
34 to "Android 14 (U)"
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// 2. Logic Engines (Replica of App Code)
|
||||
// ==========================================
|
||||
|
||||
object SimShiftManager {
|
||||
// Jeonju Logic
|
||||
val JEONJU_CYCLE = listOf(
|
||||
"석간", "석간", "석간", "휴무", "휴무",
|
||||
"주간", "주간", "주간", "주간", "주간", "휴무", "휴무",
|
||||
"야간", "야간", "야간", "야간", "야간", "휴무",
|
||||
"석간", "석간"
|
||||
)
|
||||
val JEONJU_BASE = LocalDate.of(2026, 2, 1)
|
||||
val TEAM_OFFSETS = mapOf("A" to 0, "B" to 15, "C" to 10, "D" to 5)
|
||||
|
||||
fun getJeonju(date: LocalDate, team: String): String {
|
||||
val offset = TEAM_OFFSETS[team] ?: 0
|
||||
val days = ChronoUnit.DAYS.between(JEONJU_BASE, date).toInt()
|
||||
val index = Math.floorMod(days + offset, JEONJU_CYCLE.size)
|
||||
return JEONJU_CYCLE[index]
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. Android API Simulation Logic
|
||||
// ==========================================
|
||||
|
||||
fun simulateAlarmBehavior(sdkInt: Int, alarmTime: String): String {
|
||||
// Core Logic: We use AlarmManager.setAlarmClock across ALL versions for maximum reliability
|
||||
// (As confirmed in AlarmUtils.kt implementation)
|
||||
var method = "AlarmManager.setAlarmClock"
|
||||
var desc = "Reliable, shows icon"
|
||||
|
||||
// Permission Check
|
||||
var perms = "None"
|
||||
if (sdkInt >= 31) { // Android 12+
|
||||
// SCHEDULE_EXACT_ALARM is required for exact alarms
|
||||
perms = "SCHEDULE_EXACT_ALARM"
|
||||
}
|
||||
if (sdkInt >= 33) { // Android 13+
|
||||
// Include Notification Permission
|
||||
perms += ", POST_NOTIFICATIONS"
|
||||
}
|
||||
|
||||
// PendingIntent Flags
|
||||
var flags = "FLAG_UPDATE_CURRENT"
|
||||
if (sdkInt >= 23) { // Android 6.0+
|
||||
flags += " | FLAG_IMMUTABLE" // Mandatory on Android 12+, Good practice on M+
|
||||
}
|
||||
|
||||
// Doze Mode
|
||||
var doze = "Wakes Device (Idle)"
|
||||
if (method == "AlarmManager.setAlarmClock") {
|
||||
doze = "Bypasses Doze Mode (Highest Priority)"
|
||||
}
|
||||
|
||||
return "Using $method ($desc) | Flags: $flags | Perms: $perms | Doze: $doze"
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. Runner
|
||||
// ==========================================
|
||||
|
||||
@Test
|
||||
fun runCrossVersionSimulation() {
|
||||
val outputFile = File("CROSS_VERSION_REPORT.md")
|
||||
val writer = outputFile.bufferedWriter(StandardCharsets.UTF_8)
|
||||
|
||||
writer.write("# 📱 Cross-Version Alarm Accuracy Report (Android 8.0 ~ 14)\n")
|
||||
writer.write("**Period**: 2026-02-01 ~ 2026-03-31\n")
|
||||
writer.write("**Scenario**: Jeonju Factory - Team C (Standard)\n\n")
|
||||
|
||||
writer.write("| Date | Shift | Alarm Time | Android Version | Simulation Result (API Behavior) |\n")
|
||||
writer.write("|---|---|---|---|---|\n")
|
||||
|
||||
var curr = START_DATE
|
||||
val factory = "Jeonju"
|
||||
val team = "C"
|
||||
|
||||
while (!curr.isAfter(END_DATE)) {
|
||||
val shift = SimShiftManager.getJeonju(curr, team)
|
||||
|
||||
// Determine Alarm Time
|
||||
var time: String? = null
|
||||
if (shift == "주간") time = "06:00"
|
||||
else if (shift == "석간") time = "14:00"
|
||||
else if (shift == "야간") time = "22:00"
|
||||
|
||||
if (time != null) {
|
||||
// For each day, test against ALL target Android versions
|
||||
for ((sdk, verName) in TARGET_VERSIONS) {
|
||||
val behavior = simulateAlarmBehavior(sdk, time)
|
||||
writer.write("| $curr | $shift | **$time** | $verName | $behavior |\n")
|
||||
}
|
||||
} else {
|
||||
// write just one line for OFF day to reduce noise, or skip?
|
||||
// Let's print one line for clarity that logic works
|
||||
writer.write("| $curr | $shift | - | All Versions | No Alarm Scheduled (OFF) |\n")
|
||||
}
|
||||
curr = curr.plusDays(1)
|
||||
}
|
||||
|
||||
writer.close()
|
||||
println("Cross-Version Report Generated at CROSS_VERSION_REPORT.md")
|
||||
}
|
||||
}
|
||||
231
app/src/test/java/com/example/shiftalarm/SimulationTest.kt
Normal file
231
app/src/test/java/com/example/shiftalarm/SimulationTest.kt
Normal file
@@ -0,0 +1,231 @@
|
||||
|
||||
package com.example.shiftalarm
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDate
|
||||
import java.time.DayOfWeek
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
class SimulationTest {
|
||||
|
||||
// ==========================================
|
||||
// 1. Simulation Configuration
|
||||
// ==========================================
|
||||
val START_DATE = LocalDate.of(2026, 2, 1)
|
||||
val END_DATE = LocalDate.of(2026, 3, 31)
|
||||
|
||||
// Holidays (Mock)
|
||||
val HOLIDAYS = setOf(
|
||||
LocalDate.of(2026, 2, 11), // Mock Holiday
|
||||
LocalDate.of(2026, 3, 1) // Samiljeol
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// 2. Logic (Duplicated for Safety in Unit Test)
|
||||
// ==========================================
|
||||
|
||||
// Internal Logic Simulating ShiftCalculator
|
||||
object SimShiftCalculator {
|
||||
val JEONJU_CYCLE = listOf(
|
||||
"석간", "석간", "석간", "휴무", "휴무",
|
||||
"주간", "주간", "주간", "주간", "주간", "휴무", "휴무",
|
||||
"야간", "야간", "야간", "야간", "야간", "휴무",
|
||||
"석간", "석간"
|
||||
)
|
||||
val JEONJU_BASE_DATE = LocalDate.of(2026, 2, 1) // Verify base date usage in actual code
|
||||
|
||||
val TEAM_OFFSETS = mapOf("A" to 0, "B" to 15, "C" to 10, "D" to 5)
|
||||
|
||||
fun getJeonjuShift(date: LocalDate, team: String): String {
|
||||
val offset = TEAM_OFFSETS[team] ?: 0
|
||||
val days = ChronoUnit.DAYS.between(JEONJU_BASE_DATE, date).toInt()
|
||||
val index = Math.floorMod(days + offset, JEONJU_CYCLE.size)
|
||||
return JEONJU_CYCLE[index]
|
||||
}
|
||||
|
||||
fun getNonsanShift(date: LocalDate, team: String): String {
|
||||
val dayOfWeek = date.dayOfWeek
|
||||
if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) return "휴무"
|
||||
|
||||
val baseDate = LocalDate.of(2026, 2, 9)
|
||||
val alignedDate = date.minusDays((dayOfWeek.value - 1).toLong())
|
||||
val weeksPassed = ChronoUnit.WEEKS.between(baseDate, alignedDate).toInt()
|
||||
|
||||
val rotation = listOf("주간", "야간", "석간")
|
||||
val startOffset = when (team) {
|
||||
"A" -> 0; "B" -> 1; "C" -> 2; else -> 0
|
||||
}
|
||||
val index = Math.floorMod(startOffset + weeksPassed, 3)
|
||||
return rotation[index]
|
||||
}
|
||||
}
|
||||
|
||||
// Internal Logic Simulating ShiftAlarmDefaults values
|
||||
fun getDefaultAlarmTime(shift: String, factory: String): String? {
|
||||
if (!listOf("주간", "석간", "야간", "야간 맞교대", "주간 맞교대").contains(shift)) return null
|
||||
|
||||
return if (factory == "Nonsan") {
|
||||
when (shift) {
|
||||
"주간" -> "07:00"
|
||||
"석간" -> "15:00"
|
||||
"야간" -> "23:00"
|
||||
"야간 맞교대" -> "19:00"
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
when (shift) {
|
||||
"주간" -> "06:00"
|
||||
"석간" -> "14:00"
|
||||
"야간" -> "22:00"
|
||||
"야간 맞교대" -> "18:00"
|
||||
"주간 맞교대" -> "06:00"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. User Data Structures
|
||||
// ==========================================
|
||||
|
||||
data class UserSettings(
|
||||
val factory: String,
|
||||
val team: String,
|
||||
val customShifts: Map<LocalDate, String> = emptyMap(),
|
||||
val customAlarmTimes: Map<LocalDate, String> = emptyMap(),
|
||||
val customShiftAlarmTimes: Map<String, String> = emptyMap(),
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class SimResult(
|
||||
val date: LocalDate,
|
||||
val shift: String,
|
||||
val alarmTime: String?,
|
||||
val isHoliday: Boolean,
|
||||
val notes: String
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// 4. Engine
|
||||
// ==========================================
|
||||
|
||||
fun runSimulation(user: UserSettings): List<SimResult> {
|
||||
val results = mutableListOf<SimResult>()
|
||||
var currentDate = START_DATE
|
||||
while (!currentDate.isAfter(END_DATE)) {
|
||||
var shift = if (user.factory == "Nonsan") {
|
||||
SimShiftCalculator.getNonsanShift(currentDate, user.team)
|
||||
} else {
|
||||
SimShiftCalculator.getJeonjuShift(currentDate, user.team)
|
||||
}
|
||||
|
||||
var notes = ""
|
||||
if (user.customShifts.containsKey(currentDate)) {
|
||||
shift = user.customShifts[currentDate]!!
|
||||
notes += "[Manual Override] "
|
||||
}
|
||||
|
||||
var alarmTime: String? = null
|
||||
if (shift == "휴무" || shift == "비번") {
|
||||
alarmTime = null
|
||||
} else {
|
||||
alarmTime = getDefaultAlarmTime(shift, user.factory)
|
||||
|
||||
if (user.customShiftAlarmTimes.containsKey(shift)) {
|
||||
alarmTime = user.customShiftAlarmTimes[shift]
|
||||
notes += "[Shift Rule] "
|
||||
}
|
||||
|
||||
if (user.customAlarmTimes.containsKey(currentDate)) {
|
||||
alarmTime = user.customAlarmTimes[currentDate]
|
||||
notes += "[Date Rule] "
|
||||
}
|
||||
}
|
||||
|
||||
val isHoliday = HOLIDAYS.contains(currentDate)
|
||||
if (isHoliday) notes += "HOLIDAY "
|
||||
|
||||
results.add(SimResult(currentDate, shift, alarmTime, isHoliday, notes))
|
||||
currentDate = currentDate.plusDays(1)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 5. Reporting
|
||||
// ==========================================
|
||||
|
||||
fun generateReport(user: UserSettings, results: List<SimResult>): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("\n### Simulation Report: ${user.name} (${user.factory} - Team ${user.team})\n")
|
||||
sb.append("| Date | Day | Shift | Alarm Time | Status | Logic Check |\n")
|
||||
sb.append("|---|---|---|---|---|---|\n")
|
||||
|
||||
val dayNames = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
|
||||
|
||||
for (r in results) {
|
||||
val dayOfW = r.date.dayOfWeek.value
|
||||
// 1=Mon...7=Sun.
|
||||
// list index 0=Sun, 6=Sat.
|
||||
// 1 % 7 = 1 (Mon). 7 % 7 = 0 (Sun). Correct.
|
||||
val dayStr = dayNames[dayOfW % 7]
|
||||
|
||||
val alarmStr = r.alarmTime ?: "-"
|
||||
val status = if (r.alarmTime != null) "ON" else "OFF"
|
||||
|
||||
var check = "OK"
|
||||
if (r.shift == "주간" && user.factory == "Jeonju" && r.alarmTime == "06:00") check = "Valid (Default)"
|
||||
if (r.shift == "주간" && user.factory == "Nonsan" && r.alarmTime == "07:00") check = "Valid (Nonsan)"
|
||||
|
||||
sb.append("| ${r.date} | $dayStr | ${r.shift} | $alarmStr | $status | $check ${r.notes} |\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runFullSimulation() {
|
||||
val outputFile = File("simulation_result.md")
|
||||
// Use UTF-8 explicitly
|
||||
val writer = outputFile.bufferedWriter(StandardCharsets.UTF_8)
|
||||
|
||||
writer.write("# ShiftRing Alarm Logic Simulation V2 (Robust)\n")
|
||||
writer.write("Period: $START_DATE ~ $END_DATE\n")
|
||||
|
||||
// Scenario A: Jeonju C
|
||||
val userJeonju = UserSettings("Jeonju", "C", name = "User 1 (Jeonju-C Standard)")
|
||||
val resA = runSimulation(userJeonju)
|
||||
val reportA = generateReport(userJeonju, resA)
|
||||
writer.write(reportA)
|
||||
println(reportA)
|
||||
|
||||
// Scenario B: Nonsan A
|
||||
val userNonsan = UserSettings("Nonsan", "A", name = "User 2 (Nonsan-A Standard)")
|
||||
val resB = runSimulation(userNonsan)
|
||||
val reportB = generateReport(userNonsan, resB)
|
||||
writer.write(reportB)
|
||||
println(reportB)
|
||||
|
||||
// Scenario C: Custom
|
||||
val userCustom = UserSettings(
|
||||
factory = "Jeonju",
|
||||
team = "A",
|
||||
customShifts = mapOf(
|
||||
LocalDate.of(2026, 2, 14) to "휴무",
|
||||
LocalDate.of(2026, 2, 15) to "주간"
|
||||
),
|
||||
customAlarmTimes = mapOf(
|
||||
LocalDate.of(2026, 2, 20) to "05:00"
|
||||
),
|
||||
name = "User 3 (Jeonju-A Customizer)"
|
||||
)
|
||||
val resC = runSimulation(userCustom)
|
||||
val reportC = generateReport(userCustom, resC)
|
||||
writer.write(reportC)
|
||||
println(reportC)
|
||||
|
||||
writer.close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user