@@ -22,6 +22,9 @@ import cz.vutbr.fit.interlockSim.sim.LoopProcess
2222import cz.vutbr.fit.interlockSim.sim.SpeedControllable
2323import io.mockk.every
2424import io.mockk.mockk
25+ import io.mockk.unmockkAll
26+ import org.junit.jupiter.api.AfterEach
27+ import org.junit.jupiter.api.BeforeEach
2528import org.junit.jupiter.api.DisplayName
2629import org.junit.jupiter.api.Test
2730import org.junit.jupiter.api.Timeout
@@ -33,6 +36,16 @@ import java.util.concurrent.TimeUnit
3336 * `SimulationController.setSpeed` must reach the running main process when it
3437 * implements [SpeedControllable], not just the bookkeeping field on
3538 * [SimulationRunner]. Without this, speed buttons are dead in `exampleGui` mode.
39+ *
40+ * ## Synchronisation design
41+ * Each test gets its own [startedLatch] and [blockSim] latches (instance fields,
42+ * not companion-object shared state). [blockSim] is counted down in [tearDown]
43+ * even if the test fails early, so the sim thread always unblocks and daemon threads
44+ * do not linger across tests.
45+ *
46+ * The 30-second timeouts accommodate busy CI runners where thread scheduling can be
47+ * delayed by several seconds (CI uses `maxParallelForks = availableProcessors()`
48+ * with multiple JVM forks competing for CPU).
3649 */
3750@DisplayName(" SimulationController -> SpeedControllable propagation" )
3851class SimulationControllerSpeedPropagationTest {
@@ -51,44 +64,59 @@ class SimulationControllerSpeedPropagationTest {
5164 override suspend fun iteration () = Unit
5265 }
5366
67+ // ── Per-test latches (instance fields, NOT companion-object shared state) ───
68+ // JUnit 5 creates a fresh test instance per method, so these are naturally
69+ // isolated. tearDown() guarantees blockSim is released even on test failure,
70+ // preventing lingering sim threads from other tests.
71+ private lateinit var startedLatch: CountDownLatch
72+ private lateinit var blockSim: CountDownLatch
73+
74+ @BeforeEach
75+ fun setUp () {
76+ startedLatch = CountDownLatch (1 )
77+ blockSim = CountDownLatch (1 )
78+ }
79+
80+ @AfterEach
81+ fun tearDown () {
82+ // Guarantee the sim thread unblocks even if the test fails before releaseSim().
83+ blockSim.countDown()
84+ unmockkAll()
85+ }
86+
5487 private fun newController (): SimulationController = SimulationController ()
5588
56- private fun mockContext (mainProcess : LoopProcess ? ): DefaultSimulationContext {
57- val started = CountDownLatch (1 )
58- val blockSim = CountDownLatch (1 )
59- return mockk<DefaultSimulationContext >(relaxed = true ).also { ctx ->
89+ private fun mockContext (mainProcess : LoopProcess ? ): DefaultSimulationContext =
90+ mockk<DefaultSimulationContext >(relaxed = true ).also { ctx ->
6091 every { ctx.getMainProcess() } returns mainProcess
6192 every { ctx.run () } answers {
62- started .countDown()
63- blockSim.await(10 , TimeUnit .SECONDS )
93+ startedLatch .countDown()
94+ blockSim.await(30 , TimeUnit .SECONDS )
6495 }
65- // Stash the latches on the mock for the test to access.
66- ctx.attachLatches(started, blockSim)
6796 }
68- }
6997
7098 @Test
71- @Timeout(value = 10 , unit = TimeUnit .SECONDS )
99+ @Timeout(value = 30 , unit = TimeUnit .SECONDS )
72100 @DisplayName(" setSpeed propagates to a SpeedControllable main process while running" )
73101 fun setSpeedReachesSpeedControllable () {
74102 val process = FakeSpeedyMainProcess ()
75103 val ctx = mockContext(process)
76104 val controller = newController()
77105
78106 controller.start(ctx)
79- assertThat(ctx. startedLatch() .await(5 , TimeUnit .SECONDS )).isTrue()
107+ assertThat(startedLatch.await(30 , TimeUnit .SECONDS )).isTrue()
80108
81109 controller.setSpeed(2.5 )
82110
83111 assertThat(process.speedMultiplier).isEqualTo(2.5 )
84112 assertThat(controller.runner!! .speedMultiplier).isEqualTo(2.5 )
85113
86- ctx.releaseSim ()
114+ blockSim.countDown ()
87115 controller.stop()
88116 }
89117
90118 @Test
91- @Timeout(value = 10 , unit = TimeUnit .SECONDS )
119+ @Timeout(value = 30 , unit = TimeUnit .SECONDS )
92120 @DisplayName(" setSpeed before start applies on next start (desiredSpeed propagation)" )
93121 fun preStartSpeedAppliedOnStart () {
94122 val process = FakeSpeedyMainProcess ()
@@ -97,63 +125,50 @@ class SimulationControllerSpeedPropagationTest {
97125
98126 controller.setSpeed(0.5 )
99127 controller.start(ctx)
100- assertThat(ctx. startedLatch() .await(5 , TimeUnit .SECONDS )).isTrue()
128+ assertThat(startedLatch.await(30 , TimeUnit .SECONDS )).isTrue()
101129
102130 assertThat(process.speedMultiplier).isEqualTo(0.5 )
103131 assertThat(controller.runner!! .speedMultiplier).isEqualTo(0.5 )
104132
105- ctx.releaseSim ()
133+ blockSim.countDown ()
106134 controller.stop()
107135 }
108136
109137 @Test
110- @Timeout(value = 10 , unit = TimeUnit .SECONDS )
138+ @Timeout(value = 30 , unit = TimeUnit .SECONDS )
111139 @DisplayName(" setSpeed is a safe no-op when main process is not SpeedControllable" )
112140 fun nonControllableMainProcessNoOp () {
113141 val ctx = mockContext(PlainMainProcess ())
114142 val controller = newController()
115143
116144 controller.start(ctx)
117- assertThat(ctx. startedLatch() .await(5 , TimeUnit .SECONDS )).isTrue()
145+ assertThat(startedLatch.await(30 , TimeUnit .SECONDS )).isTrue()
118146
119147 controller.setSpeed(3.0 ) // must not throw, must not crash on the cast
120148 assertThat(controller.runner!! .speedMultiplier).isEqualTo(3.0 )
121149
122- ctx.releaseSim ()
150+ blockSim.countDown ()
123151 controller.stop()
124152 }
125153
126154 @Test
127- @Timeout(value = 10 , unit = TimeUnit .SECONDS )
155+ @Timeout(value = 30 , unit = TimeUnit .SECONDS )
128156 @DisplayName(" setSpeed is a safe no-op when main process is null" )
129157 fun nullMainProcessNoOp () {
130158 val ctx = mockContext(null )
131159 val controller = newController()
132160
133161 controller.start(ctx)
134- // runner and speedMultiplier are assigned synchronously inside start() before
135- // the sim thread is launched, so no need to await the sim thread here.
162+ // Wait for the sim thread to actually start and block on blockSim.await()
163+ // before we call setSpeed() and assert on runner.speedMultiplier.
164+ // On a busy CI runner, thread scheduling can delay the sim thread by
165+ // several seconds — without this await the test is non-deterministic.
166+ assertThat(startedLatch.await(30 , TimeUnit .SECONDS )).isTrue()
167+
136168 controller.setSpeed(1.5 )
137169 assertThat(controller.runner!! .speedMultiplier).isEqualTo(1.5 )
138170
139- ctx.releaseSim ()
171+ blockSim.countDown ()
140172 controller.stop()
141173 }
142-
143- // ── latch wiring kept off the production type ────────────────────────────
144- // Stores per-test latches via a side-table keyed on the mock instance, so
145- // production DefaultSimulationContext stays unchanged.
146- companion object {
147- private val latches = java.util.IdentityHashMap <DefaultSimulationContext , Pair <CountDownLatch , CountDownLatch >>()
148- }
149-
150- private fun DefaultSimulationContext.attachLatches (started : CountDownLatch , block : CountDownLatch ) {
151- latches[this ] = started to block
152- }
153-
154- private fun DefaultSimulationContext.startedLatch (): CountDownLatch = latches.getValue(this ).first
155-
156- private fun DefaultSimulationContext.releaseSim () {
157- latches.getValue(this ).second.countDown()
158- }
159174}
0 commit comments