Skip to content

Commit a7a543b

Browse files
test: #180 add unit and integration tests for ResettableTimer functionality
1 parent 4ae00d4 commit a7a543b

File tree

2 files changed

+181
-10
lines changed

2 files changed

+181
-10
lines changed

openspec/changes/issue-180-refactor-nanotimer-thread-reuse/tasks.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@
3333

3434
### Phase 4: Testing
3535

36-
- [ ] Add unit tests for `ResettableTimer`:
37-
- [ ] Test max delay execution
38-
- [ ] Test notifyReady() before min delay (should wait)
39-
- [ ] Test notifyReady() after min delay (should execute immediately)
40-
- [ ] Test cancel() prevents execution
41-
- [ ] Test pause/resume timing
42-
- [ ] Test shutdown cleanup
43-
- [ ] Add integration test: run 10000 turns, verify single thread
44-
- [ ] Add memory test: verify no thread count growth over time
45-
- [ ] Update any existing timing-related tests
36+
- [x] Add unit tests for `ResettableTimer`:
37+
- [x] Test max delay execution
38+
- [x] Test notifyReady() before min delay (should wait)
39+
- [x] Test notifyReady() after min delay (should execute immediately)
40+
- [x] Test cancel() prevents execution
41+
- [x] Test pause/resume timing
42+
- [x] Test shutdown cleanup
43+
- [x] Add integration test: run 10000 turns, verify single thread
44+
- [x] Add memory test: verify no thread count growth over time
45+
- [x] Update any existing timing-related tests (no other timing tests found)
4646

