|
24 | 24 | */
|
25 | 25 | package com.iluwatar.twin;
|
26 | 26 |
|
27 |
| -import static org.junit.jupiter.api.Assertions.assertEquals; |
28 |
| -import static org.junit.jupiter.api.Assertions.assertFalse; |
29 |
| -import static org.junit.jupiter.api.Assertions.assertNotNull; |
30 |
| -import static org.junit.jupiter.api.Assertions.assertTrue; |
31 |
| -import static org.mockito.Mockito.atLeast; |
| 27 | +import static java.lang.Thread.sleep; |
| 28 | +import static java.time.Duration.ofMillis; |
| 29 | +import static org.junit.jupiter.api.Assertions.assertTimeout; |
| 30 | +import static org.mockito.Mockito.atLeastOnce; |
32 | 31 | import static org.mockito.Mockito.mock;
|
33 | 32 | import static org.mockito.Mockito.reset;
|
34 |
| -import static org.mockito.Mockito.verifyNoInteractions; |
| 33 | +import static org.mockito.Mockito.verify; |
| 34 | +import static org.mockito.Mockito.verifyNoMoreInteractions; |
35 | 35 |
|
36 | 36 | import java.util.concurrent.TimeUnit;
|
37 | 37 | import org.junit.jupiter.api.Test;
|
38 |
| -import org.junit.jupiter.api.Timeout; |
39 | 38 |
|
40 | 39 | /** BallThreadTest */
|
41 | 40 | class BallThreadTest {
|
42 | 41 |
|
43 |
| - /** Verify if the {@link BallThread} can be resumed */ |
| 42 | + /** Verify if the {@link BallThread} can be suspended */ |
44 | 43 | @Test
|
45 | 44 | void testSuspend() {
|
46 | 45 | assertTimeout(
|
47 | 46 | ofMillis(5000),
|
48 | 47 | () -> {
|
49 | 48 | final var ballThread = new BallThread();
|
50 |
| - |
51 | 49 | final var ballItem = mock(BallItem.class);
|
52 | 50 | ballThread.setTwin(ballItem);
|
53 | 51 |
|
54 |
| - ballThread.start(); |
| 52 | + // FIXED: Use run() instead of start() for new architecture |
| 53 | + ballThread.run(); |
55 | 54 | sleep(200);
|
56 | 55 | verify(ballItem, atLeastOnce()).draw();
|
57 | 56 | verify(ballItem, atLeastOnce()).move();
|
58 |
| - ballThread.suspendMe(); |
59 | 57 |
|
| 58 | + ballThread.suspendMe(); |
| 59 | + reset(ballItem); // Reset mock to track suspension behavior |
60 | 60 | sleep(1000);
|
61 | 61 |
|
62 | 62 | ballThread.stopMe();
|
63 |
| - ballThread.join(); |
64 |
| - |
| 63 | + // FIXED: Use awaitShutdown() instead of join() |
| 64 | + ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
65 | 65 | verifyNoMoreInteractions(ballItem);
|
66 | 66 | });
|
67 | 67 | }
|
68 | 68 |
|
69 | 69 | /** Verify if the {@link BallThread} can be resumed */
|
70 | 70 | @Test
|
71 |
| - @Timeout(value = 5, unit = TimeUnit.SECONDS) |
72 |
| - void testEventDrivenAnimation() throws InterruptedException { |
73 |
| - // Start the event-driven animation using run() method |
74 |
| - ballThread.run(); |
75 |
| - |
76 |
| - assertTrue(ballThread.isRunning()); |
77 |
| - assertFalse(ballThread.isSuspended()); |
78 |
| - |
79 |
| - // Wait for a few animation cycles (250ms intervals) |
80 |
| - Thread.sleep(800); // ~3 animation cycles |
81 |
| - |
82 |
| - // Verify animation methods were called by scheduler |
83 |
| - verify(mockBallItem, atLeast(2)).draw(); |
84 |
| - verify(mockBallItem, atLeast(2)).move(); |
85 |
| - |
86 |
| - ballThread.stopMe(); |
87 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
88 |
| - |
89 |
| - assertFalse(ballThread.isRunning()); |
90 |
| - } |
91 |
| - |
92 |
| - @Test |
93 |
| - @Timeout(value = 5, unit = TimeUnit.SECONDS) |
94 |
| - void testZeroCpuSuspension() throws InterruptedException { |
95 |
| - ballThread.run(); |
96 |
| - |
97 |
| - // Let it run for a bit |
98 |
| - Thread.sleep(300); |
99 |
| - verify(mockBallItem, atLeast(1)).draw(); |
100 |
| - verify(mockBallItem, atLeast(1)).move(); |
101 |
| - |
102 |
| - // Reset mock to track suspension behavior |
103 |
| - reset(mockBallItem); |
104 |
| - |
105 |
| - // Elite suspension - Zero CPU usage |
106 |
| - ballThread.suspendMe(); |
107 |
| - assertTrue(ballThread.isSuspended()); |
108 |
| - |
109 |
| - // Wait during suspension - should have ZERO CPU usage and no calls |
110 |
| - Thread.sleep(600); |
111 |
| - |
112 |
| - // Verify NO animation occurred during suspension |
113 |
| - verifyNoInteractions(mockBallItem); |
114 |
| - |
115 |
| - ballThread.stopMe(); |
116 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
117 |
| - } |
118 |
| - |
119 |
| - @Test |
120 |
| - @Timeout(value = 5, unit = TimeUnit.SECONDS) |
121 |
| - void testInstantResume() throws InterruptedException { |
122 |
| - // Start suspended |
123 |
| - ballThread.suspendMe(); |
124 |
| - ballThread.run(); |
125 |
| - |
126 |
| - assertTrue(ballThread.isRunning()); |
127 |
| - assertTrue(ballThread.isSuspended()); |
128 |
| - |
129 |
| - // Wait while suspended - no activity expected |
130 |
| - Thread.sleep(500); |
131 |
| - verifyNoInteractions(mockBallItem); |
132 |
| - |
133 |
| - // Instant resume |
134 |
| - ballThread.resumeMe(); |
135 |
| - assertFalse(ballThread.isSuspended()); |
136 |
| - |
137 |
| - // Wait for animation to resume |
138 |
| - Thread.sleep(600); // 2+ animation cycles |
139 |
| - |
140 |
| - // Verify animation resumed |
141 |
| - verify(mockBallItem, atLeast(1)).draw(); |
142 |
| - verify(mockBallItem, atLeast(1)).move(); |
143 |
| - |
144 |
| - ballThread.stopMe(); |
145 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
146 |
| - } |
147 |
| - |
148 |
| - @Test |
149 |
| - @Timeout(value = 5, unit = TimeUnit.SECONDS) |
150 |
| - void testGracefulShutdown() throws InterruptedException { |
151 |
| - ballThread.run(); |
152 |
| - assertTrue(ballThread.isRunning()); |
153 |
| - |
154 |
| - // Let it animate |
155 |
| - Thread.sleep(300); |
156 |
| - verify(mockBallItem, atLeast(1)).draw(); |
157 |
| - |
158 |
| - // Test graceful shutdown |
159 |
| - ballThread.stopMe(); |
160 |
| - |
161 |
| - // Should complete shutdown within timeout |
162 |
| - boolean shutdownCompleted = ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
163 |
| - assertTrue(shutdownCompleted, "Shutdown should complete within timeout"); |
164 |
| - |
165 |
| - assertFalse(ballThread.isRunning()); |
166 |
| - assertFalse(ballThread.isSuspended()); |
167 |
| - } |
168 |
| - |
169 |
| - @Test |
170 |
| - void testPerformanceMetrics() { |
171 |
| - // Test performance monitoring capabilities |
172 |
| - assertFalse(ballThread.isRunning()); |
173 |
| - assertEquals(0, ballThread.getAnimationCycles()); |
174 |
| - assertEquals(0, ballThread.getSuspendCount()); |
175 |
| - assertEquals(4.0, ballThread.getFrameRate(), 0.1); // 1000ms / 250ms = 4 FPS |
176 |
| - |
177 |
| - String report = ballThread.getPerformanceReport(); |
178 |
| - assertNotNull(report); |
179 |
| - assertTrue(report.contains("Event-Driven")); |
180 |
| - assertTrue(report.contains("Zero Busy-Wait")); |
181 |
| - } |
182 |
| - |
183 |
| - @Test |
184 |
| - @Timeout(value = 6, unit = TimeUnit.SECONDS) |
185 |
| - void testMultipleSuspendResumeCycles() throws InterruptedException { |
186 |
| - ballThread.run(); |
187 |
| - |
188 |
| - for (int cycle = 1; cycle <= 3; cycle++) { |
189 |
| - // Run for a bit |
190 |
| - Thread.sleep(200); |
191 |
| - verify(mockBallItem, atLeast(1)).draw(); |
192 |
| - |
193 |
| - // Suspend |
194 |
| - ballThread.suspendMe(); |
195 |
| - assertTrue(ballThread.isSuspended()); |
196 |
| - |
197 |
| - reset(mockBallItem); // Reset to track suspension |
198 |
| - Thread.sleep(200); |
199 |
| - verifyNoInteractions(mockBallItem); // No activity during suspension |
200 |
| - |
201 |
| - // Resume |
202 |
| - ballThread.resumeMe(); |
203 |
| - assertFalse(ballThread.isSuspended()); |
204 |
| - |
205 |
| - // Verify suspend count tracking |
206 |
| - assertEquals(cycle, ballThread.getSuspendCount()); |
207 |
| - } |
208 |
| - |
209 |
| - ballThread.stopMe(); |
210 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
211 |
| - } |
212 |
| - |
213 |
| - @Test |
214 |
| - @Timeout(value = 3, unit = TimeUnit.SECONDS) |
215 |
| - void testNullTwinHandling() throws InterruptedException { |
216 |
| - ballThread.setTwin(null); // Set null twin |
217 |
| - ballThread.run(); |
218 |
| - |
219 |
| - // Should not crash with null twin |
220 |
| - Thread.sleep(500); |
221 |
| - |
222 |
| - assertTrue(ballThread.isRunning()); |
223 |
| - |
224 |
| - ballThread.stopMe(); |
225 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
226 |
| - } |
227 |
| - |
228 |
| - @Test |
229 |
| - @Timeout(value = 5, unit = TimeUnit.SECONDS) |
230 |
| - void testRapidStateChanges() throws InterruptedException { |
231 |
| - ballThread.run(); |
| 71 | + void testResume() { |
| 72 | + assertTimeout( |
| 73 | + ofMillis(5000), |
| 74 | + () -> { |
| 75 | + final var ballThread = new BallThread(); |
| 76 | + final var ballItem = mock(BallItem.class); |
| 77 | + ballThread.setTwin(ballItem); |
232 | 78 |
|
233 |
| - // Rapid suspend/resume cycles |
234 |
| - for (int i = 0; i < 10; i++) { |
235 |
| - ballThread.suspendMe(); |
236 |
| - Thread.sleep(50); |
237 |
| - ballThread.resumeMe(); |
238 |
| - Thread.sleep(50); |
239 |
| - } |
| 79 | + ballThread.suspendMe(); // Suspend before starting |
| 80 | + // 🚀 FIXED: Use run() instead of start() |
| 81 | + ballThread.run(); |
| 82 | + sleep(1000); |
| 83 | + verifyNoMoreInteractions(ballItem); // Should be no activity while suspended |
240 | 84 |
|
241 |
| - // Should handle rapid changes gracefully |
242 |
| - assertTrue(ballThread.isRunning()); |
243 |
| - assertEquals(10, ballThread.getSuspendCount()); |
| 85 | + ballThread.resumeMe(); |
| 86 | + sleep(300); |
| 87 | + verify(ballItem, atLeastOnce()).draw(); |
| 88 | + verify(ballItem, atLeastOnce()).move(); |
244 | 89 |
|
245 |
| - ballThread.stopMe(); |
246 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
| 90 | + ballThread.stopMe(); |
| 91 | + // FIXED: Use awaitShutdown() instead of join() |
| 92 | + ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
| 93 | + }); |
247 | 94 | }
|
248 | 95 |
|
| 96 | + /** |
| 97 | + * UPDATED: Test graceful shutdown instead of interrupt (New architecture doesn't use |
| 98 | + * Thread.interrupt()) |
| 99 | + */ |
249 | 100 | @Test
|
250 |
| - @Timeout(value = 4, unit = TimeUnit.SECONDS) |
251 |
| - void testAnimationTimingAccuracy() throws InterruptedException { |
252 |
| - ballThread.run(); |
253 |
| - |
254 |
| - long startTime = System.currentTimeMillis(); |
255 |
| - |
256 |
| - // Wait for exactly 1 second |
257 |
| - Thread.sleep(1000); |
258 |
| - |
259 |
| - long elapsed = System.currentTimeMillis() - startTime; |
| 101 | + void testGracefulShutdown() { |
| 102 | + assertTimeout( |
| 103 | + ofMillis(5000), |
| 104 | + () -> { |
| 105 | + final var ballThread = new BallThread(); |
| 106 | + final var ballItem = mock(BallItem.class); |
| 107 | + ballThread.setTwin(ballItem); |
260 | 108 |
|
261 |
| - // Should have approximately 4 animation cycles (250ms each) |
262 |
| - verify(mockBallItem, atLeast(3)).draw(); |
| 109 | + // FIXED: Use run() instead of start() |
| 110 | + ballThread.run(); |
| 111 | + sleep(200); // Let it run briefly |
263 | 112 |
|
264 |
| - // Timing should be accurate (not drifting like busy-waiting) |
265 |
| - assertTrue(elapsed >= 1000, "Should not complete too early"); |
266 |
| - assertTrue(elapsed < 1100, "Should not have significant timing drift"); |
| 113 | + verify(ballItem, atLeastOnce()).draw(); |
| 114 | + verify(ballItem, atLeastOnce()).move(); |
267 | 115 |
|
268 |
| - ballThread.stopMe(); |
269 |
| - ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
270 |
| - } |
| 116 | + // NEW: Test graceful shutdown instead of interrupt |
| 117 | + ballThread.stopMe(); |
| 118 | + boolean shutdownCompleted = ballThread.awaitShutdown(3, TimeUnit.SECONDS); |
271 | 119 |
|
272 |
| - // Helper method to create verification with mock |
273 |
| - private static void verify(BallItem mock, org.mockito.verification.VerificationMode mode) { |
274 |
| - org.mockito.Mockito.verify(mock, mode); |
| 120 | + // Verify shutdown completed successfully |
| 121 | + if (!shutdownCompleted) { |
| 122 | + throw new RuntimeException("Shutdown did not complete within timeout"); |
| 123 | + } |
| 124 | + }); |
275 | 125 | }
|
276 | 126 | }
|
0 commit comments