LCOV - code coverage report
Current view: top level - tests/src - test0042.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 358 358
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 12 12
Branches: 50.0 % 6 3

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

Generated by: LCOV version 2.0-1