LCOV - code coverage report
Current view: top level - tests/src - test0041.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 132 132
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 5 5
Branches: 75.0 % 4 3

             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                 :             : }
        

Generated by: LCOV version 2.0-1