1#include "nightmare/internal.h"
2#include <test/test.h>
3
4#if defined(TEST_ENABLED) && defined(TEST_NIGHTMARE_SMOKE)
5TEST_GROUP_DECLARE(nightmare_liveness);
6
7TEST_DECLARE_UNIT(nightmare_liveness, first_sample_arms) {
8 struct nightmare_liveness_state state = {
9 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
10 .last_progress = 100,
11 .last_change_ms = 1000,
12 .threshold_ms = 3000,
13 };
14
15 bool fired =
16 nightmare_liveness_eval(state: &state, current_progress: 100, now_ms: 1000, false, true, stop: NM_RUN, observer_cpu: 0);
17 TEST_ASSERT(!fired);
18 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
19 TEST_ASSERT_EQ(state.last_change_ms, 1000);
20 TEST_ASSERT_EQ(state.last_progress, 100);
21 return TEST_SUCCESS;
22}
23
24TEST_DECLARE_UNIT(nightmare_liveness, progress_resets_baseline) {
25 struct nightmare_liveness_state state = {
26 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
27 .last_progress = 100,
28 .last_change_ms = 1000,
29 .threshold_ms = 3000,
30 };
31
32 /* Advance time to 2500ms with progress increment from 100 to 105 */
33 bool fired =
34 nightmare_liveness_eval(state: &state, current_progress: 105, now_ms: 2500, false, true, stop: NM_RUN, observer_cpu: 0);
35 TEST_ASSERT(!fired);
36 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
37 TEST_ASSERT_EQ(state.last_change_ms, 2500);
38 TEST_ASSERT_EQ(state.last_progress, 105);
39
40 /* Advance time to 4500ms (2000ms silence since 2500ms) with no progress */
41 fired = nightmare_liveness_eval(state: &state, current_progress: 105, now_ms: 4500, false, true, stop: NM_RUN, observer_cpu: 0);
42 TEST_ASSERT(!fired);
43 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
44 return TEST_SUCCESS;
45}
46
47TEST_DECLARE_UNIT(nightmare_liveness, silence_below_threshold) {
48 struct nightmare_liveness_state state = {
49 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
50 .last_progress = 50,
51 .last_change_ms = 1000,
52 .threshold_ms = 3000,
53 };
54
55 /* 2999ms silence is less than 3000ms threshold */
56 bool fired =
57 nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 3999, false, true, stop: NM_RUN, observer_cpu: 0);
58 TEST_ASSERT(!fired);
59 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
60 return TEST_SUCCESS;
61}
62
63TEST_DECLARE_UNIT(nightmare_liveness, silence_at_threshold_latches) {
64 struct nightmare_liveness_state state = {
65 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
66 .last_progress = 50,
67 .last_change_ms = 1000,
68 .threshold_ms = 3000,
69 };
70
71 /* 3000ms silence reaches threshold */
72 bool fired =
73 nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 4000, false, true, stop: NM_RUN, observer_cpu: 2);
74 TEST_ASSERT(fired);
75 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_PENDING);
76 TEST_ASSERT_EQ(state.pending.silent_ms, 3000);
77 TEST_ASSERT_EQ(state.pending.progress, 50);
78 TEST_ASSERT_EQ(state.pending.observer_cpu, 2);
79 return TEST_SUCCESS;
80}
81
82TEST_DECLARE_UNIT(nightmare_liveness, quiescence_rebaselines) {
83 struct nightmare_liveness_state state = {
84 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
85 .last_progress = 50,
86 .last_change_ms = 1000,
87 .threshold_ms = 3000,
88 };
89
90 /* 5000ms elapse with no progress, but quiesce_requested is true */
91 bool fired =
92 nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 6000, true, true, stop: NM_RUN, observer_cpu: 0);
93 TEST_ASSERT(!fired);
94 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
95 TEST_ASSERT_EQ(state.last_change_ms, 6000);
96 TEST_ASSERT_EQ(state.last_progress, 50);
97
98 /* The first sample after quiesce causes silent window of time */
99 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 7000, false, true, stop: NM_RUN, observer_cpu: 0);
100 TEST_ASSERT(!fired);
101 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
102 TEST_ASSERT_EQ(state.last_change_ms, 7000);
103 TEST_ASSERT(!state.was_quiesced);
104
105 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 9999, false, true, stop: NM_RUN, observer_cpu: 1);
106 TEST_ASSERT(!fired);
107
108 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 10000, false, true, stop: NM_RUN, observer_cpu: 1);
109 TEST_ASSERT(fired);
110 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_PENDING);
111 TEST_ASSERT_EQ(state.pending.silent_ms, 3000);
112 TEST_ASSERT_EQ(state.pending.observer_cpu, 1);
113 return TEST_SUCCESS;
114}
115
116TEST_DECLARE_UNIT(nightmare_liveness, inactive_or_stopped_ignored) {
117 struct nightmare_liveness_state state = {
118 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
119 .last_progress = 50,
120 .last_change_ms = 1000,
121 .threshold_ms = 3000,
122 };
123
124 /* Run inactive */
125 bool fired =
126 nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 5000, false, false, stop: NM_RUN, observer_cpu: 0);
127 TEST_ASSERT(!fired);
128 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
129
130 /* Stop requested (e.g. NM_STOP_BUDGET) */
131 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 5000, false, true,
132 stop: NM_STOP_BUDGET, observer_cpu: 0);
133 TEST_ASSERT(!fired);
134 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
135
136 /* Phase is OFF */
137 atomic_store(&state.phase, NM_LIVE_OFF);
138 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 5000, false, true, stop: NM_RUN, observer_cpu: 0);
139 TEST_ASSERT(!fired);
140 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_OFF);
141 return TEST_SUCCESS;
142}
143
144TEST_DECLARE_UNIT(nightmare_liveness, counter_wrap_resets) {
145 struct nightmare_liveness_state state = {
146 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
147 .last_progress = UINT64_MAX,
148 .last_change_ms = 1000,
149 .threshold_ms = 3000,
150 };
151
152 /* Counter wraps from UINT64_MAX to 0 -> baseline resets */
153 bool fired =
154 nightmare_liveness_eval(state: &state, current_progress: 0, now_ms: 2000, false, true, stop: NM_RUN, observer_cpu: 0);
155 TEST_ASSERT(!fired);
156 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_ARMED);
157 TEST_ASSERT_EQ(state.last_change_ms, 2000);
158 TEST_ASSERT_EQ(state.last_progress, 0);
159 return TEST_SUCCESS;
160}
161
162TEST_DECLARE_UNIT(nightmare_liveness, at_most_once_pending) {
163 struct nightmare_liveness_state state = {
164 .phase = ATOMIC_VAR_INIT(NM_LIVE_ARMED),
165 .last_progress = 50,
166 .last_change_ms = 1000,
167 .threshold_ms = 3000,
168 };
169
170 /* First trigger latches pending */
171 bool fired =
172 nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 4500, false, true, stop: NM_RUN, observer_cpu: 0);
173 TEST_ASSERT(fired);
174 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_PENDING);
175 TEST_ASSERT_EQ(state.pending.silent_ms, 3500);
176
177 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 6000, false, true, stop: NM_RUN, observer_cpu: 0);
178 TEST_ASSERT(!fired);
179 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_PENDING);
180 TEST_ASSERT_EQ(state.pending.silent_ms, 3500);
181
182 atomic_store(&state.phase, NM_LIVE_REPORTED);
183 fired = nightmare_liveness_eval(state: &state, current_progress: 50, now_ms: 7000, false, true, stop: NM_RUN, observer_cpu: 0);
184 TEST_ASSERT(!fired);
185 TEST_ASSERT_EQ(atomic_load(&state.phase), NM_LIVE_REPORTED);
186 TEST_ASSERT_EQ(state.pending.silent_ms, 3500);
187 return TEST_SUCCESS;
188}
189
190TEST_DECLARE_UNIT(nightmare_liveness, start_stop_lifecycle) {
191 TEST_ASSERT(nightmare_liveness_start(2000, NIGHTMARE_ON_STALL_REPORT));
192 TEST_ASSERT_EQ(atomic_load(&nightmare_liveness.phase), NM_LIVE_ARMED);
193 TEST_ASSERT_EQ(nightmare_liveness.threshold_ms, 2000);
194 nightmare_liveness_stop();
195 TEST_ASSERT_EQ(atomic_load(&nightmare_liveness.phase), NM_LIVE_OFF);
196 return TEST_SUCCESS;
197}
198
199TEST_DECLARE_UNIT(nightmare_liveness, reject_zero_threshold) {
200 TEST_ASSERT(!nightmare_liveness_start(0, NIGHTMARE_ON_STALL_REPORT));
201 return TEST_SUCCESS;
202}
203
204TEST_DECLARE_UNIT(nightmare_liveness, start_stop_stress) {
205 for (size_t i = 0; i < 1000; i++) {
206 TEST_ASSERT(nightmare_liveness_start(1000, NIGHTMARE_ON_STALL_REPORT));
207 nightmare_liveness_stop();
208 }
209 return TEST_SUCCESS;
210}
211
212static atomic_uint mock_probe_count = 0;
213static void mock_subject_probe(struct nightmare_ctx *ctx) {
214 (void) ctx;
215 atomic_fetch_add(&mock_probe_count, 1);
216}
217
218static struct nightmare_ops mock_probe_ops = {
219 .probe = mock_subject_probe,
220};
221
222static struct nightmare mock_probe_nm = {
223 .name = "mock_probe",
224 .ops = &mock_probe_ops,
225};
226
227TEST_DECLARE_UNIT(nightmare_liveness, poll_consumes_pending) {
228 atomic_store(&nightmare_runtime.stop, NM_RUN);
229 TEST_ASSERT(nightmare_liveness_start(1000, NIGHTMARE_ON_STALL_REPORT));
230
231 /* Simulate watchdog callback triggering a stall */
232 bool eval_fired = nightmare_liveness_eval(
233 state: &nightmare_liveness, current_progress: nightmare_liveness.last_progress,
234 now_ms: nightmare_liveness.last_change_ms + 1500, false, true, stop: NM_RUN, observer_cpu: 0);
235 TEST_ASSERT(eval_fired);
236 TEST_ASSERT_EQ(atomic_load(&nightmare_liveness.phase), NM_LIVE_PENDING);
237
238 /* Poll in thread context */
239 nightmare_liveness_poll();
240
241 TEST_ASSERT_EQ(atomic_load(&nightmare_liveness.phase), NM_LIVE_REPORTED);
242 TEST_ASSERT_EQ(atomic_load(&nightmare_runtime.stop), NM_STOP_STALL);
243
244 nightmare_liveness_stop();
245 atomic_store(&nightmare_runtime.stop, NM_RUN);
246 return TEST_SUCCESS;
247}
248
249TEST_DECLARE_UNIT(nightmare_liveness, poll_invokes_probe) {
250 atomic_store(&mock_probe_count, 0);
251 const struct nightmare *saved = nightmare_runtime.ctx.nm;
252 nightmare_runtime.ctx.nm = &mock_probe_nm;
253
254 nightmare_liveness_poll();
255
256 TEST_ASSERT_EQ(atomic_load(&mock_probe_count), 1);
257
258 nightmare_runtime.ctx.nm = saved;
259 return TEST_SUCCESS;
260}
261#endif
262