Branch data Line data Source code
1 : : #include "sute.h"
2 : : #include <sysexits.h>
3 : :
4 : : typedef struct
5 : : {
6 : : const char *path;
7 : : bool required;
8 : : bool has_entries;
9 : : Return result;
10 : : } ReaderCapture;
11 : :
12 : : static ReaderCapture *active_reader = NULL;
13 : :
14 : : /**
15 : : * @brief Preserve the reader's return value while capturing its diagnostics
16 : : */
17 : 90 : static void capture_reader(void)
18 : : {
19 : 90 : char program_name[] = "testitall";
20 : 90 : struct argp_state state = {.name = program_name,.err_stream = stderr,.flags = ARGP_NO_EXIT};
21 : 180 : active_reader->result = read_config_file(active_reader->path,
22 : 90 : active_reader->required,&active_reader->has_entries,&state);
23 : 90 : }
24 : :
25 : : /**
26 : : * @brief Apply a file to fresh defaults and check the complete diagnostic output
27 : : * @param relative_path Configuration path under TMPDIR
28 : : * @param required Whether absence must cause an error
29 : : * @param expected_result Expected reader return status
30 : : * @param expected_entries Whether a valid record must have been encountered
31 : : * @param error_template Complete stderr template, or NULL for silence
32 : : * @return SUCCESS when the reader and all captured output match
33 : : */
34 : 90 : static Return check_reader(
35 : : const char *relative_path,
36 : : bool required,
37 : : Return expected_result,
38 : : bool expected_entries,
39 : : const char *error_template)
40 : : {
41 : : /* Status returned by this function through deliver()
42 : : Default value assumes successful completion */
43 : 90 : Return status = SUCCESS;
44 : : /* Cleanup must discard a pending diagnostic line before the next run */
45 : 90 : config->config_file_error_line = 12;
46 : 90 : free_config();
47 : 90 : ASSERT(config->config_file_error_line == -1);
48 : 90 : ASSERT(SUCCESS == init_config_and_check_output());
49 : 90 : ASSERT(config->config_file_error_line == -1);
50 : 90 : m_create(char,path,MEMORY_STRING);
51 : 90 : m_create(char,captured_stdout,MEMORY_STRING);
52 : 90 : m_create(char,captured_stderr,MEMORY_STRING);
53 : 90 : m_create(char,pattern,MEMORY_STRING);
54 : 90 : ReaderCapture capture = {
55 : : .required = required,
56 : : .has_entries = true,
57 : : .result = FAILURE
58 : : };
59 : :
60 : 90 : ASSERT(SUCCESS == construct_path(relative_path,path));
61 : 90 : IF(SUCCESS == status)
62 : : {
63 : 90 : capture.path = m_text(path);
64 : 90 : active_reader = &capture;
65 : : }
66 : 90 : ASSERT(SUCCESS == function_capture(capture_reader,captured_stdout,captured_stderr));
67 : 90 : active_reader = NULL;
68 : 90 : ASSERT(capture.result == expected_result);
69 : 90 : ASSERT(capture.has_entries == expected_entries);
70 : 90 : ASSERT(config->config_file_error_line == -1);
71 : 90 : ASSERT(captured_stdout->length == 0U);
72 : :
73 [ + + ]: 90 : if(error_template != NULL)
74 : : {
75 : 44 : ASSERT(SUCCESS == get_file_content(error_template,pattern));
76 : 44 : ASSERT(SUCCESS == match_pattern(captured_stderr,pattern,error_template));
77 : : } else {
78 : 46 : ASSERT(captured_stderr->length == 0U);
79 : : }
80 : :
81 : 90 : call(m_del(pattern));
82 : 90 : call(m_del(captured_stderr));
83 : 90 : call(m_del(captured_stdout));
84 : 90 : call(m_del(path));
85 : 90 : deliver(status);
86 : : }
87 : :
88 : : /**
89 : : * @brief Apply repeated settings across sections and retain their strings after reading finishes
90 : : * @return Return status for scalar, list, alias, and output-setting application
91 : : */
92 : 1 : static Return test0045_1(void)
93 : : {
94 : 1 : INITTEST;
95 : 1 : const LOGMODES default_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
96 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001.conf",
97 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
98 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
99 : 1 : ASSERT(config->roots.length == 4U);
100 : 1 : memory *roots = m_data(memory,conf(roots));
101 : 1 : ASSERT(strcmp(m_text(&roots[0]),"/data/first") == 0);
102 : 1 : ASSERT(strcmp(m_text(&roots[1]),"/data/second#suffix") == 0);
103 : 1 : ASSERT(strcmp(m_text(&roots[2]),"root with spaces") == 0);
104 : 1 : ASSERT(strcmp(m_text(&roots[3]),"[literal continuation]") == 0);
105 : :
106 : 1 : const char *expected_ignore[] = {
107 : : "^cache/","^tmp/","^something_eles/","^other/","^things/","^before#after"
108 : : };
109 : 1 : ASSERT(config->ignore != NULL);
110 [ + + + - ]: 7 : for(size_t i = 0; i < sizeof(expected_ignore) / sizeof(expected_ignore[0]) && SUCCESS == status; ++i)
111 : : {
112 : 6 : ASSERT(config->ignore[i] != NULL);
113 : 6 : ASSERT(strcmp(config->ignore[i],expected_ignore[i]) == 0);
114 : : }
115 : 1 : ASSERT(config->ignore[6] == NULL);
116 : 1 : ASSERT(config->include != NULL);
117 : 1 : ASSERT(config->include[0] != NULL && config->include[1] != NULL);
118 : 1 : ASSERT(strcmp(config->include[0],"^one;two=three:four/") == 0);
119 : 1 : ASSERT(strcmp(config->include[1],"^following/") == 0);
120 : 1 : ASSERT(config->include[2] == NULL);
121 : 1 : ASSERT(config->include_specified == true);
122 : 1 : ASSERT(config->db_drop_ignored == false);
123 : 1 : ASSERT(config->db_drop_inaccessible == false);
124 : 1 : ASSERT(config->one_file_system == false);
125 : 1 : ASSERT(config->db_check_level == QUICK);
126 : 1 : ASSERT(config->maxdepth == 7);
127 : 1 : ASSERT(strcmp(confstr(db_primary_file_path),"/second/final.db") == 0);
128 : 1 : ASSERT(strcmp(confstr(db_file_name),"final.db") == 0);
129 : 1 : ASSERT(config->compare_filter == (CF_CHECKSUM_MISMATCH | CF_FIRST_SOURCE));
130 : 1 : ASSERT(config->dry_run == true && config->dry_run_with_checksums == true);
131 : 1 : ASSERT(config->verbose == false);
132 : 1 : ASSERT(rational_logger_mode == default_logger_mode);
133 : 1 : ASSERT(rational_color_mode == COLOR_MODE_NEVER);
134 : :
135 : : /* Mixed-case FULL replaces QUICK instead of retaining the earlier value */
136 : 1 : ASSERT(SUCCESS == copy_path("tests/templates/0045_001_5.conf","0045_reader.conf"));
137 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
138 : 1 : ASSERT(config->db_check_level == FULL);
139 : :
140 : : /* The largest supported traversal depth is accepted without truncation */
141 : 1 : ASSERT(SUCCESS == copy_path("tests/templates/0045_001_6.conf","0045_reader.conf"));
142 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
143 : 1 : ASSERT(config->maxdepth == 32767);
144 : :
145 : : /* BOM, CRLF, initial indentation, and an unterminated continuation */
146 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001_1.conf",
147 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
148 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
149 : 1 : ASSERT(config->roots.length == 2U);
150 : 1 : roots = m_data(memory,conf(roots));
151 : 1 : ASSERT(strcmp(m_text(&roots[0]),"first") == 0);
152 : 1 : ASSERT(strcmp(m_text(&roots[1]),"second") == 0);
153 : :
154 : : /* A selected file can contain no records at all */
155 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001_2.conf",
156 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
157 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,false,NULL));
158 : 1 : ASSERT(config->roots.length == 0U);
159 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001_3.conf",
160 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
161 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,false,NULL));
162 : 1 : ASSERT(config->roots.length == 0U);
163 : :
164 : : /* Section headers alone do not count as settings */
165 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001_4.conf",
166 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
167 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,false,NULL));
168 : 1 : ASSERT(config->roots.length == 0U);
169 : :
170 : 1 : call(delete_path_if_present("0045_reader.conf"));
171 : 1 : RETURN_STATUS;
172 : : }
173 : :
174 : : /**
175 : : * @brief Reject invalid syntax and keys before consuming any later records
176 : : * @return Return status for first-error handling and original line numbers
177 : : */
178 : 1 : static Return test0045_2(void)
179 : : {
180 : 1 : INITTEST;
181 : : static const struct {
182 : : const char *config_template;
183 : : const char *error_template;
184 : : } cases[] = {
185 : : {"tests/templates/0045_002_1.conf","templates/0045_002.txt"},
186 : : {"tests/templates/0045_002_2.conf","templates/0045_003.txt"},
187 : : {"tests/templates/0045_002_3.conf","templates/0045_003.txt"},
188 : : {"tests/templates/0045_002_4.conf","templates/0045_003.txt"},
189 : : {"tests/templates/0045_002_5.conf","templates/0045_004.txt"},
190 : : {"tests/templates/0045_002_6.conf","templates/0045_024.txt"},
191 : : {"tests/templates/0045_002_7.conf","templates/0045_025.txt"},
192 : : {"tests/templates/0045_002_8.conf","templates/0045_005.txt"},
193 : : {"tests/templates/0045_002_9.conf","templates/0045_026.txt"},
194 : : {"tests/templates/0045_002_10.conf","templates/0045_027.txt"},
195 : : {"tests/templates/0045_002_11.conf","templates/0045_028.txt"}
196 : : };
197 : :
198 [ + + + - ]: 12 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
199 : : {
200 : 11 : ASSERT(SUCCESS == copy_from_origin(cases[i].config_template,
201 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
202 : 11 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,
203 : : cases[i].error_template));
204 : 11 : ASSERT(config->roots.length == 1U);
205 : : }
206 : :
207 : : /* A first-line failure must prevent the later valid key from setting the flag */
208 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_002_12.conf",
209 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
210 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,false,
211 : : "templates/0045_006.txt"));
212 : 1 : ASSERT(config->roots.length == 0U);
213 : :
214 : 1 : call(delete_path_if_present("0045_reader.conf"));
215 : 1 : RETURN_STATUS;
216 : : }
217 : :
218 : : /**
219 : : * @brief Reject an oversized line and stop before applying subsequent settings
220 : : * @return Return status for the parser's long-line error and unread later settings
221 : : */
222 : 1 : static Return test0045_3(void)
223 : : {
224 : 1 : INITTEST;
225 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_003.conf",
226 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
227 : : /* inih reports the error after recognizing the key from the buffered prefix */
228 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,
229 : : "templates/0045_007.txt"));
230 : : /* The valid path after the oversized line must not be applied */
231 : 1 : ASSERT(config->roots.length == 0U);
232 : :
233 : 1 : call(delete_path_if_present("0045_reader.conf"));
234 : 1 : RETURN_STATUS;
235 : : }
236 : :
237 : : /**
238 : : * @brief Distinguish optional absence from required-file and read failures
239 : : * @return Return status for filesystem failure handling
240 : : */
241 : 1 : static Return test0045_4(void)
242 : : {
243 : 1 : INITTEST;
244 : :
245 : 1 : ASSERT(SUCCESS == delete_path_if_present("0045_reader.conf"));
246 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",false,SUCCESS,false,NULL));
247 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,false,
248 : : "templates/0045_008.txt"));
249 : :
250 : : /* A regular file used as a parent produces ENOTDIR even with elevated privileges */
251 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_001_2.conf",
252 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
253 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf/child",false,FAILURE,false,
254 : : "templates/0045_009.txt"));
255 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf/child",true,FAILURE,false,
256 : : "templates/0045_009.txt"));
257 : :
258 : : /* A directory opens successfully but causes a read error on the first read */
259 : 1 : ASSERT(SUCCESS == create_directory("0045_config_directory"));
260 : 1 : ASSERT(SUCCESS == check_reader("0045_config_directory",true,FAILURE,false,
261 : : "templates/0045_010.txt"));
262 : 1 : call(delete_path_if_present("0045_config_directory"));
263 : :
264 : 1 : call(delete_path_if_present("0045_reader.conf"));
265 : 1 : RETURN_STATUS;
266 : : }
267 : :
268 : : /**
269 : : * @brief Apply the last boolean value and update the flag's related settings
270 : : * @return Return status for boolean spellings, repeated flags, and dry-run modes
271 : : */
272 : 1 : static Return test0045_5(void)
273 : : {
274 : 1 : INITTEST;
275 : 1 : free_config();
276 : 1 : ASSERT(SUCCESS == init_config_and_check_output());
277 : 1 : const LOGMODES default_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
278 : : const struct {
279 : : const char *config_template;
280 : : bool enabled;
281 : 1 : } cases[] = {
282 : : {"tests/templates/0045_005_1.conf",true},
283 : : {"tests/templates/0045_005_2.conf",false},
284 : : {"tests/templates/0045_005_3.conf",false},
285 : : {"tests/templates/0045_005_4.conf",true},
286 : : {"tests/templates/0045_005_5.conf",false},
287 : : {"tests/templates/0045_005_6.conf",false},
288 : : {"tests/templates/0045_005_7.conf",true},
289 : : {"tests/templates/0045_005_8.conf",false},
290 : : {"tests/templates/0045_005_9.conf",false},
291 : : {"tests/templates/0045_005_10.conf",true},
292 : : {"tests/templates/0045_005_11.conf",false},
293 : : {"tests/templates/0045_005_12.conf",false},
294 : : {"tests/templates/0045_005_13.conf",true},
295 : : {"tests/templates/0045_005_14.conf",false},
296 : : {"tests/templates/0045_005_15.conf",false},
297 : : {"tests/templates/0045_005_16.conf",true}
298 : : };
299 : :
300 [ + + + - ]: 17 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
301 : : {
302 : 16 : ASSERT(SUCCESS == copy_from_origin(cases[i].config_template,
303 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
304 : 16 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
305 : 16 : ASSERT(config->progress == cases[i].enabled);
306 : 16 : ASSERT(config->update == cases[i].enabled);
307 : 16 : ASSERT(config->dry_run == cases[i].enabled);
308 : 16 : ASSERT(config->rehash_locked == cases[i].enabled);
309 : 16 : ASSERT(config->one_file_system == cases[i].enabled);
310 : 16 : ASSERT(config->db_drop_ignored == cases[i].enabled);
311 : 16 : ASSERT(config->db_drop_inaccessible == cases[i].enabled);
312 : 16 : ASSERT(config->watch_timestamps == cases[i].enabled);
313 : 16 : ASSERT(config->force == cases[i].enabled);
314 : 16 : ASSERT(config->quiet_ignored == cases[i].enabled);
315 : 16 : ASSERT(config->verbose == cases[i].enabled);
316 : 16 : ASSERT(config->show_remembered_messages_at_exit == cases[i].enabled);
317 : 16 : ASSERT(config->dry_run_with_checksums == false);
318 : :
319 [ + + ]: 16 : if(cases[i].enabled == true)
320 : : {
321 : 6 : ASSERT(rational_logger_mode == VERBOSE);
322 : : } else {
323 : 10 : ASSERT(rational_logger_mode == default_logger_mode);
324 : : }
325 : : }
326 : :
327 : : /* A plain true value replaces the checksum mode; a later named mode enables it */
328 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_005_17.conf",
329 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
330 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
331 : 1 : ASSERT(config->dry_run == true && config->dry_run_with_checksums == false);
332 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_005_18.conf",
333 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
334 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
335 : 1 : ASSERT(config->dry_run == true && config->dry_run_with_checksums == true);
336 : :
337 : : /* Disabling an output mode preserves a different active mode */
338 : : const struct {
339 : : const char *config_template;
340 : : LOGMODES logger_mode;
341 : : bool verbose;
342 : 1 : } output_cases[] = {
343 : : {"tests/templates/0045_005_19.conf",default_logger_mode,false},
344 : : {"tests/templates/0045_005_20.conf",default_logger_mode,false},
345 : : {"tests/templates/0045_005_21.conf",SILENT,false},
346 : : {"tests/templates/0045_005_22.conf",VERBOSE,true}
347 : : };
348 : :
349 [ + + + - ]: 5 : for(size_t i = 0; i < sizeof(output_cases) / sizeof(output_cases[0]) && SUCCESS == status; ++i)
350 : : {
351 : 4 : ASSERT(SUCCESS == copy_from_origin(output_cases[i].config_template,
352 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
353 : 4 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
354 : 4 : ASSERT(rational_logger_mode == output_cases[i].logger_mode);
355 : 4 : ASSERT(config->verbose == output_cases[i].verbose);
356 : : }
357 : :
358 : 1 : call(delete_path_if_present("0045_reader.conf"));
359 : 1 : RETURN_STATUS;
360 : : }
361 : :
362 : : /**
363 : : * @brief Attribute shared option errors to their original configuration lines
364 : : * @return Return status for invalid values and immediate parsing termination
365 : : */
366 : 1 : static Return test0045_6(void)
367 : : {
368 : 1 : INITTEST;
369 : : static const struct {
370 : : const char *config_template;
371 : : const char *error_template;
372 : : } cases[] = {
373 : : {"tests/templates/0045_006_1.conf","templates/0045_011.txt"},
374 : : {"tests/templates/0045_006_2.conf","templates/0045_020.txt"},
375 : : {"tests/templates/0045_006_3.conf","templates/0045_029.txt"},
376 : : {"tests/templates/0045_006_4.conf","templates/0045_030.txt"},
377 : : {"tests/templates/0045_006_5.conf","templates/0045_031.txt"},
378 : : {"tests/templates/0045_006_6.conf","templates/0045_012.txt"},
379 : : {"tests/templates/0045_006_7.conf","templates/0045_013.txt"},
380 : : {"tests/templates/0045_006_8.conf","templates/0045_014.txt"},
381 : : {"tests/templates/0045_006_9.conf","templates/0045_015.txt"},
382 : : {"tests/templates/0045_006_10.conf","templates/0045_016.txt"},
383 : : {"tests/templates/0045_006_11.conf","templates/0045_017.txt"},
384 : : {"tests/templates/0045_006_12.conf","templates/0045_018.txt"},
385 : : {"tests/templates/0045_006_13.conf","templates/0045_019.txt"},
386 : : {"tests/templates/0045_006_14.conf","templates/0045_032.txt"},
387 : : {"tests/templates/0045_006_15.conf","templates/0045_015.txt"},
388 : : {"tests/templates/0045_006_16.conf","templates/0045_015.txt"},
389 : : {"tests/templates/0045_006_17.conf","templates/0045_018.txt"},
390 : : {"tests/templates/0045_006_18.conf","templates/0045_018.txt"}
391 : : };
392 : :
393 [ + + + - ]: 19 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
394 : : {
395 : 18 : ASSERT(SUCCESS == copy_from_origin(cases[i].config_template,
396 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
397 : 18 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,cases[i].error_template));
398 : 18 : ASSERT(config->roots.length == 1U);
399 : 18 : memory *roots = m_data(memory,conf(roots));
400 : 18 : ASSERT(strcmp(m_text(&roots[0]),"first") == 0);
401 : : }
402 : :
403 : 1 : call(delete_path_if_present("0045_reader.conf"));
404 : 1 : RETURN_STATUS;
405 : : }
406 : :
407 : : /**
408 : : * @brief Retain regex text from repeated keys and continuations for working compilation
409 : : * @return Return status for valid and empty expressions in all three regex lists
410 : : */
411 : 1 : static Return test0045_7(void)
412 : : {
413 : 1 : INITTEST;
414 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_007.conf",
415 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
416 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
417 : :
418 : 1 : char **patterns[] = {config->ignore,config->include,config->lock_checksum};
419 : :
420 [ + + + - ]: 4 : for(size_t i = 0; i < sizeof(patterns) / sizeof(patterns[0]) && SUCCESS == status; ++i)
421 : : {
422 : 3 : ASSERT(patterns[i] != NULL);
423 : 3 : ASSERT(patterns[i][0] != NULL && patterns[i][1] != NULL && patterns[i][2] != NULL);
424 : 3 : ASSERT(strcmp(patterns[i][0],"^first/") == 0);
425 : 3 : ASSERT(strcmp(patterns[i][1],"^second/") == 0);
426 : 3 : ASSERT(strcmp(patterns[i][2],"") == 0);
427 : 3 : ASSERT(patterns[i][3] == NULL);
428 : : }
429 : :
430 : : /* Reading keeps no compiled objects; working compilation creates them */
431 : 1 : ASSERT(config->ignore_pcre_compiled == NULL);
432 : 1 : ASSERT(config->include_pcre_compiled == NULL);
433 : 1 : ASSERT(config->lock_checksum_pcre_compiled == NULL);
434 : 1 : ASSERT(SUCCESS == compile_patterns());
435 : 1 : pcre2_code **compiled[] = {
436 : 1 : config->ignore_pcre_compiled,config->include_pcre_compiled,config->lock_checksum_pcre_compiled
437 : : };
438 : :
439 [ + + + - ]: 4 : for(size_t i = 0; i < sizeof(compiled) / sizeof(compiled[0]) && SUCCESS == status; ++i)
440 : : {
441 : 3 : ASSERT(compiled[i] != NULL);
442 : 3 : ASSERT(compiled[i][0] != NULL && compiled[i][1] != NULL && compiled[i][2] != NULL);
443 : 3 : ASSERT(compiled[i][3] == NULL);
444 : : }
445 : :
446 : 1 : call(delete_path_if_present("0045_reader.conf"));
447 : 1 : RETURN_STATUS;
448 : : }
449 : :
450 : : /**
451 : : * @brief Check the compiler's failure status while its normal output is captured
452 : : * @return SUCCESS when working compilation rejects the stored expression
453 : : */
454 : 9 : static Return capture_pattern_compilation_failure(void)
455 : : {
456 : 9 : INITTEST;
457 : 9 : ASSERT(FAILURE == compile_patterns());
458 : 9 : deliver(status);
459 : : }
460 : :
461 : : /**
462 : : * @brief Defer regex errors until working compilation and prioritize configuration parsing errors
463 : : * @return Return status for first entries, repeated keys, and continuation values
464 : : */
465 : 1 : static Return test0045_8(void)
466 : : {
467 : 1 : INITTEST;
468 : 1 : m_create(char,pattern,MEMORY_STRING);
469 : 1 : ASSERT(SUCCESS == get_file_content("templates/0045_021.txt",pattern));
470 : : const struct {
471 : : const char *config_template;
472 : : /* Address of the Config list populated after check_reader() resets defaults */
473 : : char ***patterns;
474 : : bool has_valid_pattern;
475 : 1 : } cases[] = {
476 : 1 : {"tests/templates/0045_008_1.conf",&config->ignore,false},
477 : 1 : {"tests/templates/0045_008_2.conf",&config->include,false},
478 : 1 : {"tests/templates/0045_008_3.conf",&config->lock_checksum,false},
479 : 1 : {"tests/templates/0045_008_4.conf",&config->ignore,true},
480 : 1 : {"tests/templates/0045_008_5.conf",&config->include,true},
481 : 1 : {"tests/templates/0045_008_6.conf",&config->lock_checksum,true},
482 : 1 : {"tests/templates/0045_008_7.conf",&config->ignore,true},
483 : 1 : {"tests/templates/0045_008_8.conf",&config->include,true},
484 : 1 : {"tests/templates/0045_008_9.conf",&config->lock_checksum,true}
485 : : };
486 : :
487 [ + + + - ]: 10 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
488 : : {
489 : 9 : ASSERT(SUCCESS == copy_from_origin(cases[i].config_template,
490 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
491 : 9 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
492 : 9 : ASSERT(config->roots.length == 1U);
493 : 9 : memory *roots = m_data(memory,conf(roots));
494 : 9 : ASSERT(strcmp(m_text(&roots[0]),"after-pattern") == 0);
495 : 9 : ASSERT(*cases[i].patterns != NULL);
496 : 9 : ASSERT((*cases[i].patterns)[0] != NULL);
497 : :
498 [ + + ]: 9 : if(cases[i].has_valid_pattern == true)
499 : : {
500 : 6 : ASSERT(strcmp((*cases[i].patterns)[0],"^first/") == 0);
501 : 6 : ASSERT((*cases[i].patterns)[1] != NULL);
502 : 6 : ASSERT(strcmp((*cases[i].patterns)[1],"(") == 0);
503 : 6 : ASSERT((*cases[i].patterns)[2] == NULL);
504 : : } else {
505 : 3 : ASSERT(strcmp((*cases[i].patterns)[0],"(") == 0);
506 : 3 : ASSERT((*cases[i].patterns)[1] == NULL);
507 : : }
508 : :
509 : : /* Use the normal uncolored diagnostic, without verbose or testing traces */
510 : 9 : rational_logger_mode = REGULAR;
511 : 9 : rational_color_mode = COLOR_MODE_NEVER;
512 : 9 : ASSERT(SUCCESS == match_function_output(m_text(pattern),NULL,capture_pattern_compilation_failure));
513 : 9 : ASSERT(config->ignore_pcre_compiled == NULL);
514 : 9 : ASSERT(config->include_pcre_compiled == NULL);
515 : 9 : ASSERT(config->lock_checksum_pcre_compiled == NULL);
516 : : }
517 : :
518 : : /* A later option error is reported before regex compilation can start */
519 : 1 : ASSERT(SUCCESS == copy_from_origin("tests/templates/0045_008_10.conf",
520 : : "0045_reader.conf",REQUIRE_SOURCE_EXISTS));
521 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,"templates/0045_015.txt"));
522 : 1 : ASSERT(config->ignore != NULL && config->ignore[0] != NULL);
523 : 1 : ASSERT(strcmp(config->ignore[0],"(") == 0);
524 : 1 : ASSERT(config->roots.length == 0U);
525 : :
526 : 1 : call(delete_path_if_present("0045_reader.conf"));
527 : 1 : call(m_del(pattern));
528 : 1 : RETURN_STATUS;
529 : : }
530 : :
531 : : /**
532 : : * @brief Check source decisions from preliminary results without running argp
533 : : * @return SUCCESS when source selection and discarded error results match
534 : : */
535 : 1 : static Return check_settings_source_choices(void)
536 : : {
537 : 1 : INITTEST;
538 : 1 : char program_name[] = "testitall";
539 : : static const struct {
540 : : int argc;
541 : : error_t preliminary_error;
542 : : const char *explicit_path;
543 : : bool has_other_arguments;
544 : : bool information_mode_requested;
545 : : const char *expected_path;
546 : : bool expected_required;
547 : : } cases[] = {
548 : : {1,EX_OK,NULL,false,false,APP_NAME ".conf",false},
549 : : {2,EX_OK,NULL,true,false,NULL,false},
550 : : {2,EX_OK,NULL,false,false,NULL,false},
551 : : {3,EX_OK,"profile.conf",false,false,"profile.conf",true},
552 : : {2,EX_OK,"",false,false,"",true},
553 : : {2,EX_OK,"--",false,false,"--",true},
554 : : /* Preliminary parsing supplies the last path when --config is repeated */
555 : : {5,EX_OK,"last.conf",false,false,"last.conf",true},
556 : : {1,EINVAL,NULL,false,false,NULL,false},
557 : : {3,ARGP_ERR_UNKNOWN,"profile.conf",false,false,NULL,false},
558 : : {4,EINVAL,"profile.conf",true,false,NULL,false},
559 : : {2,EINVAL,NULL,true,false,NULL,false},
560 : : /* Information requests suppress both automatic and explicit file selection */
561 : : {2,EX_OK,NULL,true,true,NULL,false},
562 : : {4,EX_OK,"profile.conf",true,true,NULL,false},
563 : : {3,EX_OK,"",true,true,NULL,false},
564 : : {4,EINVAL,"profile.conf",true,true,NULL,false}
565 : : };
566 : :
567 [ + + + - ]: 16 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
568 : : {
569 : 15 : config->config_file_path = cases[i].explicit_path;
570 : 15 : ArgumentParseContext context = {
571 : : .config_required = true,
572 : 15 : .has_other_arguments = cases[i].has_other_arguments,
573 : 15 : .information_mode_requested = cases[i].information_mode_requested,
574 : : .diagnostic_state = {.name = program_name,.err_stream = stderr}
575 : : };
576 : 15 : ASSERT(SUCCESS == select_settings_source(cases[i].argc,cases[i].preliminary_error,&context));
577 : 15 : ASSERT(context.config_required == cases[i].expected_required);
578 : :
579 [ + + ]: 15 : if(cases[i].expected_path != NULL)
580 : : {
581 : 5 : ASSERT(config->config_file_path != NULL);
582 : 5 : ASSERT(strcmp(config->config_file_path,cases[i].expected_path) == 0);
583 : : } else {
584 : 10 : ASSERT(config->config_file_path == NULL);
585 : : }
586 : :
587 [ + + ]: 15 : if(cases[i].preliminary_error != EX_OK)
588 : : {
589 : 5 : ASSERT(context.has_other_arguments == false);
590 : 5 : ASSERT(context.information_mode_requested == false);
591 : : }
592 : : }
593 : :
594 : 1 : deliver(status);
595 : : }
596 : :
597 : : /**
598 : : * @brief Select settings sources silently and defer preliminary errors to CLI parsing
599 : : * @return Return status for source decisions and empty diagnostic streams
600 : : */
601 : 1 : static Return test0045_9(void)
602 : : {
603 : 1 : INITTEST;
604 : 1 : ASSERT(SUCCESS == match_function_output(NULL,NULL,check_settings_source_choices));
605 : 1 : RETURN_STATUS;
606 : : }
607 : :
608 : : /**
609 : : * @brief Reject an explicit configuration file combined with other arguments
610 : : * @return SUCCESS when the conflict fails without leaving a selected file
611 : : */
612 : 1 : static Return check_settings_source_conflict(void)
613 : : {
614 : 1 : INITTEST;
615 : 1 : char program_name[] = "testitall";
616 : 1 : config->config_file_path = "missing.conf";
617 : 1 : ArgumentParseContext context = {
618 : : .config_required = true,
619 : : .has_other_arguments = true,
620 : : .diagnostic_state = {.name = program_name,.err_stream = stderr}
621 : : };
622 : 1 : ASSERT(FAILURE == select_settings_source(4,EX_OK,&context));
623 : 1 : ASSERT(config->config_file_path == NULL);
624 : 1 : ASSERT(context.config_required == false);
625 : 1 : deliver(status);
626 : : }
627 : :
628 : : /**
629 : : * @brief Check the complete diagnostic for a config and CLI source conflict
630 : : * @return Return status for the expected conflict message and empty stdout
631 : : */
632 : 1 : static Return test0045_10(void)
633 : : {
634 : 1 : INITTEST;
635 : 1 : m_create(char,pattern,MEMORY_STRING);
636 : 1 : ASSERT(SUCCESS == get_file_content("templates/0045_022.txt",pattern));
637 : 1 : ASSERT(SUCCESS == match_function_output(NULL,m_text(pattern),check_settings_source_conflict));
638 : 1 : call(m_del(pattern));
639 : 1 : RETURN_STATUS;
640 : : }
641 : :
642 : : /**
643 : : * @brief Apply quoted values while preserving spaces, backslashes, and literal contents
644 : : * @return Return status for quote removal and shared value interpretation
645 : : */
646 : 1 : static Return test0045_11(void)
647 : : {
648 : 1 : INITTEST;
649 : 1 : ASSERT(SUCCESS == copy_path("tests/templates/0045_011.conf","0045_reader.conf"));
650 : 1 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
651 : 1 : ASSERT(config->roots.length == 3U);
652 : 1 : memory *roots = m_data(memory,conf(roots));
653 : 1 : ASSERT(strcmp(m_text(&roots[0]),"/mnt/my files") == 0);
654 : 1 : ASSERT(strcmp(m_text(&roots[1])," spaced path ") == 0);
655 : 1 : ASSERT(strcmp(m_text(&roots[2]),"$HOME/$(literal)") == 0);
656 : :
657 : 1 : const char *expected_ignore[] = {
658 : : "^proc/.*","^sys/.*","\\.log$","^\\w+\\s+\\d+$","\"literal quotes\"","^before#after","^literal\"$"
659 : : };
660 : 1 : ASSERT(config->ignore != NULL);
661 [ + + + - ]: 8 : for(size_t i = 0; i < sizeof(expected_ignore) / sizeof(expected_ignore[0]) && SUCCESS == status; ++i)
662 : : {
663 : 7 : ASSERT(config->ignore[i] != NULL);
664 : 7 : ASSERT(strcmp(config->ignore[i],expected_ignore[i]) == 0);
665 : : }
666 : 1 : ASSERT(config->ignore[7] == NULL);
667 : 1 : ASSERT(config->include != NULL && config->include[0] != NULL);
668 : 1 : ASSERT(strcmp(config->include[0],"^dir with spaces/") == 0);
669 : 1 : ASSERT(config->include[1] == NULL);
670 : 1 : ASSERT(strcmp(confstr(db_primary_file_path),"/tmp/my data.db") == 0);
671 : 1 : ASSERT(strcmp(confstr(db_file_name),"my data.db") == 0);
672 : 1 : ASSERT(config->maxdepth == 3);
673 : 1 : ASSERT(config->db_check_level == QUICK);
674 : 1 : ASSERT(config->update == true);
675 : 1 : ASSERT(config->quiet_ignored == false);
676 : 1 : ASSERT(config->progress == false);
677 : 1 : ASSERT(config->dry_run == true && config->dry_run_with_checksums == true);
678 : :
679 : 1 : call(delete_path_if_present("0045_reader.conf"));
680 : 1 : RETURN_STATUS;
681 : : }
682 : :
683 : : /**
684 : : * @brief Reject missing or mismatched closing quotes at the original line
685 : : * @return Return status for complete diagnostics and stopping before later records
686 : : */
687 : 1 : static Return test0045_12(void)
688 : : {
689 : 1 : INITTEST;
690 : 1 : const char *cases[] = {
691 : : "tests/templates/0045_012_1.conf",
692 : : "tests/templates/0045_012_2.conf",
693 : : "tests/templates/0045_012_3.conf",
694 : : "tests/templates/0045_012_4.conf"
695 : : };
696 : :
697 [ + + + - ]: 5 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
698 : : {
699 : 4 : ASSERT(SUCCESS == copy_path(cases[i],"0045_reader.conf"));
700 : 4 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,"templates/0045_012_1.txt"));
701 : 4 : ASSERT(config->roots.length == 1U);
702 : 4 : memory *roots = m_data(memory,conf(roots));
703 : 4 : ASSERT(strcmp(m_text(&roots[0]),"first") == 0);
704 : 4 : ASSERT(config->ignore == NULL);
705 : : }
706 : :
707 : 1 : call(delete_path_if_present("0045_reader.conf"));
708 : 1 : RETURN_STATUS;
709 : : }
710 : :
711 : : /**
712 : : * @brief Preserve comparison paths in file order across repeated keys and continuation lines
713 : : * @return Return status for owned paths, literal values, and configuration cleanup
714 : : */
715 : 1 : static Return test0045_13(void)
716 : : {
717 : 1 : INITTEST;
718 : : const struct {
719 : : const char *config_template;
720 : : const char *first_path;
721 : : const char *second_path;
722 : 1 : } cases[] = {
723 : : {"tests/templates/0045_013_1.conf","first.db","second.db"},
724 : : {"tests/templates/0045_013_2.conf","first.db","second.db"},
725 : : {"tests/templates/0045_013_3.conf","first db.db","second db.db"},
726 : : {"tests/templates/0045_013_4.conf","first db.db","second db.db"},
727 : : {"tests/templates/0045_013_5.conf","true","false"}
728 : : };
729 : :
730 [ + + + - ]: 6 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
731 : : {
732 : 5 : ASSERT(SUCCESS == copy_path(cases[i].config_template,"0045_reader.conf"));
733 : 5 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,SUCCESS,true,NULL));
734 : 5 : ASSERT(config->compare == true);
735 : 5 : ASSERT(config->roots.length == 0U);
736 : 5 : ASSERT(config->db_file_paths.length == 2U);
737 : 5 : memory *paths = m_data(memory,conf(db_file_paths));
738 : 5 : ASSERT(strcmp(m_text(&paths[0]),cases[i].first_path) == 0);
739 : 5 : ASSERT(strcmp(m_text(&paths[1]),cases[i].second_path) == 0);
740 : : }
741 : :
742 : : /* Release the populated array and verify the next configuration starts empty */
743 : 1 : free_config();
744 : 1 : ASSERT(config->db_file_paths.length == 0U);
745 : 1 : ASSERT(SUCCESS == init_config_and_check_output());
746 : 1 : ASSERT(config->compare == false);
747 : 1 : ASSERT(config->db_file_paths.length == 0U);
748 : :
749 : 1 : call(delete_path_if_present("0045_reader.conf"));
750 : 1 : RETURN_STATUS;
751 : : }
752 : :
753 : : /**
754 : : * @brief Reject an empty comparison path before applying subsequent configuration entries
755 : : * @return Return status for line diagnostics and preservation of earlier paths
756 : : */
757 : 1 : static Return test0045_14(void)
758 : : {
759 : 1 : INITTEST;
760 : : const struct {
761 : : const char *config_template;
762 : : bool compare;
763 : : size_t path_count;
764 : 1 : } cases[] = {
765 : : {"tests/templates/0045_014_1.conf",false,0U},
766 : : {"tests/templates/0045_014_2.conf",true,1U},
767 : : {"tests/templates/0045_014_3.conf",true,1U},
768 : : {"tests/templates/0045_014_4.conf",true,1U}
769 : : };
770 : :
771 [ + + + - ]: 5 : for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]) && SUCCESS == status; ++i)
772 : : {
773 : 4 : ASSERT(SUCCESS == copy_path(cases[i].config_template,"0045_reader.conf"));
774 : 4 : ASSERT(SUCCESS == check_reader("0045_reader.conf",true,FAILURE,true,"templates/0045_014_1.txt"));
775 : 4 : ASSERT(config->compare == cases[i].compare);
776 : 4 : ASSERT(config->db_file_paths.length == cases[i].path_count);
777 : 4 : ASSERT(config->update == false);
778 [ + + + - ]: 4 : if(cases[i].path_count != 0U && SUCCESS == status)
779 : : {
780 : 3 : memory *paths = m_data(memory,conf(db_file_paths));
781 : 3 : ASSERT(strcmp(m_text(&paths[0]),"first.db") == 0);
782 : : }
783 : : }
784 : :
785 : 1 : call(delete_path_if_present("0045_reader.conf"));
786 : 1 : RETURN_STATUS;
787 : : }
788 : :
789 : : /**
790 : : * @brief Exercise configuration reading and source selection independently of CLI startup
791 : : * @return Return status for the configuration reader and source selection tests
792 : : */
793 : 1 : Return test0045(void)
794 : : {
795 : 1 : INITTEST;
796 : 1 : Config *initial_config = config;
797 : 1 : Config test_config = {0};
798 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
799 : 1 : const RATIONAL_COLOR_MODE initial_color_mode = atomic_load_explicit(&rational_color_mode,memory_order_relaxed);
800 : 1 : config = &test_config;
801 : 1 : ASSERT(SUCCESS == init_config_and_check_output());
802 : :
803 : 1 : TEST(test0045_1,"Configuration applies values and retains owned strings after reading");
804 : 1 : TEST(test0045_2,"Invalid configuration syntax and keys stop at the first error");
805 : 1 : TEST(test0045_3,"Oversized configuration lines report a parser error");
806 : 1 : TEST(test0045_4,"Configuration absence, opening failures, and read failures remain distinct");
807 : 1 : TEST(test0045_5,"Boolean values apply the last setting and update related modes");
808 : 1 : TEST(test0045_6,"Shared option errors retain their text and configuration line numbers");
809 : 1 : TEST(test0045_7,"Regex values retain their order and remain available for working compilation");
810 : 1 : TEST(test0045_8,"Regex errors are deferred until working compilation, after configuration parsing");
811 : 1 : TEST(test0045_9,"Settings source selection is silent and discards preliminary parsing errors");
812 : 1 : TEST(test0045_10,"Explicit configuration and other arguments produce a source conflict");
813 : 1 : TEST(test0045_11,"Quoted configuration values preserve literal contents and apply normal value rules");
814 : 1 : TEST(test0045_12,"Unclosed configuration quotes report their line and stop reading");
815 : 1 : TEST(test0045_13,"Comparison paths retain their order and literal contents until configuration cleanup");
816 : 1 : TEST(test0045_14,"Empty comparison paths report their line and stop configuration reading");
817 : :
818 : : /* Restore the runner's configuration and output policy after any test result */
819 : 1 : free_config();
820 : 1 : config = initial_config;
821 : 1 : rational_logger_mode = initial_logger_mode;
822 : 1 : rational_color_mode = initial_color_mode;
823 : :
824 : 1 : RETURN_STATUS;
825 : : }
|