4747
### Phase 5: Validation
4848

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package core
2+
3+
import dev.robocode.tankroyale.server.core.ResettableTimer
4+
import io.kotest.core.spec.style.FunSpec
5+
import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual
6+
import io.kotest.matchers.longs.shouldBeLessThan
7+
import io.kotest.matchers.shouldBe
8+
import java.util.concurrent.CountDownLatch
9+
import java.util.concurrent.TimeUnit
10+
import java.util.concurrent.atomic.AtomicLong
11+
import java.util.concurrent.atomic.AtomicReference
12+
13+
14+
class ResettableTimerTest : FunSpec({
15+
16+
val toleranceNanos = TimeUnit.MILLISECONDS.toNanos(25)
17+
18+
test("executes after max delay when not notified") {
19+
val executedAt = AtomicLong(0L)
20+
val latch = CountDownLatch(1)
21+
val timer = ResettableTimer {
22+
executedAt.set(System.nanoTime())
23+
latch.countDown()
24+
}
25+
26+
val maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(120)
27+
val startTime = System.nanoTime()
28+
29+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = maxDelayNanos)
30+
31+
latch.await(800, TimeUnit.MILLISECONDS) shouldBe true
32+
33+
val elapsed = executedAt.get() - startTime
34+
elapsed shouldBeGreaterThanOrEqual (maxDelayNanos - toleranceNanos)
35+
elapsed shouldBeLessThan (maxDelayNanos + TimeUnit.MILLISECONDS.toNanos(250))
36+
37+
timer.shutdown()
38+
}
39+
40+
test("notifyReady before min delay waits for min delay") {
41+
val executedAt = AtomicLong(0L)
42+
val latch = CountDownLatch(1)
43+
val timer = ResettableTimer {
44+
executedAt.set(System.nanoTime())
45+
latch.countDown()
46+
}
47+
48+
val minDelayNanos = TimeUnit.MILLISECONDS.toNanos(140)
49+
val maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(400)
50+
val startTime = System.nanoTime()
51+
52+
timer.schedule(minDelayNanos = minDelayNanos, maxDelayNanos = maxDelayNanos)
53+
timer.notifyReady()
54+
55+
latch.await(900, TimeUnit.MILLISECONDS) shouldBe true
56+
57+
val elapsed = executedAt.get() - startTime
58+
elapsed shouldBeGreaterThanOrEqual (minDelayNanos - toleranceNanos)
59+
elapsed shouldBeLessThan (maxDelayNanos + TimeUnit.MILLISECONDS.toNanos(250))
60+
61+
timer.shutdown()
62+
}
63+
64+
test("notifyReady after min delay executes promptly") {
65+
val executedAt = AtomicLong(0L)
66+
val latch = CountDownLatch(1)
67+
val timer = ResettableTimer {
68+
executedAt.set(System.nanoTime())
69+
latch.countDown()
70+
}
71+
72+
val minDelayNanos = TimeUnit.MILLISECONDS.toNanos(60)
73+
val maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(500)
74+
val startTime = System.nanoTime()
75+
76+
timer.schedule(minDelayNanos = minDelayNanos, maxDelayNanos = maxDelayNanos)
77+
Thread.sleep(90)
78+
val notifyTime = System.nanoTime()
79+
timer.notifyReady()
80+
81+
latch.await(700, TimeUnit.MILLISECONDS) shouldBe true
82+
83+
val elapsed = executedAt.get() - startTime
84+
val notifyElapsed = executedAt.get() - notifyTime
85+
elapsed shouldBeGreaterThanOrEqual (minDelayNanos - toleranceNanos)
86+
notifyElapsed shouldBeLessThan (TimeUnit.MILLISECONDS.toNanos(140))
87+
88+
timer.shutdown()
89+
}
90+
91+
test("cancel prevents execution") {
92+
val latch = CountDownLatch(1)
93+
val timer = ResettableTimer { latch.countDown() }
94+
95+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(120))
96+
timer.cancel()
97+
98+
latch.await(250, TimeUnit.MILLISECONDS) shouldBe false
99+
100+
timer.shutdown()
101+
}
102+
103+
test("pause and resume excludes paused time") {
104+
val executedAt = AtomicLong(0L)
105+
val latch = CountDownLatch(1)
106+
val timer = ResettableTimer {
107+
executedAt.set(System.nanoTime())
108+
latch.countDown()
109+
}
110+
111+
val maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(200)
112+
val pauseMillis = 140L
113+
val startTime = System.nanoTime()
114+
115+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = maxDelayNanos)
116+
Thread.sleep(60)
117+
timer.pause()
118+
119+
latch.await(150, TimeUnit.MILLISECONDS) shouldBe false
120+
121+
Thread.sleep(pauseMillis)
122+
timer.resume()
123+
124+
latch.await(600, TimeUnit.MILLISECONDS) shouldBe true
125+
126+
val elapsed = executedAt.get() - startTime
127+
val expectedMinElapsed = maxDelayNanos + TimeUnit.MILLISECONDS.toNanos(pauseMillis) - toleranceNanos
128+
129+
elapsed shouldBeGreaterThanOrEqual expectedMinElapsed
130+
elapsed shouldBeLessThan (maxDelayNanos + TimeUnit.MILLISECONDS.toNanos(pauseMillis + 250))
131+
132+
timer.shutdown()
133+
}
134+
135+
test("shutdown stops execution") {
136+
val latch = CountDownLatch(1)
137+
val timer = ResettableTimer { latch.countDown() }
138+
139+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = TimeUnit.MILLISECONDS.toNanos(200))
140+
timer.shutdown()
141+
142+
latch.await(300, TimeUnit.MILLISECONDS) shouldBe false
143+
}
144+
145+
test("reuses a single timer thread across many schedules") {
146+
val latchRef = AtomicReference(CountDownLatch(0))
147+
val timer = ResettableTimer { latchRef.get().countDown() }
148+
149+
val firstLatch = CountDownLatch(1)
150+
latchRef.set(firstLatch)
151+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = 0L)
152+
firstLatch.await(1, TimeUnit.SECONDS) shouldBe true
153+
154+
val baselineCount = countTimerThreads()
155+
156+
repeat(9_999) {
157+
val latch = CountDownLatch(1)
158+
latchRef.set(latch)
159+
timer.schedule(minDelayNanos = 0L, maxDelayNanos = 0L)
160+
latch.await(1, TimeUnit.SECONDS) shouldBe true
161+
}
162+
163+
val afterCount = countTimerThreads()
164+
afterCount shouldBe baselineCount
165+
166+
timer.shutdown()
167+
}
168+
})
169+
170+
private fun countTimerThreads(): Int =
171+
Thread.getAllStackTraces().keys.count { it.isAlive && it.name == "TurnTimeoutTimer" }

0 commit comments

Comments
 (0)