Branch data Line data Source code
1 : : #include "sute.h"
2 : : #include <errno.h>
3 : :
4 : : /**
5 : : * @brief Create an isolated database with the columns used by file saves
6 : : *
7 : : * Each scenario gets its own real SQLite file and a minimal files table.
8 : : * The connection uses EXTRA synchronization, DELETE journaling, and NORMAL
9 : : * locking so transaction behavior can be tested without application startup
10 : : *
11 : : * @param[in] filename Database filename relative to TMPDIR
12 : : * @param[out] test_config Configuration receiving the writable connection
13 : : * @return SUCCESS when the database is ready, otherwise FAILURE
14 : : */
15 : 8 : static Return open_database(
16 : : const char *filename,
17 : : Config *test_config)
18 : : {
19 : : /* Status returned by this function through provide()
20 : : Default value assumes successful completion */
21 : 8 : Return status = SUCCESS;
22 : :
23 : : /* Create a real database file for this scenario */
24 : 8 : ASSERT(SUCCESS == open_db_from_tmpdir(filename,
25 : : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,&test_config->db));
26 : :
27 : : /* Create the file-record schema and establish the writable connection settings */
28 : 8 : ASSERT(SQLITE_OK == sqlite3_exec(test_config->db,
29 : : "CREATE TABLE files (ID INTEGER PRIMARY KEY,offset INTEGER,"
30 : : "relative_path TEXT UNIQUE,sha512 BLOB,stat BLOB,mdContext BLOB);"
31 : : "PRAGMA synchronous=EXTRA;"
32 : : "PRAGMA journal_mode=DELETE;"
33 : : "PRAGMA locking_mode=NORMAL;",NULL,NULL,NULL));
34 : 8 : test_config->sqlite_open_flag = SQLITE_OPEN_READWRITE;
35 : :
36 : 8 : provide(status);
37 : : }
38 : :
39 : : /**
40 : : * @brief Check one integer result on the current database connection
41 : : *
42 : : * @param[in] sql Query returning one integer column and one row
43 : : * @param expected Expected column value
44 : : * @return SUCCESS when the query returns the expected value, otherwise FAILURE
45 : : */
46 : 18 : static Return expect_integer(
47 : : const char *sql,
48 : : const int expected)
49 : : {
50 : : /* Status returned by this function through provide()
51 : : Default value assumes successful completion */
52 : 18 : Return status = SUCCESS;
53 : 18 : sqlite3_stmt *statement = NULL;
54 : :
55 : : /* Read one integer and require the query to return exactly one row */
56 : 18 : ASSERT(SQLITE_OK == sqlite3_prepare_v2(config->db,sql,-1,&statement,NULL));
57 : 18 : ASSERT(SQLITE_ROW == sqlite3_step(statement));
58 : 18 : ASSERT(expected == sqlite3_column_int(statement,0));
59 : 18 : ASSERT(SQLITE_DONE == sqlite3_step(statement));
60 : :
61 : : /* Release the statement even if preparation or a result check failed */
62 : 18 : const int finalize_status = sqlite3_finalize(statement);
63 : 18 : ASSERT(SQLITE_OK == finalize_status);
64 : :
65 : 18 : provide(status);
66 : : }
67 : :
68 : : /**
69 : : * @brief Verify shared timing and visibility of multiple pending file saves
70 : : *
71 : : * Two file records must share one transaction and its original start time.
72 : : * A separate connection must see neither record before the deadline and both
73 : : * records after commit. Explicit timestamps exercise one nanosecond before
74 : : * the deadline and the deadline itself without sleeping. A successful commit
75 : : * must also remove the rollback journal in DELETE mode
76 : : *
77 : : * @return Return status for the transaction timing and persistence checks
78 : : */
79 : 1 : static Return test0042_1(void)
80 : : {
81 : 1 : INITTEST;
82 : 1 : Config *initial_config = config;
83 : 1 : Config test_config = {0};
84 : 1 : const char *database_filename = "0042_timed_batch.db";
85 : 1 : DBrow row = {0};
86 : 1 : File file = {.db = &row,.zero_size_file = true};
87 : 1 : bool path_known = false;
88 : 1 : int visible_files = -1;
89 : 1 : struct stat journal_stat = {0};
90 : 1 : m_create(char,relative_path,MEMORY_STRING);
91 : 1 : m_create(char,journal_path,MEMORY_STRING);
92 : :
93 : : /* Save the first file record and verify that it starts a transaction */
94 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
95 : 1 : config = &test_config;
96 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"first"));
97 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
98 : 1 : ASSERT(test_config.file_transaction_active == true);
99 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) == 0);
100 : 1 : ASSERT(SUCCESS == expect_integer(
101 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
102 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
103 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
104 : :
105 : : /* Save another record in the same batch without moving its start time */
106 : 1 : const long long int started_ns = test_config.file_transaction_started_ns;
107 : 1 : ASSERT(started_ns > 0);
108 : 1 : row = (DBrow){0};
109 : 1 : file = (File){.db = &row,.zero_size_file = true};
110 : 1 : path_known = false;
111 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"second"));
112 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
113 : 1 : ASSERT(test_config.file_transaction_started_ns == started_ns);
114 : :
115 : : /* A separate connection must not see either pending record */
116 : 1 : ASSERT(SUCCESS == db_read_files_count(database_filename,&visible_files));
117 : 1 : ASSERT(visible_files == 0);
118 : :
119 : : /* Supply a time just before the deadline; the batch must remain pending */
120 : 1 : ASSERT(SUCCESS == db_file_transaction_check(started_ns + DB_CHECKPOINT_INTERVAL_NS - 1LL));
121 : 1 : ASSERT(test_config.file_transaction_active == true);
122 : 1 : ASSERT(SUCCESS == db_read_files_count(database_filename,&visible_files));
123 : 1 : ASSERT(visible_files == 0);
124 : :
125 : : /* At the deadline, both records must become visible to another connection */
126 : 1 : ASSERT(SUCCESS == db_file_transaction_check(started_ns + DB_CHECKPOINT_INTERVAL_NS));
127 : 1 : ASSERT(test_config.file_transaction_active == false);
128 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
129 : 1 : ASSERT(SUCCESS == db_read_files_count(database_filename,&visible_files));
130 : 1 : ASSERT(visible_files == 2);
131 : :
132 : : /* DELETE mode must remove the journal when the transaction commits */
133 : 1 : ASSERT(SUCCESS == construct_path("0042_timed_batch.db-journal",journal_path));
134 : 1 : errno = 0;
135 : 1 : ASSERT(stat(m_text(journal_path),&journal_stat) == -1);
136 : 1 : ASSERT(errno == ENOENT);
137 : :
138 : : /* A cleanup rollback after commit must succeed without an active transaction */
139 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
140 : :
141 : : /* Restore shared state and close the test connection even after a failed check */
142 : 1 : config = initial_config;
143 : 1 : const int close_status = sqlite3_close(test_config.db);
144 : 1 : ASSERT(SQLITE_OK == close_status);
145 : :
146 : : /* Remove the temporary database and release the scenario buffers */
147 : 1 : call(delete_path_if_present(database_filename));
148 : 1 : call(m_del(journal_path));
149 : 1 : call(m_del(relative_path));
150 : :
151 : 1 : RETURN_STATUS;
152 : : }
153 : :
154 : : /**
155 : : * @brief Commit a completed batch and preserve the database settings
156 : : *
157 : : * The test directly calls commit and then rollback to model normal traversal
158 : : * cleanup. The record must remain saved, SQLite must leave the transaction,
159 : : * and connection settings must stay unchanged. The successful write keeps the
160 : : * database modification flag set
161 : : *
162 : : * @return Return status for normal completion
163 : : */
164 : 1 : static Return test0042_2(void)
165 : : {
166 : 1 : INITTEST;
167 : 1 : Config *initial_config = config;
168 : 1 : Config test_config = {0};
169 : 1 : const char *database_filename = "0042_completed_batch.db";
170 : 1 : DBrow row = {0};
171 : 1 : File file = {.db = &row,.zero_size_file = true};
172 : 1 : bool path_known = false;
173 : 1 : m_create(char,relative_path,MEMORY_STRING);
174 : :
175 : : /* Begin with an unmodified database and save one pending record */
176 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
177 : 1 : config = &test_config;
178 : 1 : ASSERT(test_config.db_primary_file_modified == false);
179 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"completed"));
180 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
181 : 1 : ASSERT(test_config.file_transaction_active == true);
182 : 1 : ASSERT(test_config.db_primary_file_modified == true);
183 : :
184 : : /* Model normal completion: commit the batch, then run rollback cleanup */
185 : 1 : ASSERT(SUCCESS == db_file_transaction_commit());
186 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
187 : :
188 : : /* The committed record must survive cleanup and the settings must be preserved */
189 : 1 : ASSERT(test_config.file_transaction_active == false);
190 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
191 : 1 : ASSERT(test_config.db_primary_file_modified == true);
192 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",1));
193 : 1 : ASSERT(SUCCESS == expect_integer(
194 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
195 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
196 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
197 : :
198 : : /* Restore shared state and close the test connection even after a failed check */
199 : 1 : config = initial_config;
200 : 1 : const int close_status = sqlite3_close(test_config.db);
201 : 1 : ASSERT(SQLITE_OK == close_status);
202 : :
203 : : /* Remove the temporary database and release the scenario buffers */
204 : 1 : call(delete_path_if_present(database_filename));
205 : 1 : call(m_del(relative_path));
206 : :
207 : 1 : RETURN_STATUS;
208 : : }
209 : :
210 : : /**
211 : : * @brief Save and commit a file while graceful interruption is active
212 : : *
213 : : * A previously committed record establishes the starting state. A second
214 : : * record is saved and committed with HALTED and the interrupt flag set.
215 : : * Each operation must return SUCCESS | HALTED and both records must remain.
216 : : * Results are kept separately from the test status, and process globals are
217 : : * restored before checking those results or performing other test operations.
218 : : *
219 : : * The flags are set directly to isolate behavior after interruption. Signal
220 : : * delivery and completion of the filesystem traversal are outside this test
221 : : *
222 : : * @return Return status for interrupted completion
223 : : */
224 : 1 : static Return test0042_3(void)
225 : : {
226 : 1 : INITTEST;
227 : 1 : Config *initial_config = config;
228 : 1 : Config test_config = {0};
229 : 1 : const Return initial_global_status = atomic_load(&global_return_status);
230 : 1 : const bool initial_interrupt_flag = atomic_load(&global_interrupt_flag);
231 : 1 : const char *database_filename = "0042_interrupted_batch.db";
232 : 1 : DBrow row = {0};
233 : 1 : File file = {.db = &row,.zero_size_file = true};
234 : 1 : bool path_known = false;
235 : 1 : Return interrupted_save_status = FAILURE;
236 : 1 : Return interrupted_commit_status = FAILURE;
237 : 1 : m_create(char,relative_path,MEMORY_STRING);
238 : :
239 : : /* Commit a baseline record before simulating interruption */
240 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
241 : 1 : config = &test_config;
242 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"completed"));
243 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
244 : 1 : ASSERT(SUCCESS == db_file_transaction_commit());
245 : 1 : ASSERT(test_config.db_primary_file_modified == true);
246 : :
247 : : /* Prepare another independent file record for the interrupted save */
248 : 1 : row = (DBrow){0};
249 : 1 : file = (File){.db = &row,.zero_size_file = true};
250 : 1 : path_known = false;
251 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"interrupted"));
252 : :
253 : : /* Set the state produced by graceful interruption and save directly.
254 : : Keep the returned HALTED flag separate from the test status */
255 [ + - ]: 1 : if(SUCCESS == status)
256 : : {
257 : 1 : atomic_store(&global_return_status,HALTED);
258 : 1 : atomic_store(&global_interrupt_flag,true);
259 : 1 : interrupted_save_status = db_save_file_record(relative_path,&file,&path_known,true);
260 : : }
261 : :
262 : : /* Restore globals before checking the saved result so simulated interruption
263 : : cannot affect later helper returns or escape into the test runner */
264 : 1 : atomic_store(&global_return_status,initial_global_status);
265 : 1 : atomic_store(&global_interrupt_flag,initial_interrupt_flag);
266 : 1 : ASSERT(interrupted_save_status == (SUCCESS | HALTED));
267 : 1 : ASSERT(test_config.file_transaction_active == true);
268 : 1 : ASSERT(test_config.db_primary_file_modified == true);
269 : :
270 : : /* Commit must also run successfully while the same interruption flags are set */
271 [ + - ]: 1 : if(SUCCESS == status)
272 : : {
273 : 1 : atomic_store(&global_return_status,HALTED);
274 : 1 : atomic_store(&global_interrupt_flag,true);
275 : 1 : interrupted_commit_status = db_file_transaction_commit();
276 : : }
277 : :
278 : : /* Restore both globals even if the commit failed, then check its full status */
279 : 1 : atomic_store(&global_return_status,initial_global_status);
280 : 1 : atomic_store(&global_interrupt_flag,initial_interrupt_flag);
281 : 1 : ASSERT(interrupted_commit_status == (SUCCESS | HALTED));
282 : :
283 : : /* Cleanup must preserve both committed records and the connection settings */
284 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
285 : 1 : ASSERT(test_config.file_transaction_active == false);
286 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
287 : 1 : ASSERT(test_config.db_primary_file_modified == true);
288 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",2));
289 : 1 : ASSERT(SUCCESS == expect_integer(
290 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
291 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
292 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
293 : :
294 : : /* Restore shared state and close the test connection even after a failed check */
295 : 1 : config = initial_config;
296 : 1 : const int close_status = sqlite3_close(test_config.db);
297 : 1 : ASSERT(SQLITE_OK == close_status);
298 : :
299 : : /* Remove the temporary database and release the scenario buffers */
300 : 1 : call(delete_path_if_present(database_filename));
301 : 1 : call(m_del(relative_path));
302 : :
303 : 1 : RETURN_STATUS;
304 : : }
305 : :
306 : : /**
307 : : * @brief Roll back pending writes without losing an earlier committed batch
308 : : *
309 : : * Two records are committed before a third record starts another transaction.
310 : : * An explicit rollback must remove only the third record and keep the database
311 : : * modification flag set. This models cleanup after a technical failure by
312 : : * calling rollback directly; no traversal failure is injected
313 : : *
314 : : * @return Return status for rollback after a committed batch
315 : : */
316 : 1 : static Return test0042_4(void)
317 : : {
318 : 1 : INITTEST;
319 : 1 : Config *initial_config = config;
320 : 1 : Config test_config = {0};
321 : 1 : const char *database_filename = "0042_rollback_after_commit.db";
322 : 1 : DBrow row = {0};
323 : 1 : File file = {.db = &row,.zero_size_file = true};
324 : 1 : bool path_known = false;
325 : 1 : m_create(char,relative_path,MEMORY_STRING);
326 : :
327 : : /* Save and commit two records to establish an earlier completed batch */
328 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
329 : 1 : config = &test_config;
330 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"first"));
331 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
332 : 1 : row = (DBrow){0};
333 : 1 : file = (File){.db = &row,.zero_size_file = true};
334 : 1 : path_known = false;
335 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"second"));
336 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
337 : 1 : ASSERT(SUCCESS == db_file_transaction_commit());
338 : 1 : ASSERT(test_config.db_primary_file_modified == true);
339 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",2));
340 : :
341 : : /* Start another batch; the writer can see its third, uncommitted record */
342 : 1 : row = (DBrow){0};
343 : 1 : file = (File){.db = &row,.zero_size_file = true};
344 : 1 : path_known = false;
345 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"pending"));
346 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
347 : 1 : ASSERT(test_config.file_transaction_active == true);
348 : 1 : ASSERT(test_config.db_primary_file_modified == true);
349 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",3));
350 : :
351 : : /* Explicitly roll back the current batch and keep the two committed records */
352 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
353 : 1 : ASSERT(test_config.file_transaction_active == false);
354 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
355 : 1 : ASSERT(test_config.db_primary_file_modified == true);
356 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",2));
357 : 1 : ASSERT(SUCCESS == expect_integer(
358 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
359 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
360 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
361 : :
362 : : /* Restore shared state and close the test connection even after a failed check */
363 : 1 : config = initial_config;
364 : 1 : const int close_status = sqlite3_close(test_config.db);
365 : 1 : ASSERT(SQLITE_OK == close_status);
366 : :
367 : : /* Remove the temporary database and release the scenario buffers */
368 : 1 : call(delete_path_if_present(database_filename));
369 : 1 : call(m_del(relative_path));
370 : :
371 : 1 : RETURN_STATUS;
372 : : }
373 : :
374 : : /**
375 : : * @brief Preserve the modification flag when the first changed batch rolls back
376 : : *
377 : : * Setup inserts existing rows directly through SQLite, leaving the application's
378 : : * modification flag clear. Saving a new file record must set that flag.
379 : : * Rollback removes the new record but preserves the existing rows and the flag:
380 : : * the flag records that a write succeeded during the run, including writes
381 : : * whose transaction was later rolled back
382 : : *
383 : : * @return Return status for rollback with an initially clear modification flag
384 : : */
385 : 1 : static Return test0042_5(void)
386 : : {
387 : 1 : INITTEST;
388 : 1 : Config *initial_config = config;
389 : 1 : Config test_config = {0};
390 : 1 : const char *database_filename = "0042_rollback_initial_batch.db";
391 : 1 : DBrow row = {0};
392 : 1 : File file = {.db = &row,.zero_size_file = true};
393 : 1 : bool path_known = false;
394 : 1 : m_create(char,relative_path,MEMORY_STRING);
395 : :
396 : : /* Seed existing records directly so setup does not set the application flag */
397 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
398 : 1 : config = &test_config;
399 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(test_config.db,
400 : : "INSERT INTO files(relative_path) VALUES('first'),('second');",NULL,NULL,NULL));
401 : 1 : ASSERT(test_config.db_primary_file_modified == false);
402 : :
403 : : /* The first application save must set the modification flag before commit */
404 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"pending"));
405 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
406 : 1 : ASSERT(test_config.file_transaction_active == true);
407 : 1 : ASSERT(test_config.db_primary_file_modified == true);
408 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",3));
409 : :
410 : : /* Rollback removes the pending record but leaves the flag set by that write */
411 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
412 : 1 : ASSERT(test_config.file_transaction_active == false);
413 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
414 : 1 : ASSERT(test_config.db_primary_file_modified == true);
415 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",2));
416 : 1 : ASSERT(SUCCESS == expect_integer(
417 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
418 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
419 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
420 : :
421 : : /* Restore shared state and close the test connection even after a failed check */
422 : 1 : config = initial_config;
423 : 1 : const int close_status = sqlite3_close(test_config.db);
424 : 1 : ASSERT(SQLITE_OK == close_status);
425 : :
426 : : /* Remove the temporary database and release the scenario buffers */
427 : 1 : call(delete_path_if_present(database_filename));
428 : 1 : call(m_del(relative_path));
429 : :
430 : 1 : RETURN_STATUS;
431 : : }
432 : :
433 : : /**
434 : : * @brief Keep writable dry-run saves outside transactions
435 : : *
436 : : * A writable connection makes the application's dry-run guard responsible for
437 : : * preventing writes. The test calls begin, file save, deadline check, commit,
438 : : * and rollback, then verifies an empty table, no active transaction, and the
439 : : * original connection settings. Whole-file SHA512 values before the sequence
440 : : * and after closing the connection must match
441 : : *
442 : : * @return Return status for writable dry-run configuration and file contents
443 : : */
444 : 1 : static Return test0042_6(void)
445 : : {
446 : 1 : INITTEST;
447 : 1 : Config *initial_config = config;
448 : 1 : Config test_config = {0};
449 : 1 : const char *database_filename = "0042_writable_dry_run.db";
450 : 1 : DBrow row = {0};
451 : 1 : File file = {.db = &row,.zero_size_file = true};
452 : 1 : bool path_known = false;
453 : 1 : unsigned char checksum_before[SHA512_DIGEST_LENGTH] = {0};
454 : 1 : unsigned char checksum_after[SHA512_DIGEST_LENGTH] = {0};
455 : 1 : m_create(char,relative_path,MEMORY_STRING);
456 : 1 : m_create(char,database_path,MEMORY_STRING);
457 : :
458 : : /* Prepare a writable database and hash its complete initial contents */
459 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
460 : 1 : ASSERT(SUCCESS == construct_path(database_filename,database_path));
461 : 1 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(database_path),checksum_before));
462 : 1 : config = &test_config;
463 : : /* Exercise the transaction entry points and file save with dry-run enabled */
464 : 1 : test_config.dry_run = true;
465 : 1 : ASSERT(SUCCESS == db_file_transaction_begin());
466 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"simulated"));
467 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
468 : 1 : ASSERT(SUCCESS == db_file_transaction_check(cur_time_monotonic_ns() + DB_CHECKPOINT_INTERVAL_NS));
469 : 1 : ASSERT(SUCCESS == db_file_transaction_commit());
470 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
471 : :
472 : : /* No record or active transaction may remain, and settings must be unchanged */
473 : 1 : ASSERT(test_config.file_transaction_active == false);
474 : 1 : ASSERT(test_config.db_primary_file_modified == false);
475 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
476 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",0));
477 : 1 : ASSERT(SUCCESS == expect_integer(
478 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
479 : : " AND (SELECT journal_mode FROM pragma_journal_mode)='delete'"
480 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
481 : :
482 : : /* Restore shared state and close the test connection even after a failed check */
483 : 1 : config = initial_config;
484 : 1 : const int close_status = sqlite3_close(test_config.db);
485 : 1 : ASSERT(SQLITE_OK == close_status);
486 : :
487 : : /* After closing SQLite, compare the entire database file with the original */
488 : 1 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(database_path),checksum_after));
489 : 1 : ASSERT(memcmp(checksum_before,checksum_after,sizeof(checksum_before)) == 0);
490 : :
491 : : /* Remove the temporary database and release the scenario buffers */
492 : 1 : call(delete_path_if_present(database_filename));
493 : 1 : call(m_del(database_path));
494 : 1 : call(m_del(relative_path));
495 : :
496 : 1 : RETURN_STATUS;
497 : : }
498 : :
499 : : /**
500 : : * @brief Keep read-only dry-run saves outside transactions
501 : : *
502 : : * The prepared database is reopened with the application's read-only
503 : : * synchronization and locking settings. The dry-run calls must succeed while
504 : : * leaving those settings intact, adding no rows, and leaving no transaction
505 : : * active. Whole-file SHA512 values before the sequence and after closing the
506 : : * connection must match
507 : : *
508 : : * @return Return status for read-only dry-run configuration and file contents
509 : : */
510 : 1 : static Return test0042_7(void)
511 : : {
512 : 1 : INITTEST;
513 : 1 : Config *initial_config = config;
514 : 1 : Config test_config = {0};
515 : 1 : const char *database_filename = "0042_readonly_dry_run.db";
516 : 1 : DBrow row = {0};
517 : 1 : File file = {.db = &row,.zero_size_file = true};
518 : 1 : bool path_known = false;
519 : 1 : unsigned char checksum_before[SHA512_DIGEST_LENGTH] = {0};
520 : 1 : unsigned char checksum_after[SHA512_DIGEST_LENGTH] = {0};
521 : 1 : m_create(char,relative_path,MEMORY_STRING);
522 : 1 : m_create(char,database_path,MEMORY_STRING);
523 : :
524 : : /* Create the database through a writable connection before reopening it read-only */
525 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
526 : 1 : const int setup_close_status = sqlite3_close(test_config.db);
527 : 1 : ASSERT(SQLITE_OK == setup_close_status);
528 : :
529 : : /* Clear a closed handle to prevent a second close. If closing failed, keep
530 : : the handle available for the unconditional cleanup below */
531 [ + - ]: 1 : if(SQLITE_OK == setup_close_status)
532 : : {
533 : 1 : test_config.db = NULL;
534 : : }
535 : :
536 : : /* Hash the prepared file before any dry-run operation */
537 : 1 : ASSERT(SUCCESS == construct_path(database_filename,database_path));
538 : 1 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(database_path),checksum_before));
539 : 1 : config = &test_config;
540 : : /* Reopen read-only so the dry-run calls must work without write access */
541 : 1 : test_config.dry_run = true;
542 : 1 : test_config.sqlite_open_flag = SQLITE_OPEN_READONLY;
543 : 1 : ASSERT(SUCCESS == open_db_from_tmpdir(database_filename,SQLITE_OPEN_READONLY,&test_config.db));
544 : :
545 : : /* Use the read-only settings from db_init(). Dry-run must preserve them
546 : : instead of applying the writable connection settings */
547 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(test_config.db,
548 : : "PRAGMA synchronous=OFF;PRAGMA locking_mode=EXCLUSIVE;",NULL,NULL,NULL));
549 : :
550 : : /* Exercise the same transaction and save calls as the writable dry-run case */
551 : 1 : ASSERT(SUCCESS == db_file_transaction_begin());
552 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"simulated"));
553 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
554 : 1 : ASSERT(SUCCESS == db_file_transaction_check(cur_time_monotonic_ns() + DB_CHECKPOINT_INTERVAL_NS));
555 : 1 : ASSERT(SUCCESS == db_file_transaction_commit());
556 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
557 : :
558 : : /* Require no active transaction, no inserted row, and unchanged settings */
559 : 1 : ASSERT(test_config.file_transaction_active == false);
560 : 1 : ASSERT(test_config.db_primary_file_modified == false);
561 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
562 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",0));
563 : 1 : ASSERT(SUCCESS == expect_integer(
564 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=0"
565 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='exclusive';",1));
566 : :
567 : : /* Restore shared state and close the test connection even after a failed check */
568 : 1 : config = initial_config;
569 : 1 : const int close_status = sqlite3_close(test_config.db);
570 : 1 : ASSERT(SQLITE_OK == close_status);
571 : :
572 : : /* After closing SQLite, verify that the complete file contents are unchanged */
573 : 1 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(database_path),checksum_after));
574 : 1 : ASSERT(memcmp(checksum_before,checksum_after,sizeof(checksum_before)) == 0);
575 : :
576 : : /* Remove the temporary database and release the scenario buffers */
577 : 1 : call(delete_path_if_present(database_filename));
578 : 1 : call(m_del(database_path));
579 : 1 : call(m_del(relative_path));
580 : :
581 : 1 : RETURN_STATUS;
582 : : }
583 : :
584 : : static Return commit_status = SUCCESS;
585 : :
586 : : /**
587 : : * @brief Capture the expected diagnostic from a blocked transaction commit
588 : : *
589 : : * The capture callback has no return value, so the commit result is saved
590 : : * separately to distinguish the expected commit failure from a capture failure
591 : : */
592 : 1 : static void capture_commit(void)
593 : : {
594 : 1 : commit_status = db_file_transaction_commit();
595 : 1 : }
596 : :
597 : : /**
598 : : * @brief Roll back a batch after a real reader prevents COMMIT
599 : : *
600 : : * A separate connection keeps a read transaction open while the writer has
601 : : * a pending record. Its shared lock must make COMMIT fail with SQLITE_BUSY,
602 : : * leaving the writer's transaction active. An explicit rollback must then
603 : : * discard the pending record and end that transaction while the reader is
604 : : * still open. The write keeps the database modification flag set
605 : : *
606 : : * @return Return status for the failed-commit cleanup checks
607 : : */
608 : 1 : static Return test0042_8(void)
609 : : {
610 : 1 : INITTEST;
611 : 1 : Config *initial_config = config;
612 : 1 : Config test_config = {0};
613 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
614 : 1 : const char *database_filename = "0042_blocked_commit.db";
615 : 1 : DBrow row = {0};
616 : 1 : File file = {.db = &row,.zero_size_file = true};
617 : 1 : bool path_known = false;
618 : 1 : sqlite3 *reader = NULL;
619 : 1 : m_create(char,relative_path,MEMORY_STRING);
620 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
621 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
622 : :
623 : : /* Create a writer transaction containing one pending record */
624 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
625 : 1 : config = &test_config;
626 : 1 : rational_logger_mode = REGULAR;
627 : 1 : ASSERT(SUCCESS == m_copy_literal(relative_path,"pending"));
628 : 1 : ASSERT(SUCCESS == db_save_file_record(relative_path,&file,&path_known,true));
629 : :
630 : : /* BEGIN keeps the reader transaction open after SELECT acquires its shared
631 : : lock. Keeping this reader open prevents the writer from committing */
632 : 1 : ASSERT(SUCCESS == open_db_from_tmpdir(database_filename,SQLITE_OPEN_READONLY,&reader));
633 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(reader,"BEGIN;SELECT COUNT(*) FROM files;",NULL,NULL,NULL));
634 : :
635 : : /* Capture the expected error while keeping the commit result separate */
636 : 1 : commit_status = SUCCESS;
637 : 1 : ASSERT(SUCCESS == function_capture(capture_commit,captured_stdout,captured_stderr));
638 : 1 : ASSERT((commit_status & FAILURE) != 0);
639 : 1 : ASSERT(sqlite3_errcode(test_config.db) == SQLITE_BUSY);
640 : 1 : ASSERT(captured_stdout->length > 0U);
641 : 1 : ASSERT(captured_stderr->length == 0U);
642 : :
643 : : /* A blocked commit must leave the writer transaction available for rollback */
644 : 1 : ASSERT(test_config.file_transaction_active == true);
645 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) == 0);
646 : :
647 : : /* Roll back while the reader is still open; the successful write keeps
648 : : the modification flag set even though its row is discarded */
649 : 1 : ASSERT(SUCCESS == db_file_transaction_rollback());
650 : 1 : ASSERT(test_config.file_transaction_active == false);
651 : 1 : ASSERT(test_config.db_primary_file_modified == true);
652 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
653 : :
654 : : /* Close the reader even after a failed check, then verify the empty table */
655 : 1 : const int reader_close_status = sqlite3_close(reader);
656 : 1 : ASSERT(SQLITE_OK == reader_close_status);
657 : 1 : ASSERT(SUCCESS == expect_integer("SELECT COUNT(*) FROM files;",0));
658 : 1 : ASSERT(SUCCESS == expect_integer(
659 : : "SELECT (SELECT synchronous FROM pragma_synchronous)=3"
660 : : " AND (SELECT locking_mode FROM pragma_locking_mode)='normal';",1));
661 : :
662 : : /* Restore shared state and close the test connection even after a failed check */
663 : 1 : config = initial_config;
664 : 1 : rational_logger_mode = initial_logger_mode;
665 : 1 : commit_status = SUCCESS;
666 : 1 : const int close_status = sqlite3_close(test_config.db);
667 : 1 : ASSERT(SQLITE_OK == close_status);
668 : :
669 : : /* Remove the temporary database and release the scenario buffers */
670 : 1 : call(delete_path_if_present(database_filename));
671 : 1 : call(m_del(captured_stderr));
672 : 1 : call(m_del(captured_stdout));
673 : 1 : call(m_del(relative_path));
674 : :
675 : 1 : RETURN_STATUS;
676 : : }
677 : :
678 : : /**
679 : : * @brief Run file-transaction timing, completion, and no-write regressions
680 : : *
681 : : * The scenarios call file-save and transaction functions against independent
682 : : * SQLite files. File records represent empty files, so saving them requires
683 : : * no traversal or input-file reads. Pending records must become
684 : : * visible to another connection only after commit; rollback must discard the
685 : : * pending batch while preserving previously committed records.
686 : : *
687 : : * Explicit timestamps exercise the commit deadline without waiting. Directly
688 : : * assigned interruption flags check that saving and committing still work with
689 : : * HALTED. Dry-run cases compare database-file hashes and connection settings,
690 : : * and a real reader lock exercises recovery from a blocked commit.
691 : : *
692 : : * These checks cover the called functions. Signal delivery and the traversal's
693 : : * decision to call commit or rollback are outside their scope
694 : : *
695 : : * @return Return status for all transaction scenarios
696 : : */
697 : 1 : Return test0042(void)
698 : : {
699 : 1 : INITTEST;
700 : :
701 : 1 : TEST(test0042_1,"File batches become visible at their shared deadline");
702 : 1 : TEST(test0042_2,"Normal completion commits the pending file batch");
703 : 1 : TEST(test0042_3,"Graceful interruption allows file saves and commits");
704 : 1 : TEST(test0042_4,"Rollback preserves earlier committed files");
705 : 1 : TEST(test0042_5,"Rollback preserves the flag set by the first write");
706 : 1 : TEST(test0042_6,"Writable dry-run preserves database contents and settings");
707 : 1 : TEST(test0042_7,"Read-only dry-run preserves database contents and settings");
708 : 1 : TEST(test0042_8,"A blocked commit leaves a batch that can be rolled back");
709 : :
710 : 1 : RETURN_STATUS;
711 : : }
|