Skip to content

Commit d7e8126

Browse files
committed
Add SubjectRecurrenceTest
1 parent e7f27ae commit d7e8126

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright (C) 2024-2025 OpenAni and contributors.
3+
*
4+
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
5+
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
6+
*
7+
* https://github.com/open-ani/ani/blob/main/LICENSE
8+
*/
9+
10+
package me.him188.ani.app.data.models.subject
11+
12+
import kotlinx.datetime.DatePeriod
13+
import kotlinx.datetime.Instant
14+
import kotlinx.datetime.LocalDate
15+
import kotlinx.datetime.TimeZone
16+
import kotlinx.datetime.atStartOfDayIn
17+
import kotlinx.datetime.plus
18+
import kotlinx.datetime.toLocalDateTime
19+
import me.him188.ani.datasources.api.PackedDate
20+
import kotlin.test.Test
21+
import kotlin.test.assertEquals
22+
import kotlin.test.assertNull
23+
import kotlin.time.Duration
24+
import kotlin.time.Duration.Companion.days
25+
26+
27+
/**
28+
* Tests for [SubjectRecurrence.calculateEpisodeAirTime].
29+
*/
30+
class SubjectRecurrenceTest {
31+
private val zone: TimeZone = TimeZone.UTC
32+
33+
/** Helper to pack a [LocalDate] in the same way production code expects. */
34+
private fun LocalDate.packed(): PackedDate = PackedDate.parseFromDate(this.toString())
35+
36+
/** Short alias: add whole–day durations to an [Instant]. */
37+
private operator fun Instant.plus(days: Int) = this + days.days
38+
39+
// -------------------------------------------------------------------------
40+
// Happy‑path cases
41+
// -------------------------------------------------------------------------
42+
43+
@Test
44+
fun `exact-date - first-episode - returns-startTime`() {
45+
val startDate = LocalDate(2025, 1, 3)
46+
val startInstant = startDate.atStartOfDayIn(zone)
47+
48+
val sut = SubjectRecurrence(
49+
startTime = startInstant,
50+
interval = 7.days, // weekly
51+
)
52+
53+
val result = sut.calculateEpisodeAirTime(startDate.packed())
54+
55+
assertEquals(startInstant, result, "Should return the first airing instant")
56+
}
57+
58+
@Test
59+
fun `exact-date - n-th-episode - returns-correct-instant`() {
60+
val start = Instant.parse("2025-01-01T12:00:00Z") // midday for safer TZ maths
61+
val interval = 7.days
62+
val episodesToSkip = 5L // ask for episode #5
63+
64+
val expectedInstant = start + interval * episodesToSkip.toInt()
65+
val wantedDate = expectedInstant.toLocalDateTime(zone).date
66+
67+
val sut = SubjectRecurrence(start, interval)
68+
69+
val result = sut.calculateEpisodeAirTime(wantedDate.packed())
70+
71+
assertEquals(expectedInstant, result, "Should return the n‑th episode instant")
72+
}
73+
74+
@Test
75+
fun `off-by-one-day - still-within-tolerance`() {
76+
val start = LocalDate(2024, 6, 1)
77+
val startInstant = start.atStartOfDayIn(zone)
78+
val sut = SubjectRecurrence(startInstant, 14.days) // bi‑weekly show
79+
80+
val targetDate = start + DatePeriod(days = 1) // 24 h after first episode
81+
val result = sut.calculateEpisodeAirTime(targetDate.packed())
82+
83+
assertEquals(startInstant, result, "±1 day should still map to the first episode")
84+
}
85+
86+
// -------------------------------------------------------------------------
87+
// Error / edge‑cases
88+
// -------------------------------------------------------------------------
89+
90+
@Test
91+
fun `date-before-start - returns-null`() {
92+
val startInstant = Instant.parse("2025-03-10T00:00:00Z")
93+
val sut = SubjectRecurrence(startInstant, 7.days)
94+
95+
val dayBefore = LocalDate(2025, 3, 9).packed()
96+
assertNull(sut.calculateEpisodeAirTime(dayBefore), "Dates before premiere must be null")
97+
}
98+
99+
@Test
100+
fun `outside-24h-window - returns-null`() {
101+
val startInstant = Instant.parse("2025-02-01T00:00:00Z")
102+
val sut = SubjectRecurrence(startInstant, 7.days)
103+
104+
// Two days after the first episode -> outside tolerance
105+
val farDate = (startInstant + 2).toLocalDateTime(zone).date.packed()
106+
assertNull(sut.calculateEpisodeAirTime(farDate), "More than ±24 h should not match")
107+
}
108+
109+
@Test
110+
fun `zero-or-negative-interval - returns-null`() {
111+
val sutZero = SubjectRecurrence(
112+
startTime = Instant.parse("2025-05-01T00:00:00Z"),
113+
interval = Duration.ZERO,
114+
)
115+
val sutNegative = SubjectRecurrence(
116+
startTime = Instant.parse("2025-05-01T00:00:00Z"),
117+
interval = (-7).days,
118+
)
119+
val anyDate = LocalDate(2025, 5, 1).packed()
120+
121+
assertNull(sutZero.calculateEpisodeAirTime(anyDate), "Zero interval is invalid")
122+
assertNull(sutNegative.calculateEpisodeAirTime(anyDate), "Negative interval is invalid")
123+
}
124+
}

0 commit comments

Comments
 (0)