Branch data Line data Source code
1 : : #include "sute.h"
2 : :
3 : : static Return cleanup_status = SUCCESS;
4 : :
5 : : /**
6 : : * @brief Prepare two missing file records under an existing fixture root
7 : : *
8 : : * Each scenario uses its own real SQLite file. Only the columns required by
9 : : * missing-file cleanup are created. DELETE journaling and NORMAL locking allow
10 : : * a separate reader to block commit without preventing the writer from starting
11 : : *
12 : : * @param[in] filename Database filename relative to TMPDIR
13 : : * @param[out] test_config Configuration receiving the writable connection
14 : : * @return SUCCESS when both records and the root are stored, otherwise FAILURE
15 : : */
16 : 3 : static Return open_database(
17 : : const char *filename,
18 : : Config *test_config)
19 : : {
20 : : /* Status returned by this function through provide()
21 : : Default value assumes successful completion */
22 : 3 : Return status = SUCCESS;
23 : 3 : sqlite3_stmt *statement = NULL;
24 : 3 : m_create(char,root_path,MEMORY_STRING);
25 : :
26 : 3 : test_config->db_file_name = m_init(char,MEMORY_STRING);
27 : 3 : test_config->update = true;
28 : 3 : test_config->db_primary_file_exists = true;
29 : 3 : test_config->sqlite_open_flag = SQLITE_OPEN_READWRITE;
30 : :
31 : : /* Use the existing copied fixture without changing any of its files */
32 : 3 : ASSERT(SUCCESS == construct_path("tests/fixtures/diffs/diff1",root_path));
33 : 3 : ASSERT(SUCCESS == m_copy_string(&test_config->db_file_name,filename));
34 : 3 : ASSERT(SUCCESS == open_db_from_tmpdir(filename,
35 : : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,&test_config->db));
36 : 3 : ASSERT(SQLITE_OK == sqlite3_exec(test_config->db,
37 : : "PRAGMA journal_mode=DELETE;"
38 : : "PRAGMA locking_mode=NORMAL;"
39 : : "CREATE TABLE paths(prefix TEXT NOT NULL);"
40 : : "CREATE TABLE files(ID INTEGER PRIMARY KEY,relative_path TEXT NOT NULL);"
41 : : "INSERT INTO files VALUES(1,'0044_missing_first'),(2,'0044_missing_second');",
42 : : NULL,NULL,NULL));
43 : :
44 : : /* Bind the absolute root so its spelling does not need SQL escaping */
45 : 3 : ASSERT(SQLITE_OK == sqlite3_prepare_v2(test_config->db,
46 : : "INSERT INTO paths(prefix) VALUES(?1);",-1,&statement,NULL));
47 : 3 : ASSERT(SQLITE_OK == sqlite3_bind_text(statement,1,m_text(root_path),-1,SQLITE_TRANSIENT));
48 : 3 : ASSERT(SQLITE_DONE == sqlite3_step(statement));
49 : :
50 : : /* Release preparation resources even when an assertion failed */
51 : 3 : const int finalize_status = sqlite3_finalize(statement);
52 : 3 : ASSERT(SQLITE_OK == finalize_status);
53 : 3 : call(m_del(root_path));
54 : :
55 : 3 : provide(status);
56 : : }
57 : :
58 : : /**
59 : : * @brief Capture cleanup diagnostics without exposing interruption to the runner
60 : : *
61 : : * The cleanup result is kept separately from capture success. Interruption
62 : : * globals are restored before the capture helper resumes reading its buffers
63 : : */
64 : 3 : static void capture_cleanup(void)
65 : : {
66 : 3 : const Return initial_global_status = atomic_load(&global_return_status);
67 : 3 : const bool initial_interrupt_flag = atomic_load(&global_interrupt_flag);
68 : :
69 : 3 : cleanup_status = db_delete_missing_metadata();
70 : :
71 : 3 : atomic_store(&global_return_status,initial_global_status);
72 : 3 : atomic_store(&global_interrupt_flag,initial_interrupt_flag);
73 : 3 : }
74 : :
75 : : /**
76 : : * @brief Roll back the first deletion when the second deletion fails
77 : : *
78 : : * The trigger fails only after one of the two rows has already been removed,
79 : : * regardless of their iteration order. Both original paths must remain visible
80 : : * to another connection after cleanup returns its technical failure
81 : : *
82 : : * @return Return status for partial-deletion rollback
83 : : */
84 : 1 : static Return test0044_1(void)
85 : : {
86 : 1 : INITTEST;
87 : 1 : Config *initial_config = config;
88 : 1 : Config test_config = {0};
89 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
90 : 1 : const char *database_filename = "0044_failed_delete.db";
91 : 1 : const char *expected_paths[] = {"0044_missing_first","0044_missing_second"};
92 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
93 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
94 : :
95 : : /* Fail the second deletion without automatically rolling back the transaction */
96 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
97 : 1 : config = &test_config;
98 : 1 : rational_logger_mode = REGULAR;
99 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(test_config.db,
100 : : "CREATE TRIGGER abort_second_delete BEFORE DELETE ON files "
101 : : "WHEN (SELECT COUNT(*) FROM files)=1 "
102 : : "BEGIN SELECT RAISE(ABORT,'forced deletion failure'); END;",
103 : : NULL,NULL,NULL));
104 : :
105 : : /* Require the intended failure and restoration of both committed records */
106 : 1 : cleanup_status = SUCCESS;
107 : 1 : ASSERT(SUCCESS == function_capture(capture_cleanup,captured_stdout,captured_stderr));
108 : 1 : ASSERT((cleanup_status & FAILURE) != 0);
109 : 1 : ASSERT(NULL != strstr(m_text(captured_stdout),"forced deletion failure"));
110 : 1 : ASSERT(captured_stderr->length == 0U);
111 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
112 : 1 : ASSERT(SUCCESS == db_paths_match(database_filename,expected_paths,2));
113 : :
114 : : /* Restore shared state and close the connection after any assertion result */
115 : 1 : config = initial_config;
116 : 1 : rational_logger_mode = initial_logger_mode;
117 : 1 : cleanup_status = SUCCESS;
118 : 1 : const int close_status = sqlite3_close(test_config.db);
119 : 1 : ASSERT(SQLITE_OK == close_status);
120 : 1 : call(m_del(&test_config.db_file_name));
121 : 1 : call(delete_path_if_present(database_filename));
122 : 1 : call(m_del(captured_stderr));
123 : 1 : call(m_del(captured_stdout));
124 : :
125 : 1 : RETURN_STATUS;
126 : : }
127 : :
128 : : /**
129 : : * @brief Roll back cleanup when a reader prevents its commit
130 : : *
131 : : * A separate read transaction permits DELETE statements but blocks COMMIT in
132 : : * DELETE journal mode. Cleanup must return failure and roll back its deletions
133 : : * while the reader is still open, leaving both original paths committed
134 : : *
135 : : * @return Return status for recovery from a blocked cleanup commit
136 : : */
137 : 1 : static Return test0044_2(void)
138 : : {
139 : 1 : INITTEST;
140 : 1 : Config *initial_config = config;
141 : 1 : Config test_config = {0};
142 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
143 : 1 : const char *database_filename = "0044_blocked_commit.db";
144 : 1 : const char *expected_paths[] = {"0044_missing_first","0044_missing_second"};
145 : 1 : sqlite3 *reader = NULL;
146 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
147 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
148 : :
149 : : /* Keep a real shared lock for the entire cleanup and its rollback */
150 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
151 : 1 : config = &test_config;
152 : 1 : rational_logger_mode = REGULAR;
153 : 1 : ASSERT(SUCCESS == open_db_from_tmpdir(database_filename,SQLITE_OPEN_READONLY,&reader));
154 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(reader,"BEGIN;SELECT COUNT(*) FROM files;",NULL,NULL,NULL));
155 : :
156 : : /* The error must come from COMMIT, and no deletion may remain pending */
157 : 1 : cleanup_status = SUCCESS;
158 : 1 : ASSERT(SUCCESS == function_capture(capture_cleanup,captured_stdout,captured_stderr));
159 : 1 : ASSERT((cleanup_status & FAILURE) != 0);
160 : 1 : ASSERT(NULL != strstr(m_text(captured_stdout),"Failed to commit missing file cleanup transaction"));
161 : 1 : ASSERT(captured_stderr->length == 0U);
162 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
163 : 1 : ASSERT(SUCCESS == db_paths_match(database_filename,expected_paths,2));
164 : :
165 : : /* Close both connections and restore shared state even after a failed check */
166 : 1 : const int reader_close_status = sqlite3_close(reader);
167 : 1 : ASSERT(SQLITE_OK == reader_close_status);
168 : 1 : config = initial_config;
169 : 1 : rational_logger_mode = initial_logger_mode;
170 : 1 : cleanup_status = SUCCESS;
171 : 1 : const int close_status = sqlite3_close(test_config.db);
172 : 1 : ASSERT(SQLITE_OK == close_status);
173 : 1 : call(m_del(&test_config.db_file_name));
174 : 1 : call(delete_path_if_present(database_filename));
175 : 1 : call(m_del(captured_stderr));
176 : 1 : call(m_del(captured_stdout));
177 : :
178 : 1 : RETURN_STATUS;
179 : : }
180 : :
181 : : /**
182 : : * @brief Request graceful interruption after a file record is deleted
183 : : *
184 : : * @param[in,out] context Number of observed file deletions
185 : : * @param operation SQLite operation reported by the update hook
186 : : * @param database_name Name of the database containing the changed table
187 : : * @param table_name Name of the changed table
188 : : * @param row_id ID of the changed row
189 : : */
190 : 1 : static void interrupt_after_delete(
191 : : void *context,
192 : : int operation,
193 : : const char *database_name,
194 : : const char *table_name,
195 : : sqlite3_int64 row_id)
196 : : {
197 : : (void)operation;
198 : : (void)database_name;
199 : : (void)table_name;
200 : : (void)row_id;
201 : :
202 : 1 : int *deleted_count = context;
203 : 1 : ++(*deleted_count);
204 : 1 : atomic_store(&global_interrupt_flag,true);
205 : 1 : atomic_store(&global_return_status,HALTED);
206 : 1 : }
207 : :
208 : : /**
209 : : * @brief Commit completed deletions when interruption arrives during cleanup
210 : : *
211 : : * The update hook raises interruption only after the first real DELETE.
212 : : * Cleanup must stop before another deletion and commit the completed work.
213 : : * This isolates the interruption boundary without signals, sleeps, or timers
214 : : *
215 : : * @return Return status for graceful cleanup completion
216 : : */
217 : 1 : static Return test0044_3(void)
218 : : {
219 : 1 : INITTEST;
220 : 1 : Config *initial_config = config;
221 : 1 : Config test_config = {0};
222 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
223 : 1 : const char *database_filename = "0044_interrupted_cleanup.db";
224 : 1 : int deleted_count = 0;
225 : 1 : int visible_files = -1;
226 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
227 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
228 : :
229 : : /* Install the interrupt hook only after both baseline records are committed */
230 : 1 : ASSERT(SUCCESS == open_database(database_filename,&test_config));
231 : 1 : config = &test_config;
232 : 1 : rational_logger_mode = REGULAR;
233 : 1 : IF(SUCCESS == status)
234 : : {
235 : 1 : (void)sqlite3_update_hook(test_config.db,interrupt_after_delete,&deleted_count);
236 : : }
237 : :
238 : : /* Capture restores interruption globals before any later assertions run */
239 : 1 : cleanup_status = SUCCESS;
240 : 1 : ASSERT(SUCCESS == function_capture(capture_cleanup,captured_stdout,captured_stderr));
241 : 1 : ASSERT(cleanup_status == (SUCCESS | HALTED));
242 : 1 : ASSERT(deleted_count == 1);
243 : 1 : ASSERT(captured_stderr->length == 0U);
244 : 1 : ASSERT(sqlite3_get_autocommit(test_config.db) != 0);
245 : 1 : ASSERT(SUCCESS == db_read_files_count(database_filename,&visible_files));
246 : 1 : ASSERT(visible_files == 1);
247 : :
248 : : /* Remove the hook before its stack-based context leaves scope */
249 : 1 : IF(test_config.db != NULL)
250 : : {
251 : 1 : (void)sqlite3_update_hook(test_config.db,NULL,NULL);
252 : : }
253 : :
254 : : /* Restore shared state and release all resources after any assertion result */
255 : 1 : config = initial_config;
256 : 1 : rational_logger_mode = initial_logger_mode;
257 : 1 : cleanup_status = SUCCESS;
258 : 1 : const int close_status = sqlite3_close(test_config.db);
259 : 1 : ASSERT(SQLITE_OK == close_status);
260 : 1 : call(m_del(&test_config.db_file_name));
261 : 1 : call(delete_path_if_present(database_filename));
262 : 1 : call(m_del(captured_stderr));
263 : 1 : call(m_del(captured_stdout));
264 : :
265 : 1 : RETURN_STATUS;
266 : : }
267 : :
268 : : /**
269 : : * @brief Run missing-file cleanup transaction regressions
270 : : *
271 : : * Each scenario calls cleanup directly against two missing file records.
272 : : * Real SQLite failures check atomic rollback, and a deletion hook checks that
273 : : * graceful interruption commits completed deletions without processing more rows
274 : : *
275 : : * @return Return status for all cleanup transaction scenarios
276 : : */
277 : 1 : Return test0044(void)
278 : : {
279 : 1 : INITTEST;
280 : :
281 : 1 : TEST(test0044_1,"A failed deletion restores earlier deletions in the cleanup batch");
282 : 1 : TEST(test0044_2,"A blocked cleanup commit rolls back all deletions");
283 : 1 : TEST(test0044_3,"Graceful interruption commits completed cleanup deletions");
284 : :
285 : 1 : RETURN_STATUS;
286 : : }
|