Branch data Line data Source code
1 : : #include "sute.h"
2 : :
3 : : static const char test0038_relative_path[] = "hugetestfile";
4 : : static const char test0038_fixture_root[] = "tests/fixtures/huge";
5 : : static const int test0038_checkpoint_exit_code = 77;
6 : :
7 : : /**
8 : : * @brief Reset SHA512 checkpoint test hooks in the process environment
9 : : *
10 : : * @return Return status code
11 : : */
12 : 12 : static Return test0038_reset_checkpoint_hooks(void)
13 : : {
14 : : /* Status returned by this function through provide()
15 : : Default value assumes successful completion */
16 : 12 : Return status = SUCCESS;
17 : :
18 : 12 : call(set_environment_variable("TESTITALL_TEST_ENV_HASH_CHECKPOINT_AT_RANDOM_BYTE",""));
19 : 12 : call(set_environment_variable("TESTITALL_TEST_ENV_EXIT_AFTER_HASH_CHECKPOINT",""));
20 : 12 : call(set_environment_variable("TESTITALL_TEST_ENV_HASH_CHECKPOINT_EXIT_CODE",""));
21 : :
22 : 12 : provide(status);
23 : : }
24 : :
25 : : /**
26 : : * @brief Enable the random-byte checkpoint hook for one application run
27 : : *
28 : : * @param[in] exit_after_checkpoint Enable abrupt process termination after checkpoint
29 : : *
30 : : * @return Return status code
31 : : */
32 : 6 : static Return test0038_enable_checkpoint_hook(const bool exit_after_checkpoint)
33 : : {
34 : : /* Status returned by this function through provide()
35 : : Default value assumes successful completion */
36 : 6 : Return status = SUCCESS;
37 : :
38 : 6 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_HASH_CHECKPOINT_AT_RANDOM_BYTE","true"));
39 : 6 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_HASH_CHECKPOINT_EXIT_CODE","77"));
40 : :
41 [ + + ]: 6 : if(exit_after_checkpoint == true)
42 : : {
43 : 3 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_EXIT_AFTER_HASH_CHECKPOINT","true"));
44 : :
45 : : } else {
46 : 3 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_EXIT_AFTER_HASH_CHECKPOINT",""));
47 : : }
48 : :
49 : 6 : provide(status);
50 : : }
51 : :
52 : : /**
53 : : * @brief Remove artifacts used by one checkpoint test case
54 : : *
55 : : * @param[in] db_filename Database filename relative to TMPDIR
56 : : *
57 : : * @return Return status code
58 : : */
59 : 5 : static Return test0038_cleanup_case(const char *db_filename)
60 : : {
61 : : /* Status returned by this function through provide()
62 : : Default value assumes successful completion */
63 : 5 : Return status = SUCCESS;
64 : :
65 : 5 : call(test0038_reset_checkpoint_hooks());
66 : 5 : call(delete_path_if_present(db_filename));
67 : 5 : call(delete_path_if_present(test0038_fixture_root));
68 : :
69 : 5 : provide(status);
70 : : }
71 : :
72 : : /**
73 : : * @brief Subtest 1
74 : : *
75 : : * @details
76 : : * Forces the application to save a SHA512 checkpoint at a random byte while
77 : : * the same process keeps running. The test then checks that the final database
78 : : * row contains the completed checksum, no resume context, and a digest that
79 : : * matches the Monocypher oracle
80 : : *
81 : : * @return Return status code
82 : : */
83 : 1 : static Return test0038_1(void)
84 : : {
85 : 1 : INITTEST;
86 : :
87 : 1 : const char *db_filename = "0038_checkpoint_finalize.db";
88 : 1 : const char *arguments = "--database=0038_checkpoint_finalize.db tests/fixtures/huge";
89 : 1 : m_create(char,huge_file_path,MEMORY_STRING);
90 : 1 : struct stat huge_file_stat = {0};
91 : 1 : int row_count = 0;
92 : 1 : sqlite3_int64 file_id = 0;
93 : :
94 : : /* Prepare one isolated huge file and enable a checkpoint without process exit */
95 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
96 : 1 : ASSERT(SUCCESS == prepare_huge_fixture(huge_file_path,&huge_file_stat));
97 : 1 : ASSERT(huge_file_stat.st_size > 0);
98 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(false));
99 : :
100 : : /* Let the real application finish after writing the forced checkpoint */
101 : 1 : ASSERT(SUCCESS == runit(arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
102 : :
103 : : /* Verify that the checkpoint was finalized into one clean DB row */
104 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
105 : 1 : ASSERT(row_count == 1);
106 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&file_id));
107 : 1 : ASSERT(file_id > 0);
108 : 1 : ASSERT(SUCCESS == db_final_sha512_matches_file(db_filename,test0038_relative_path,m_text(huge_file_path)));
109 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
110 : :
111 : : /* Always clear hook state and remove per-case artifacts */
112 : 1 : call(test0038_cleanup_case(db_filename));
113 : :
114 : 1 : m_del(huge_file_path);
115 : :
116 : 1 : RETURN_STATUS;
117 : : }
118 : :
119 : : /**
120 : : * @brief Subtest 2
121 : : *
122 : : * @details
123 : : * Simulates a sudden process exit immediately after a durable SHA512
124 : : * checkpoint. The next application run must resume from the saved offset and
125 : : * context, finish the hash, clear the resume state, and store a checksum that
126 : : * matches the Monocypher oracle
127 : : *
128 : : * @return Return status code
129 : : */
130 : 1 : static Return test0038_2(void)
131 : : {
132 : 1 : INITTEST;
133 : :
134 : 1 : const char *db_filename = "0038_checkpoint_crash_resume.db";
135 : 1 : const char *first_arguments = "--database=0038_checkpoint_crash_resume.db tests/fixtures/huge";
136 : 1 : const char *second_arguments = "--update --database=0038_checkpoint_crash_resume.db tests/fixtures/huge";
137 : 1 : m_create(char,huge_file_path,MEMORY_STRING);
138 : 1 : struct stat huge_file_stat = {0};
139 : 1 : int row_count = 0;
140 : 1 : sqlite3_int64 checkpoint_offset = 0;
141 : 1 : int checkpoint_md_context_bytes = 0;
142 : :
143 : : /* Prepare the fixture and make the application exit after saving a checkpoint */
144 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
145 : 1 : ASSERT(SUCCESS == prepare_huge_fixture(huge_file_path,&huge_file_stat));
146 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(true));
147 : :
148 : : /* The controlled exit code proves that the crash hook, not a test timeout, stopped the process */
149 : 1 : ASSERT(SUCCESS == runit_background(
150 : : first_arguments,
151 : : NULL,
152 : : NULL,
153 : : test0038_checkpoint_exit_code,
154 : : ALLOW_BOTH | STDERR_ALLOW,
155 : : 0U,
156 : : 5000U,
157 : : 0,
158 : : 0U));
159 : :
160 : : /* Inspect the durable partial SHA512 state left by the interrupted run */
161 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
162 : 1 : ASSERT(row_count == 1);
163 : 1 : ASSERT(SUCCESS == read_resume_state_from_db(db_filename,test0038_relative_path,&checkpoint_offset,&checkpoint_md_context_bytes));
164 : 1 : ASSERT(checkpoint_offset > 0);
165 : 1 : ASSERT(checkpoint_offset < (sqlite3_int64)huge_file_stat.st_size);
166 : 1 : ASSERT(checkpoint_md_context_bytes > 0);
167 : :
168 : : /* Restart without crash hooks and finish from the saved checkpoint */
169 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
170 : 1 : ASSERT(SUCCESS == runit(second_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
171 : :
172 : : /* Verify that resume ended in a clean final checksum state */
173 : 1 : ASSERT(SUCCESS == db_final_sha512_matches_file(db_filename,test0038_relative_path,m_text(huge_file_path)));
174 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
175 : :
176 : : /* Always clear hook state and remove per-case artifacts */
177 : 1 : call(test0038_cleanup_case(db_filename));
178 : :
179 : 1 : m_del(huge_file_path);
180 : :
181 : 1 : RETURN_STATUS;
182 : : }
183 : :
184 : : /**
185 : : * @brief Subtest 3
186 : : *
187 : : * @details
188 : : * Crashes once during initial hashing and once during resumed hashing. Both
189 : : * partial checkpoints must update the same database row, the second offset
190 : : * must move forward, and the final run must finish that same row with a valid
191 : : * SHA512 digest
192 : : *
193 : : * @return Return status code
194 : : */
195 : 1 : static Return test0038_3(void)
196 : : {
197 : 1 : INITTEST;
198 : :
199 : 1 : const char *db_filename = "0038_checkpoint_update_same_row.db";
200 : 1 : const char *first_arguments = "--database=0038_checkpoint_update_same_row.db tests/fixtures/huge";
201 : 1 : const char *update_arguments = "--update --database=0038_checkpoint_update_same_row.db tests/fixtures/huge";
202 : 1 : m_create(char,huge_file_path,MEMORY_STRING);
203 : 1 : struct stat huge_file_stat = {0};
204 : 1 : int row_count = 0;
205 : 1 : sqlite3_int64 first_id = 0;
206 : 1 : sqlite3_int64 second_id = 0;
207 : 1 : sqlite3_int64 final_id = 0;
208 : 1 : sqlite3_int64 first_offset = 0;
209 : 1 : sqlite3_int64 second_offset = 0;
210 : 1 : int first_md_context_bytes = 0;
211 : 1 : int second_md_context_bytes = 0;
212 : :
213 : : /* Create the first durable partial checkpoint in a fresh database row */
214 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
215 : 1 : ASSERT(SUCCESS == prepare_huge_fixture(huge_file_path,&huge_file_stat));
216 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(true));
217 : 1 : ASSERT(SUCCESS == runit_background(
218 : : first_arguments,
219 : : NULL,
220 : : NULL,
221 : : test0038_checkpoint_exit_code,
222 : : ALLOW_BOTH | STDERR_ALLOW,
223 : : 0U,
224 : : 5000U,
225 : : 0,
226 : : 0U));
227 : :
228 : : /* Record the row identity and first saved offset after the first crash */
229 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
230 : 1 : ASSERT(row_count == 1);
231 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&first_id));
232 : 1 : ASSERT(SUCCESS == read_resume_state_from_db(db_filename,test0038_relative_path,&first_offset,&first_md_context_bytes));
233 : 1 : ASSERT(first_id > 0);
234 : 1 : ASSERT(first_offset > 0);
235 : 1 : ASSERT(first_offset < (sqlite3_int64)huge_file_stat.st_size);
236 : 1 : ASSERT(first_offset < ((sqlite3_int64)huge_file_stat.st_size - 1));
237 : 1 : ASSERT(first_md_context_bytes > 0);
238 : :
239 : : /* Resume once, checkpoint again, and crash after updating the same row */
240 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(true));
241 : 1 : ASSERT(SUCCESS == runit_background(
242 : : update_arguments,
243 : : NULL,
244 : : NULL,
245 : : test0038_checkpoint_exit_code,
246 : : ALLOW_BOTH | STDERR_ALLOW,
247 : : 0U,
248 : : 5000U,
249 : : 0,
250 : : 0U));
251 : :
252 : : /* The second checkpoint must keep row identity and advance the offset */
253 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
254 : 1 : ASSERT(row_count == 1);
255 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&second_id));
256 : 1 : ASSERT(SUCCESS == read_resume_state_from_db(db_filename,test0038_relative_path,&second_offset,&second_md_context_bytes));
257 : 1 : ASSERT(second_id == first_id);
258 : 1 : ASSERT(second_offset > first_offset);
259 : 1 : ASSERT(second_offset < (sqlite3_int64)huge_file_stat.st_size);
260 : 1 : ASSERT(second_md_context_bytes > 0);
261 : :
262 : : /* Finish from the second checkpoint and verify that the same row becomes final */
263 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
264 : 1 : ASSERT(SUCCESS == runit(update_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
265 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&final_id));
266 : 1 : ASSERT(final_id == first_id);
267 : 1 : ASSERT(SUCCESS == db_final_sha512_matches_file(db_filename,test0038_relative_path,m_text(huge_file_path)));
268 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
269 : :
270 : : /* Always clear hook state and remove per-case artifacts */
271 : 1 : call(test0038_cleanup_case(db_filename));
272 : :
273 : 1 : m_del(huge_file_path);
274 : :
275 : 1 : RETURN_STATUS;
276 : : }
277 : :
278 : : /**
279 : : * @brief Subtest 4
280 : : *
281 : : * @details
282 : : * Verifies that a completed checksum-locked row cannot be replaced by a
283 : : * temporary checkpoint state. Even when `--rehash-locked` reads the file again,
284 : : * the database must keep the trusted final checksum and must not store a
285 : : * partial offset or SHA512 context
286 : : *
287 : : * @return Return status code
288 : : */
289 : 1 : static Return test0038_4(void)
290 : : {
291 : 1 : INITTEST;
292 : :
293 : 1 : const char *db_filename = "0038_checkpoint_lock_checksum.db";
294 : 1 : const char *create_arguments = "--database=0038_checkpoint_lock_checksum.db "
295 : : "--lock-checksum=\"^hugetestfile$\" tests/fixtures/huge";
296 : 1 : const char *update_arguments = "--update --rehash-locked --database=0038_checkpoint_lock_checksum.db "
297 : : "--lock-checksum=\"^hugetestfile$\" tests/fixtures/huge";
298 : 1 : m_create(char,huge_file_path,MEMORY_STRING);
299 : 1 : struct stat huge_file_stat = {0};
300 : 1 : sqlite3_int64 id_before = 0;
301 : 1 : sqlite3_int64 id_after = 0;
302 : 1 : sqlite3_int64 offset_before = -1;
303 : 1 : sqlite3_int64 offset_after = -1;
304 : 1 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
305 : 1 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
306 : :
307 : : /* Create the sealed checksum-locked baseline row */
308 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
309 : 1 : ASSERT(SUCCESS == prepare_huge_fixture(huge_file_path,&huge_file_stat));
310 : 1 : ASSERT(huge_file_stat.st_size > 0);
311 : 1 : ASSERT(SUCCESS == runit(create_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
312 : :
313 : : /* Capture the trusted final state before the forced checkpoint hook runs */
314 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&id_before));
315 : 1 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,test0038_relative_path,&offset_before,sha512_before));
316 : 1 : ASSERT(offset_before == 0);
317 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
318 : 1 : ASSERT(SUCCESS == db_final_sha512_matches_file(db_filename,test0038_relative_path,m_text(huge_file_path)));
319 : :
320 : : /* Rehash the locked file while the checkpoint hook tries to save partial state */
321 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(false));
322 : 1 : ASSERT(SUCCESS == runit(update_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
323 : :
324 : : /* The locked row must remain final and byte-for-byte unchanged */
325 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&id_after));
326 : 1 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,test0038_relative_path,&offset_after,sha512_after));
327 : 1 : ASSERT(id_after == id_before);
328 : 1 : ASSERT(offset_after == 0);
329 : 1 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
330 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
331 : :
332 : : /* Always clear hook state and remove per-case artifacts */
333 : 1 : call(test0038_cleanup_case(db_filename));
334 : :
335 : 1 : m_del(huge_file_path);
336 : :
337 : 1 : RETURN_STATUS;
338 : : }
339 : :
340 : : /**
341 : : * @brief Subtest 5
342 : : *
343 : : * @details
344 : : * Checks that `--dry-run=with-checksums` may calculate SHA512 data but still
345 : : * leaves persistent database state untouched. A forced checkpoint during the
346 : : * dry run must not change the row ID, final offset, final digest, or resume
347 : : * context stored by the earlier real run
348 : : *
349 : : * @return Return status code
350 : : */
351 : 1 : static Return test0038_5(void)
352 : : {
353 : 1 : INITTEST;
354 : :
355 : 1 : const char *db_filename = "0038_checkpoint_dry_run.db";
356 : 1 : const char *create_arguments = "--database=0038_checkpoint_dry_run.db tests/fixtures/huge";
357 : 1 : const char *dry_run_arguments = "--dry-run=with-checksums --update --database=0038_checkpoint_dry_run.db tests/fixtures/huge";
358 : 1 : m_create(char,huge_file_path,MEMORY_STRING);
359 : 1 : struct stat huge_file_stat = {0};
360 : 1 : int row_count = 0;
361 : 1 : sqlite3_int64 id_before = 0;
362 : 1 : sqlite3_int64 id_after = 0;
363 : 1 : sqlite3_int64 offset_before = -1;
364 : 1 : sqlite3_int64 offset_after = -1;
365 : 1 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
366 : 1 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
367 : :
368 : : /* Create a real final row that the later dry run must not change */
369 : 1 : ASSERT(SUCCESS == test0038_reset_checkpoint_hooks());
370 : 1 : ASSERT(SUCCESS == prepare_huge_fixture(huge_file_path,&huge_file_stat));
371 : 1 : ASSERT(huge_file_stat.st_size > 0);
372 : 1 : ASSERT(SUCCESS == runit(create_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
373 : :
374 : : /* Save the baseline DB identity and final checksum state */
375 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
376 : 1 : ASSERT(row_count == 1);
377 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&id_before));
378 : 1 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,test0038_relative_path,&offset_before,sha512_before));
379 : 1 : ASSERT(offset_before == 0);
380 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
381 : :
382 : : /* Force a checkpoint opportunity during checksum-enabled dry-run mode */
383 : 1 : ASSERT(SUCCESS == test0038_enable_checkpoint_hook(false));
384 : 1 : ASSERT(SUCCESS == runit(dry_run_arguments,NULL,NULL,COMPLETED,ALLOW_BOTH));
385 : :
386 : : /* Dry-run must leave the previously saved final DB state untouched */
387 : 1 : ASSERT(SUCCESS == db_read_files_count(db_filename,&row_count));
388 : 1 : ASSERT(row_count == 1);
389 : 1 : ASSERT(SUCCESS == db_read_file_id(db_filename,test0038_relative_path,&id_after));
390 : 1 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,test0038_relative_path,&offset_after,sha512_after));
391 : 1 : ASSERT(id_after == id_before);
392 : 1 : ASSERT(offset_after == 0);
393 : 1 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
394 : 1 : ASSERT(SUCCESS == db_resume_state_is_empty(db_filename,test0038_relative_path));
395 : :
396 : : /* Always clear hook state and remove per-case artifacts */
397 : 1 : call(test0038_cleanup_case(db_filename));
398 : :
399 : 1 : m_del(huge_file_path);
400 : :
401 : 1 : RETURN_STATUS;
402 : : }
403 : :
404 : : /**
405 : : * @brief Run SHA512 random-byte checkpoint and resume system subtests
406 : : *
407 : : * @details
408 : : * These subtests exercise the real application binary instead of calling
409 : : * internal hashing functions directly. They verify that periodic SHA512
410 : : * checkpoints are written only when persistent state is allowed, survive a
411 : : * controlled process exit, resume into the same database row, and disappear
412 : : * after the final checksum is saved. The suite is slow because it hashes the
413 : : * huge-file fixture several times and launches separate application processes
414 : : *
415 : : * @return Return status code
416 : : */
417 : 1 : Return test0038(void)
418 : : {
419 : 1 : INITTEST;
420 : :
421 : 1 : SLOWTEST;
422 : :
423 : 1 : TEST(test0038_1,"Random-byte checkpoint leaves only final state after one pass");
424 : 1 : TEST(test0038_2,"Abrupt exit after checkpoint persists resume state across restart");
425 : 1 : TEST(test0038_3,"Resume checkpoint updates the same row before finalization");
426 : 1 : TEST(test0038_4,"Checksum-locked sealed row rejects partial checkpoint state");
427 : 1 : TEST(test0038_5,"Dry-run with checksums does not persist checkpoint state");
428 : :
429 : 1 : RETURN_STATUS;
430 : : }
|