Branch data Line data Source code
1 : : #include "precizer.h"
2 : :
3 : : #ifdef TESTITALL_TEST_HOOKS
4 : : #include <errno.h>
5 : : #include <limits.h>
6 : : #include <stdint.h>
7 : : #include <utime.h>
8 : :
9 : : static const char *const signal_wait_ready_fd_env_name = "TESTITALL_SIGNAL_WAIT_READY_FD";
10 : : static const int testitall_hash_checkpoint_exit_code_default = 77;
11 : :
12 : : /**
13 : : * @brief Check whether a boolean test hook environment flag is enabled
14 : : *
15 : : * Only the exact string `true` enables the hook. Empty values, missing
16 : : * variables, and all other values leave the hook disabled
17 : : *
18 : : * @param[in] env_name Environment variable name to inspect
19 : : * @return true when the flag is explicitly enabled, otherwise false
20 : : */
21 : 2605 : static bool testitall_env_flag_is_true(const char *env_name)
22 : : {
23 : 2605 : const char *flag_value = NULL;
24 : :
25 [ - + ]: 2605 : if(env_name == NULL)
26 : : {
27 : 0 : return(false);
28 : : }
29 : :
30 : 2605 : flag_value = getenv(env_name);
31 : :
32 [ + + + + ]: 2605 : if(flag_value == NULL || strcmp(flag_value,"true") != 0)
33 : : {
34 : 2597 : return(false);
35 : : }
36 : :
37 : 8 : return(true);
38 : : }
39 : :
40 : : /**
41 : : * @brief Notify the background test parent that the configured wait point was reached
42 : : *
43 : : * The notification descriptor is supplied by runit_background() only for
44 : : * zero-min-delay scenarios. The helper closes the descriptor after one
45 : : * notification so repeated visits to the same wait point do not emit another
46 : : * readiness byte
47 : : */
48 : 4 : static void signal_wait_notify_ready(void)
49 : : {
50 : 4 : const char *ready_fd_text = getenv(signal_wait_ready_fd_env_name);
51 : :
52 [ + - - + ]: 4 : if(NULL == ready_fd_text || '\0' == ready_fd_text[0])
53 : : {
54 : 0 : return;
55 : : }
56 : :
57 : 4 : errno = 0;
58 : 4 : char *end_ptr = NULL;
59 : 4 : unsigned long long parsed_ready_fd = strtoull(ready_fd_text,&end_ptr,10);
60 : :
61 [ + - ]: 4 : if(errno != 0
62 [ + - ]: 4 : || end_ptr == ready_fd_text
63 [ + - ]: 4 : || '\0' != *end_ptr
64 [ - + ]: 4 : || parsed_ready_fd > (unsigned long long)INT_MAX)
65 : : {
66 : 0 : return;
67 : : }
68 : :
69 : 4 : const int ready_fd = (int)parsed_ready_fd;
70 : 4 : const char notification = 'R';
71 : 4 : ssize_t bytes_written = 0;
72 : :
73 : : do {
74 : 4 : bytes_written = write(ready_fd,¬ification,sizeof(notification));
75 [ - + - - ]: 4 : } while(bytes_written == -1 && errno == EINTR);
76 : :
77 : 4 : (void)close(ready_fd);
78 : 4 : (void)unsetenv(signal_wait_ready_fd_env_name);
79 : : }
80 : :
81 : : /**
82 : : * @brief Pause a test run at a configured wait point
83 : : *
84 : : * Used by signal-driven tests to delay a known execution point. The wait point
85 : : * and duration are selected with `TESTITALL_SIGNAL_WAIT_POINT` and
86 : : * `TESTITALL_SIGNAL_WAIT_MS`. The delay ends early when `global_interrupt_flag`
87 : : * is set. When `TESTITALL_SIGNAL_WAIT_READY_FD` is present, the function first
88 : : * notifies runit_background() that the configured wait point was reached
89 : : *
90 : : * @param point_id Wait point identifier reached by the caller
91 : : */
92 : 356 : void signal_wait_at_point(unsigned int point_id)
93 : : {
94 : 356 : const char *configured_point = getenv("TESTITALL_SIGNAL_WAIT_POINT");
95 : :
96 [ + + - + ]: 356 : if(NULL == configured_point || '\0' == configured_point[0])
97 : : {
98 : 356 : return;
99 : : }
100 : :
101 : 6 : errno = 0;
102 : 6 : char *point_end_ptr = NULL;
103 : 6 : unsigned long long parsed_point_id = strtoull(configured_point,&point_end_ptr,10);
104 : :
105 [ + - + - : 6 : if(errno != 0 || point_end_ptr == configured_point || '\0' != *point_end_ptr)
- + ]
106 : : {
107 : 0 : return;
108 : : }
109 : :
110 [ + + ]: 6 : if(parsed_point_id != (unsigned long long)point_id)
111 : : {
112 : 2 : return;
113 : : }
114 : :
115 : 4 : signal_wait_notify_ready();
116 : :
117 : 4 : const char *timeout_text = getenv("TESTITALL_SIGNAL_WAIT_MS");
118 : :
119 [ + - - + ]: 4 : if(NULL == timeout_text || '\0' == timeout_text[0])
120 : : {
121 : 0 : return;
122 : : }
123 : :
124 : 4 : errno = 0;
125 : 4 : char *end_ptr = NULL;
126 : 4 : unsigned long long parsed_timeout_ms = strtoull(timeout_text,&end_ptr,10);
127 : :
128 [ + - + - : 4 : if(errno != 0 || end_ptr == timeout_text || '\0' != *end_ptr || parsed_timeout_ms == 0ULL)
+ - - + ]
129 : : {
130 : 0 : return;
131 : : }
132 : :
133 : 4 : uint64_t remaining_timeout_ms = (uint64_t)parsed_timeout_ms;
134 : :
135 [ + - ]: 4 : while(remaining_timeout_ms > 0U)
136 : : {
137 : : /* Allow tests to release the delay as soon as the signal handler sets the flag. */
138 [ - + ]: 4 : if(atomic_load(&global_interrupt_flag) == true)
139 : : {
140 : 4 : return;
141 : : }
142 : :
143 : 4 : uint64_t chunk_ms = remaining_timeout_ms;
144 : :
145 [ + - ]: 4 : if(chunk_ms > 10U)
146 : : {
147 : 4 : chunk_ms = 10U;
148 : : }
149 : :
150 : 4 : struct timespec delay = {
151 : : .tv_sec = 0,
152 : 4 : .tv_nsec = (long)(chunk_ms * 1000000ULL)
153 : : };
154 : :
155 [ + - + - ]: 4 : while(nanosleep(&delay,&delay) == -1 && errno == EINTR)
156 : : {
157 [ + - ]: 4 : if(atomic_load(&global_interrupt_flag) == true)
158 : : {
159 : 4 : return;
160 : : }
161 : : }
162 : :
163 : 0 : remaining_timeout_ms -= chunk_ms;
164 : : }
165 : : }
166 : :
167 : : /**
168 : : * @brief Check whether a path matches a configured test suffix
169 : : *
170 : : * Accepts an exact match or a suffix that begins after a path separator, so a
171 : : * relative test target can match both relative and absolute runtime paths
172 : : *
173 : : * @param[in] path Runtime path to inspect
174 : : * @param[in] suffix Test suffix to match
175 : : * @return true when the path matches the suffix, otherwise false
176 : : */
177 : 677 : static bool testitall_path_matches_suffix(
178 : : const char *path,
179 : : const char *suffix)
180 : : {
181 : 677 : size_t path_len = 0;
182 : 677 : size_t suffix_len = 0;
183 : :
184 [ + - - + ]: 677 : if(path == NULL || suffix == NULL)
185 : : {
186 : 0 : return false;
187 : : }
188 : :
189 [ + + ]: 677 : if(strcmp(path,suffix) == 0)
190 : : {
191 : 31 : return true;
192 : : }
193 : :
194 : 646 : path_len = strlen(path);
195 : 646 : suffix_len = strlen(suffix);
196 : :
197 [ + + ]: 646 : if(path_len < suffix_len + 1U)
198 : : {
199 : 602 : return false;
200 : : }
201 : :
202 [ + + ]: 44 : if(path[path_len - suffix_len - 1U] != '/')
203 : : {
204 : 30 : return false;
205 : : }
206 : :
207 : 14 : return strcmp(path + (path_len - suffix_len),suffix) == 0;
208 : : }
209 : :
210 : : /**
211 : : * @brief Apply a test-requested filesystem access status override
212 : : *
213 : : * Reads the target suffix and forced status from the test environment. When
214 : : * both values are present and the path matches, the requested status is written
215 : : * to @p access_status_out
216 : : *
217 : : * @param[in] path Runtime path being checked
218 : : * @param[out] access_status_out Receives the forced access status
219 : : * @return true when an override was applied, otherwise false
220 : : */
221 : 20877 : bool testitall_file_access_status_override(
222 : : const char *path,
223 : : FileAccessStatus *access_status_out)
224 : : {
225 : 20877 : const char *target_suffix = getenv("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX");
226 : 20877 : const char *forced_status = getenv("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS");
227 : :
228 [ + - ]: 20877 : if(path == NULL
229 [ + - ]: 20877 : || access_status_out == NULL
230 [ + + ]: 20877 : || target_suffix == NULL
231 [ + - ]: 20869 : || forced_status == NULL
232 [ + + ]: 20869 : || target_suffix[0] == '\0'
233 [ - + ]: 677 : || forced_status[0] == '\0')
234 : : {
235 : 20200 : return false;
236 : : }
237 : :
238 [ + + ]: 677 : if(testitall_path_matches_suffix(path,target_suffix) == false)
239 : : {
240 : 638 : return false;
241 : : }
242 : :
243 [ - + ]: 39 : if(strcmp(forced_status,"FILE_ACCESS_ALLOWED") == 0)
244 : : {
245 : 0 : *access_status_out = FILE_ACCESS_ALLOWED;
246 : :
247 [ + + ]: 39 : } else if(strcmp(forced_status,"FILE_ACCESS_DENIED") == 0){
248 : 12 : *access_status_out = FILE_ACCESS_DENIED;
249 : :
250 [ + + ]: 27 : } else if(strcmp(forced_status,"FILE_NOT_FOUND") == 0){
251 : 2 : *access_status_out = FILE_NOT_FOUND;
252 : :
253 [ + - ]: 25 : } else if(strcmp(forced_status,"FILE_ACCESS_ERROR") == 0){
254 : 25 : *access_status_out = FILE_ACCESS_ERROR;
255 : :
256 : : } else {
257 : 0 : slog(ERROR,"Test hook failed: unsupported TESTITALL_TEST_ENV_FILE_ACCESS_STATUS value %s\n",forced_status);
258 : 0 : *access_status_out = FILE_ACCESS_ERROR;
259 : : }
260 : :
261 : 39 : return true;
262 : : }
263 : :
264 : : /**
265 : : * @brief Simulate an unexpected database metadata change in dry-run mode
266 : : *
267 : : * When enabled by environment variable
268 : : * `TESTITALL_TEST_ENV_DB_FILE_TIMESTAMPS_WILL_BUMPED=true`, this helper updates
269 : : * database file timestamps via utime(), forcing metadata drift before the final
270 : : * `stat()` comparison in db_check_changes()
271 : : *
272 : : * The hook is intentionally limited to dry-run on an existing primary DB file
273 : : *
274 : : * @return SUCCESS when the hook is inactive or updated the timestamps,
275 : : * otherwise FAILURE
276 : : */
277 : 116 : Return testitall_db_bump_timestamps(void)
278 : : {
279 : : /* Status returned by this function through provide()
280 : : Default value assumes successful completion */
281 : 116 : Return status = SUCCESS;
282 : :
283 : 116 : const char *flag_value = getenv("TESTITALL_TEST_ENV_DB_FILE_TIMESTAMPS_WILL_BUMPED");
284 : :
285 [ + + + + ]: 116 : if(flag_value == NULL || strcmp(flag_value,"true") != 0)
286 : : {
287 : 114 : return(status);
288 : : }
289 : :
290 [ - + ]: 2 : if(config->dry_run != true)
291 : : {
292 : 0 : slog(ERROR,"Test hook failed: TESTITALL_TEST_ENV_DB_FILE_TIMESTAMPS_WILL_BUMPED requires --dry-run\n");
293 : 0 : return(FAILURE);
294 : : }
295 : :
296 [ - + ]: 2 : if(config->db_primary_file_exists != true)
297 : : {
298 : 0 : return(status);
299 : : }
300 : :
301 : 2 : const char *db_path = confstr(db_primary_file_path);
302 : :
303 [ + - - + ]: 2 : if(db_path == NULL || db_path[0] == '\0')
304 : : {
305 : 0 : slog(ERROR,"Test hook failed: database path is empty\n");
306 : 0 : return(FAILURE);
307 : : }
308 : :
309 [ - + ]: 2 : if(utime(db_path,NULL) != 0)
310 : : {
311 : 0 : slog(ERROR,"Test hook failed: unable to bump timestamps for %s\n",db_path);
312 : 0 : return(FAILURE);
313 : : }
314 : :
315 : 2 : slog(TESTING,"Test hook: database file timestamps were bumped for %s\n",confstr(db_file_name));
316 : :
317 : 2 : return(status);
318 : : }
319 : :
320 : : /**
321 : : * @brief Simulate missing database metadata drift during update mode
322 : : *
323 : : * When enabled by environment variable
324 : : * `TESTITALL_TEST_ENV_DB_FILE_STAT_WILL_BE_RESYNCED=true`, this helper overwrites
325 : : * the saved baseline stat (`config->db_file_stat`) with the current DB file
326 : : * stat. This forces metadata comparison in db_check_changes() to report
327 : : * IDENTICAL, even if the database was modified earlier in the same run
328 : : *
329 : : * The hook is intentionally limited to non-dry-run mode and to cases where
330 : : * `config->db_primary_file_modified` is already true
331 : : *
332 : : * @param[in] db_current_stat Current metadata for the primary database file
333 : : * @return SUCCESS when the hook is inactive or resynchronized the baseline,
334 : : * otherwise FAILURE
335 : : */
336 : 116 : Return testitall_db_resync_stat_baseline(const struct stat *db_current_stat)
337 : : {
338 : : /* Status returned by this function through provide()
339 : : Default value assumes successful completion */
340 : 116 : Return status = SUCCESS;
341 : :
342 : 116 : const char *flag_value = getenv("TESTITALL_TEST_ENV_DB_FILE_STAT_WILL_BE_RESYNCED");
343 : :
344 [ + + + + ]: 116 : if(flag_value == NULL || strcmp(flag_value,"true") != 0)
345 : : {
346 : 114 : return(status);
347 : : }
348 : :
349 [ - + ]: 2 : if(config->dry_run == true)
350 : : {
351 : 0 : slog(ERROR,"Test hook failed: TESTITALL_TEST_ENV_DB_FILE_STAT_WILL_BE_RESYNCED requires non-dry-run mode\n");
352 : 0 : return(FAILURE);
353 : : }
354 : :
355 [ - + ]: 2 : if(config->db_primary_file_exists != true)
356 : : {
357 : 0 : return(status);
358 : : }
359 : :
360 [ - + ]: 2 : if(config->db_primary_file_modified != true)
361 : : {
362 : 0 : slog(ERROR,"Test hook failed: baseline resync requested, but database modification flag is false\n");
363 : 0 : return(FAILURE);
364 : : }
365 : :
366 [ - + ]: 2 : if(db_current_stat == NULL)
367 : : {
368 : 0 : slog(ERROR,"Test hook failed: current database stat is unavailable\n");
369 : 0 : return(FAILURE);
370 : : }
371 : :
372 : : /*
373 : : * Intentionally corrupt the comparison baseline for test coverage:
374 : : * make "before" equal to "after" even after a real DB update.
375 : : */
376 : 2 : config->db_file_stat = *db_current_stat;
377 : :
378 : 2 : slog(TESTING,"Test hook: baseline database stat was resynced to current metadata for %s\n",confstr(db_file_name));
379 : :
380 : 2 : return(status);
381 : : }
382 : :
383 : : /**
384 : : * @brief Check whether a path points to the large interruption test file
385 : : *
386 : : * @param[in] path Path being hashed during a test build
387 : : * @return true when the path ends with the interruption fixture name
388 : : */
389 : 2597 : bool testitall_is_huge_interruption_target(const char *path)
390 : : {
391 [ - + ]: 2597 : if(path == NULL)
392 : : {
393 : 0 : return(false);
394 : : }
395 : :
396 : 2597 : const char *needle = "hugetestfile";
397 : 2597 : const size_t path_length = strlen(path);
398 : 2597 : const size_t needle_length = strlen(needle);
399 : :
400 [ + + ]: 2597 : if(path_length < needle_length)
401 : : {
402 : 38 : return(false);
403 : : }
404 : :
405 : 2559 : return(0 == strcmp(path + (path_length - needle_length),needle));
406 : : }
407 : :
408 : : /**
409 : : * @brief Generate a pseudo-random stop byte in the closed range [1, file_size]
410 : : *
411 : : * @param[in] file_size File size used as the inclusive upper bound
412 : : * @return Selected byte offset, or zero when @p file_size is zero
413 : : */
414 : 11 : uint64_t testitall_random_stop_byte(const uint64_t file_size)
415 : : {
416 [ - + ]: 11 : if(file_size == 0U)
417 : : {
418 : 0 : return(0U);
419 : : }
420 : :
421 : 11 : struct timespec now = {0};
422 : 11 : (void)clock_gettime(CLOCK_MONOTONIC,&now);
423 : :
424 : 11 : uint64_t seed = (uint64_t)now.tv_nsec;
425 : 11 : seed ^= ((uint64_t)now.tv_sec << 32);
426 : 11 : seed ^= (uint64_t)getpid();
427 : :
428 : 11 : return((seed % file_size) + 1U);
429 : : }
430 : :
431 : : /**
432 : : * @brief Check whether SHA512 checkpoint tests should checkpoint at a random byte
433 : : *
434 : : * Enabled by `TESTITALL_TEST_ENV_HASH_CHECKPOINT_AT_RANDOM_BYTE=true`
435 : : *
436 : : * @return true when the hook is enabled, otherwise false
437 : : */
438 : 2601 : bool testitall_hash_checkpoint_at_random_byte_enabled(void)
439 : : {
440 : 2601 : return(testitall_env_flag_is_true("TESTITALL_TEST_ENV_HASH_CHECKPOINT_AT_RANDOM_BYTE"));
441 : : }
442 : :
443 : : /**
444 : : * @brief Check whether SHA512 checkpoint tests should terminate after checkpoint
445 : : *
446 : : * Enabled by `TESTITALL_TEST_ENV_EXIT_AFTER_HASH_CHECKPOINT=true`
447 : : *
448 : : * @return true when the hook is enabled, otherwise false
449 : : */
450 : 4 : bool testitall_exit_after_hash_checkpoint_enabled(void)
451 : : {
452 : 4 : return(testitall_env_flag_is_true("TESTITALL_TEST_ENV_EXIT_AFTER_HASH_CHECKPOINT"));
453 : : }
454 : :
455 : : /**
456 : : * @brief Terminate the current process after a test-controlled hash checkpoint
457 : : *
458 : : * The exit status is read from `TESTITALL_TEST_ENV_HASH_CHECKPOINT_EXIT_CODE`.
459 : : * Invalid, empty, zero, and out-of-range values fall back to 77 so the process
460 : : * never exits successfully by accident
461 : : */
462 : 3 : void testitall_exit_after_hash_checkpoint(void)
463 : : {
464 : 3 : int exit_code = testitall_hash_checkpoint_exit_code_default;
465 : 3 : const char *exit_code_text = getenv("TESTITALL_TEST_ENV_HASH_CHECKPOINT_EXIT_CODE");
466 : :
467 [ + - + - ]: 3 : if(exit_code_text != NULL && exit_code_text[0] != '\0')
468 : : {
469 : 3 : errno = 0;
470 : 3 : char *end_ptr = NULL;
471 : 3 : const long parsed_exit_code = strtol(exit_code_text,&end_ptr,10);
472 : :
473 [ + - ]: 3 : if(errno == 0
474 [ + - ]: 3 : && end_ptr != exit_code_text
475 [ + - ]: 3 : && *end_ptr == '\0'
476 [ + - ]: 3 : && parsed_exit_code > 0L
477 [ + - ]: 3 : && parsed_exit_code <= 255L)
478 : : {
479 : 3 : exit_code = (int)parsed_exit_code;
480 : : }
481 : : }
482 : :
483 : 3 : slog(TESTING,"Test hook: exiting after hash checkpoint with code %d\n",exit_code);
484 : 3 : exit(exit_code);
485 : : }
486 : : #endif
|