Branch data Line data Source code
1 : : #include "sute.h"
2 : :
3 : : static const char *vacuum_database_path = NULL;
4 : : static Return vacuum_status = FAILURE;
5 : :
6 : : /**
7 : : * @brief Capture vacuum output while preserving the operation's return status
8 : : *
9 : : * The capture API accepts a void callback, so the result of db_vacuum() is
10 : : * saved separately from the status of the capture operation
11 : : */
12 : 2 : static void capture_vacuum(void)
13 : : {
14 : 2 : vacuum_status = db_vacuum(vacuum_database_path);
15 : 2 : }
16 : :
17 : : /**
18 : : * @brief Check reported savings against file sizes and bound the reported duration
19 : : *
20 : : * The percentage is compared with the independently measured file sizes,
21 : : * allowing for rounding to two decimal places. The displayed duration keeps
22 : : * only the largest time unit and must fit within the measured interval around
23 : : * the complete captured vacuum call.
24 : : *
25 : : * That interval includes output capture and database opening and closing.
26 : : * It provides an upper bound, but does not verify the exact SQL execution time
27 : : * or detect zero or underestimated durations
28 : : *
29 : : * @param[in] output Captured vacuum output
30 : : * @param before_size Database file size immediately before vacuuming
31 : : * @param after_size Database file size immediately after vacuuming
32 : : * @param enclosing_elapsed_ns Duration of the complete captured call in nanoseconds
33 : : * @return SUCCESS when both measurements agree with their independent bounds
34 : : */
35 : 2 : static Return check_metrics(
36 : : const memory *output,
37 : : const off_t before_size,
38 : : const off_t after_size,
39 : : const long long int enclosing_elapsed_ns)
40 : : {
41 : : /* Status returned by this function through provide()
42 : : Default value assumes successful completion */
43 : 2 : Return status = SUCCESS;
44 : 2 : long double reported_savings = 0.0L;
45 : 2 : long double reported_elapsed = 0.0L;
46 : 2 : char elapsed_unit[8] = {0};
47 : 2 : const char *metrics = strstr(m_text(output),"vacuumed: saved ");
48 : :
49 : : /* Validate the measured bounds and parse the values printed by db_vacuum() */
50 : 2 : ASSERT(before_size > 0);
51 : 2 : ASSERT(after_size > 0);
52 : 2 : ASSERT(enclosing_elapsed_ns >= 0);
53 : 2 : ASSERT(metrics != NULL);
54 : 2 : ASSERT(3 == sscanf(metrics,"vacuumed: saved %Lf%%, elapsed %Lf%7s",
55 : : &reported_savings,&reported_elapsed,elapsed_unit));
56 : :
57 : : /* Compare with the independently measured size reduction. The 0.0051
58 : : percentage-point tolerance allows rounding to two decimal places, with
59 : : a small margin for floating-point arithmetic */
60 : 2 : ASSERT(reported_savings >=
61 : : 100.0L - 100.0L * (long double)after_size / (long double)before_size - 0.0051L);
62 : 2 : ASSERT(reported_savings <=
63 : : 100.0L - 100.0L * (long double)after_size / (long double)before_size + 0.0051L);
64 : :
65 : : /* Convert the printed time unit back to nanoseconds for comparison */
66 : : static const struct {
67 : : const char *suffix;
68 : : long double nanoseconds;
69 : : } duration_units[] = {
70 : : {"ns",1.0L},
71 : : {"μs",1000.0L},
72 : : {"ms",1000000.0L},
73 : : {"s",1000000000.0L},
74 : : {"min",60000000000.0L},
75 : : {"h",3600000000000.0L},
76 : : {"d",86400000000000.0L},
77 : : {"w",604800000000000.0L},
78 : : {"mon",2628000000000000.0L},
79 : : {"y",31536000000000000.0L}
80 : : };
81 : 2 : const size_t duration_unit_count = sizeof(duration_units) / sizeof(duration_units[0]);
82 : 2 : size_t duration_unit_index = 0;
83 : :
84 : 2 : while(duration_unit_index < duration_unit_count
85 [ + - + + ]: 6 : && strcmp(elapsed_unit,duration_units[duration_unit_index].suffix) != 0)
86 : : {
87 : 4 : ++duration_unit_index;
88 : : }
89 : :
90 : : /* Require a known unit and a nonnegative duration within the outer bound */
91 : 2 : ASSERT(duration_unit_index < duration_unit_count);
92 : 2 : ASSERT(reported_elapsed >= 0.0L);
93 : 2 : ASSERT(reported_elapsed * duration_units[duration_unit_index].nanoseconds <=
94 : : (long double)enclosing_elapsed_ns);
95 : :
96 : 2 : provide(status);
97 : : }
98 : :
99 : : /**
100 : : * @brief Verify that primary database vacuum reports reclaimed space
101 : : *
102 : : * Inserting and deleting 1 MiB of data with automatic vacuuming disabled
103 : : * leaves unused pages inside the database file. Vacuuming must make the file
104 : : * smaller and report the percentage saved from its actual before/after sizes.
105 : : * The full output must identify the primary database and report a duration
106 : : * within the independently measured upper bound.
107 : : *
108 : : * The primary database must be marked as modified. A control value in its
109 : : * saved startup size must remain unchanged, proving that the vacuum operation
110 : : * did not replace that baseline with a new measurement
111 : : *
112 : : * @return Return status for the numeric measurements and complete output checks
113 : : */
114 : 1 : static Return test0041_1(void)
115 : : {
116 : 1 : INITTEST;
117 : :
118 : : /* Save shared configuration, logger settings, and capture callback state */
119 : 1 : Config *initial_config = config;
120 : 1 : Config vacuum_config = {0};
121 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
122 : 1 : const char *initial_database_path = vacuum_database_path;
123 : 1 : const Return initial_vacuum_status = vacuum_status;
124 : 1 : sqlite3 *database = NULL;
125 : 1 : struct stat before = {0};
126 : 1 : struct stat after = {0};
127 : 1 : const char *database_filename = "0041_primary_vacuum.db";
128 : :
129 : 1 : m_create(char,database_path,MEMORY_STRING);
130 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
131 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
132 : 1 : m_create(char,pattern,MEMORY_STRING);
133 : :
134 : 1 : vacuum_config.db_primary_file_path = m_init(char,MEMORY_STRING);
135 : : /* Use an arbitrary control value to detect replacement of the saved startup
136 : : size. The expected savings come from separate stat measurements */
137 : 1 : vacuum_config.db_file_stat.st_size = 137;
138 : :
139 : : /* Create unused pages by inserting and deleting 1 MiB with automatic
140 : : vacuuming disabled, so the tested call has space to reclaim */
141 : 1 : ASSERT(SUCCESS == construct_path(database_filename,database_path));
142 : 1 : ASSERT(SUCCESS == open_db_from_tmpdir(database_filename,
143 : : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,&database));
144 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(database,
145 : : "PRAGMA page_size=4096;"
146 : : "PRAGMA auto_vacuum=NONE;"
147 : : "CREATE TABLE payload (value BLOB);"
148 : : "INSERT INTO payload VALUES (zeroblob(1048576));"
149 : : "DELETE FROM payload;",
150 : : NULL,NULL,NULL));
151 : :
152 : : /* Close the preparation connection before db_vacuum() opens its own */
153 : 1 : const int close_status = sqlite3_close(database);
154 : 1 : ASSERT(SQLITE_OK == close_status);
155 : :
156 : : /* Identify this file as primary and measure its size independently */
157 : 1 : ASSERT(SUCCESS == m_copy(&vacuum_config.db_primary_file_path,database_path));
158 : 1 : ASSERT(SUCCESS == get_file_stat(m_text(database_path),&before));
159 : :
160 : : /* Route the captured operation through the isolated configuration */
161 : 1 : config = &vacuum_config;
162 : 1 : rational_logger_mode = REGULAR;
163 : 1 : vacuum_database_path = m_text(database_path);
164 : 1 : vacuum_status = FAILURE;
165 : :
166 : : /* Measure the complete call, including capture and connection overhead,
167 : : to obtain an upper bound for the reported SQL execution time */
168 : 1 : const long long int capture_start_ns = cur_time_monotonic_ns();
169 : 1 : ASSERT(SUCCESS == function_capture(capture_vacuum,captured_stdout,captured_stderr));
170 : 1 : const long long int capture_elapsed_ns = cur_time_monotonic_ns() - capture_start_ns;
171 : :
172 : : /* Require a successful operation, no stderr output, and a smaller file */
173 : 1 : ASSERT(SUCCESS == vacuum_status);
174 : 1 : ASSERT(captured_stderr->length == 0U);
175 : 1 : ASSERT(SUCCESS == get_file_stat(m_text(database_path),&after));
176 : 1 : ASSERT(after.st_size < before.st_size);
177 : :
178 : : /* Compare the full output and check its numbers against independent bounds */
179 : 1 : ASSERT(SUCCESS == get_file_content("templates/0041_001.txt",pattern));
180 : 1 : ASSERT(SUCCESS == match_pattern(captured_stdout,pattern,"templates/0041_001.txt"));
181 : 1 : ASSERT(SUCCESS == check_metrics(captured_stdout,before.st_size,after.st_size,capture_elapsed_ns));
182 : :
183 : : /* Vacuum must mark the primary file as modified without replacing its baseline */
184 : 1 : ASSERT(vacuum_config.db_primary_file_modified == true);
185 : 1 : ASSERT(vacuum_config.db_file_stat.st_size == 137);
186 : :
187 : : /* Restore shared state even if preparation or an assertion failed */
188 : 1 : config = initial_config;
189 : 1 : rational_logger_mode = initial_logger_mode;
190 : 1 : vacuum_database_path = initial_database_path;
191 : 1 : vacuum_status = initial_vacuum_status;
192 : :
193 : : /* Remove the fixture and release all buffers after any test result */
194 : 1 : call(delete_path_if_present(database_filename));
195 : 1 : call(m_del(&vacuum_config.db_primary_file_path));
196 : 1 : call(m_del(pattern));
197 : 1 : call(m_del(captured_stderr));
198 : 1 : call(m_del(captured_stdout));
199 : 1 : call(m_del(database_path));
200 : :
201 : 1 : RETURN_STATUS;
202 : : }
203 : :
204 : : /**
205 : : * @brief Verify that vacuum reports zero savings for a compact non-primary database
206 : : *
207 : : * A separate database is compacted during preparation so the tested vacuum
208 : : * call has no unused pages to reclaim. The file size must stay unchanged and
209 : : * the full output must report zero savings without the primary-database label.
210 : : * The reported duration must fit within the independently measured upper bound.
211 : : *
212 : : * The configuration names a different file as the primary database. Its
213 : : * modification flag and saved startup size must remain unchanged
214 : : *
215 : : * @return Return status for the numeric measurements and complete output checks
216 : : */
217 : 1 : static Return test0041_2(void)
218 : : {
219 : 1 : INITTEST;
220 : :
221 : : /* Save shared configuration, logger settings, and capture callback state */
222 : 1 : Config *initial_config = config;
223 : 1 : Config vacuum_config = {0};
224 : 1 : const LOGMODES initial_logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
225 : 1 : const char *initial_database_path = vacuum_database_path;
226 : 1 : const Return initial_vacuum_status = vacuum_status;
227 : 1 : sqlite3 *database = NULL;
228 : 1 : struct stat before = {0};
229 : 1 : struct stat after = {0};
230 : 1 : const char *database_filename = "0041_non_primary_vacuum.db";
231 : :
232 : 1 : m_create(char,database_path,MEMORY_STRING);
233 : 1 : m_create(char,captured_stdout,MEMORY_STRING);
234 : 1 : m_create(char,captured_stderr,MEMORY_STRING);
235 : 1 : m_create(char,pattern,MEMORY_STRING);
236 : :
237 : 1 : vacuum_config.db_primary_file_path = m_init(char,MEMORY_STRING);
238 : : /* Use an arbitrary control value to detect replacement of the saved startup
239 : : size. The expected savings come from separate stat measurements */
240 : 1 : vacuum_config.db_file_stat.st_size = 137;
241 : :
242 : : /* Build and compact the fixture before the measured call. The preparatory
243 : : VACUUM removes the space left by the inserted and deleted 1 MiB payload */
244 : 1 : ASSERT(SUCCESS == construct_path(database_filename,database_path));
245 : 1 : ASSERT(SUCCESS == open_db_from_tmpdir(database_filename,
246 : : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,&database));
247 : 1 : ASSERT(SQLITE_OK == sqlite3_exec(database,
248 : : "PRAGMA page_size=4096;"
249 : : "PRAGMA auto_vacuum=NONE;"
250 : : "CREATE TABLE payload (value BLOB);"
251 : : "INSERT INTO payload VALUES (zeroblob(1048576));"
252 : : "DELETE FROM payload;"
253 : : "VACUUM;",
254 : : NULL,NULL,NULL));
255 : :
256 : : /* Close the preparation connection before db_vacuum() opens its own */
257 : 1 : const int close_status = sqlite3_close(database);
258 : 1 : ASSERT(SQLITE_OK == close_status);
259 : :
260 : : /* Name a different primary file and measure the compact fixture size */
261 : 1 : ASSERT(SUCCESS == m_copy_literal(&vacuum_config.db_primary_file_path,"another_database.db"));
262 : 1 : ASSERT(SUCCESS == get_file_stat(m_text(database_path),&before));
263 : :
264 : : /* Route the captured operation through the isolated configuration */
265 : 1 : config = &vacuum_config;
266 : 1 : rational_logger_mode = REGULAR;
267 : 1 : vacuum_database_path = m_text(database_path);
268 : 1 : vacuum_status = FAILURE;
269 : :
270 : : /* Measure the complete call, including capture and connection overhead,
271 : : to obtain an upper bound for the reported SQL execution time */
272 : 1 : const long long int capture_start_ns = cur_time_monotonic_ns();
273 : 1 : ASSERT(SUCCESS == function_capture(capture_vacuum,captured_stdout,captured_stderr));
274 : 1 : const long long int capture_elapsed_ns = cur_time_monotonic_ns() - capture_start_ns;
275 : :
276 : : /* Require a successful operation, no stderr output, and an unchanged size */
277 : 1 : ASSERT(SUCCESS == vacuum_status);
278 : 1 : ASSERT(captured_stderr->length == 0U);
279 : 1 : ASSERT(SUCCESS == get_file_stat(m_text(database_path),&after));
280 : 1 : ASSERT(after.st_size == before.st_size);
281 : :
282 : : /* Compare the full output and check its numbers against independent bounds */
283 : 1 : ASSERT(SUCCESS == get_file_content("templates/0041_002.txt",pattern));
284 : 1 : ASSERT(SUCCESS == match_pattern(captured_stdout,pattern,"templates/0041_002.txt"));
285 : 1 : ASSERT(SUCCESS == check_metrics(captured_stdout,before.st_size,after.st_size,capture_elapsed_ns));
286 : :
287 : : /* Vacuuming another database must preserve the primary flag and baseline */
288 : 1 : ASSERT(vacuum_config.db_primary_file_modified == false);
289 : 1 : ASSERT(vacuum_config.db_file_stat.st_size == 137);
290 : :
291 : : /* Restore shared state even if preparation or an assertion failed */
292 : 1 : config = initial_config;
293 : 1 : rational_logger_mode = initial_logger_mode;
294 : 1 : vacuum_database_path = initial_database_path;
295 : 1 : vacuum_status = initial_vacuum_status;
296 : :
297 : : /* Remove the fixture and release all buffers after any test result */
298 : 1 : call(delete_path_if_present(database_filename));
299 : 1 : call(m_del(&vacuum_config.db_primary_file_path));
300 : 1 : call(m_del(pattern));
301 : 1 : call(m_del(captured_stderr));
302 : 1 : call(m_del(captured_stdout));
303 : 1 : call(m_del(database_path));
304 : :
305 : 1 : RETURN_STATUS;
306 : : }
307 : :
308 : : /**
309 : : * @brief Check primary and non-primary vacuum measurements independently
310 : : *
311 : : * Two real SQLite files exercise different outcomes. Vacuuming the primary
312 : : * database must reclaim space left by deleted data and mark that database as
313 : : * modified. Vacuuming an already compact non-primary database must report zero
314 : : * savings and leave the primary database's modification flag unchanged.
315 : : *
316 : : * Both cases compare the complete output with a template and verify the saved
317 : : * percentage against independent file-size measurements. A control value also
318 : : * checks that the saved startup size is preserved. Reported time is checked
319 : : * only against an outer duration that includes capture and connection overhead;
320 : : * this does not detect zero or underestimated SQL execution times
321 : : *
322 : : * @return Return status for both vacuum scenarios
323 : : */
324 : 1 : Return test0041(void)
325 : : {
326 : 1 : INITTEST;
327 : :
328 : 1 : TEST(test0041_1,"Primary vacuum reclaims space and preserves the startup stat");
329 : 1 : TEST(test0041_2,"Non-primary vacuum reports zero savings for a compact database");
330 : :
331 : 1 : RETURN_STATUS;
332 : : }
|