| # 2009 January 30 | 
 | # | 
 | # The author disclaims copyright to this source code.  In place of | 
 | # a legal notice, here is a blessing: | 
 | # | 
 | #    May you do good and not evil. | 
 | #    May you find forgiveness for yourself and forgive others. | 
 | #    May you share freely, never taking more than you give. | 
 | # | 
 | #*********************************************************************** | 
 | # This file implements regression tests for SQLite library.  The | 
 | # focus of this file is testing the handling of IO errors by the | 
 | # sqlite3_backup_XXX APIs. | 
 | # | 
 | # $Id: backup_ioerr.test,v 1.3 2009/04/10 18:41:01 danielk1977 Exp $ | 
 |  | 
 | set testdir [file dirname $argv0] | 
 | source $testdir/tester.tcl | 
 |  | 
 | proc data_checksum {db file} {  | 
 |   $db one "SELECT md5sum(a, b) FROM ${file}.t1"  | 
 | } | 
 | proc test_contents {name db1 file1 db2 file2} { | 
 |   $db2 eval {select * from sqlite_master} | 
 |   $db1 eval {select * from sqlite_master} | 
 |   set checksum [data_checksum $db2 $file2] | 
 |   uplevel [list do_test $name [list data_checksum $db1 $file1] $checksum] | 
 | } | 
 |  | 
 | #-------------------------------------------------------------------- | 
 | # This proc creates a database of approximately 290 pages. Depending | 
 | # on whether or not auto-vacuum is configured. Test cases backup_ioerr-1.* | 
 | # verify nothing more than this assumption. | 
 | # | 
 | proc populate_database {db {xtra_large 0}} { | 
 |   execsql { | 
 |     BEGIN; | 
 |     CREATE TABLE t1(a, b); | 
 |     INSERT INTO t1 VALUES(1, randstr(1000,1000)); | 
 |     INSERT INTO t1 SELECT a+ 1, randstr(1000,1000) FROM t1; | 
 |     INSERT INTO t1 SELECT a+ 2, randstr(1000,1000) FROM t1; | 
 |     INSERT INTO t1 SELECT a+ 4, randstr(1000,1000) FROM t1; | 
 |     INSERT INTO t1 SELECT a+ 8, randstr(1000,1000) FROM t1; | 
 |     INSERT INTO t1 SELECT a+16, randstr(1000,1000) FROM t1; | 
 |     INSERT INTO t1 SELECT a+32, randstr(1000,1000) FROM t1; | 
 |     CREATE INDEX i1 ON t1(b); | 
 |     COMMIT; | 
 |   } $db | 
 |   if {$xtra_large} { | 
 |     execsql { INSERT INTO t1 SELECT a+64, randstr(1000,1000) FROM t1 } $db | 
 |   } | 
 | } | 
 | do_test backup_ioerr-1.1 { | 
 |   populate_database db | 
 |   set nPage [expr {[file size test.db] / 1024}] | 
 |   expr {$nPage>130 && $nPage<160} | 
 | } {1} | 
 | do_test backup_ioerr-1.2 { | 
 |   expr {[file size test.db] > $sqlite_pending_byte} | 
 | } {1} | 
 | do_test backup_ioerr-1.3 { | 
 |   db close | 
 |   forcedelete test.db | 
 | } {} | 
 |  | 
 | # Turn off IO error simulation. | 
 | # | 
 | proc clear_ioerr_simulation {} { | 
 |   set ::sqlite_io_error_hit 0 | 
 |   set ::sqlite_io_error_hardhit 0 | 
 |   set ::sqlite_io_error_pending 0 | 
 |   set ::sqlite_io_error_persist 0 | 
 | } | 
 |  | 
 | #-------------------------------------------------------------------- | 
 | # The following procedure runs with SQLite's IO error simulation  | 
 | # enabled. | 
 | # | 
 | #   1) Start with a reasonably sized database. One that includes the | 
 | #      pending-byte (locking) page. | 
 | # | 
 | #   2) Open a backup process. Set the cache-size for the destination | 
 | #      database to 10 pages only. | 
 | # | 
 | #   3) Step the backup process N times to partially backup the database | 
 | #      file. If an IO error is reported, then the backup process is | 
 | #      concluded with a call to backup_finish(). | 
 | # | 
 | #      If an IO error occurs, verify that: | 
 | # | 
 | #      * the call to backup_step() returns an SQLITE_IOERR_XXX error code. | 
 | # | 
 | #      * after the failed call to backup_step() but before the call to | 
 | #        backup_finish() the destination database handle error code and  | 
 | #        error message remain unchanged. | 
 | # | 
 | #      * the call to backup_finish() returns an SQLITE_IOERR_XXX error code. | 
 | # | 
 | #      * following the call to backup_finish(), the destination database | 
 | #        handle has been populated with an error code and error message. | 
 | # | 
 | #   4) Write to the database via the source database connection. Check  | 
 | #      that: | 
 | # | 
 | #      * If an IO error occurs while writing the source database, the | 
 | #        write operation should report an IO error. The backup should  | 
 | #        proceed as normal. | 
 | # | 
 | #      * If an IO error occurs while updating the backup, the write  | 
 | #        operation should proceed normally. The error should be reported | 
 | #        from the next call to backup_step() (in step 5 of this test | 
 | #        procedure). | 
 | # | 
 | #   5) Step the backup process to finish the backup. If an IO error is  | 
 | #      reported, then the backup process is concluded with a call to  | 
 | #      backup_finish(). | 
 | # | 
 | #      Test that if an IO error occurs, or if one occurred while updating | 
 | #      the backup database during step 4, then the conditions listed | 
 | #      under step 3 are all true. | 
 | # | 
 | #   6) Finish the backup process. | 
 | # | 
 | #   * If the backup succeeds (backup_finish() returns SQLITE_OK), then | 
 | #     the contents of the backup database should match that of the | 
 | #     source database. | 
 | # | 
 | #   * If the backup fails (backup_finish() returns other than SQLITE_OK),  | 
 | #     then the contents of the backup database should be as they were  | 
 | #     before the operation was started. | 
 | # | 
 | # The following factors are varied: | 
 | # | 
 | #   * Destination database is initially larger than the source database, OR | 
 | #   * Destination database is initially smaller than the source database. | 
 | # | 
 | #   * IO errors are transient, OR | 
 | #   * IO errors are persistent. | 
 | # | 
 | #   * Destination page-size is smaller than the source. | 
 | #   * Destination page-size is the same as the source. | 
 | #   * Destination page-size is larger than the source. | 
 | # | 
 |  | 
 | set iTest 1 | 
 | foreach bPersist {0 1} { | 
 | foreach iDestPagesize {512 1024 4096} { | 
 | foreach zSetupBak [list "" {populate_database ddb 1}] { | 
 |  | 
 |   incr iTest | 
 |   set bStop 0 | 
 | for {set iError 1} {$bStop == 0} {incr iError} { | 
 |   # Disable IO error simulation. | 
 |   clear_ioerr_simulation | 
 |  | 
 |   catch { ddb close } | 
 |   catch { sdb close } | 
 |   catch { forcedelete test.db } | 
 |   catch { forcedelete bak.db } | 
 |  | 
 |   # Open the source and destination databases. | 
 |   sqlite3 sdb test.db | 
 |   sqlite3 ddb bak.db | 
 |  | 
 |   # Step 1: Populate the source and destination databases. | 
 |   populate_database sdb | 
 |   ddb eval "PRAGMA page_size = $iDestPagesize" | 
 |   ddb eval "PRAGMA cache_size = 10" | 
 |   eval $zSetupBak | 
 |  | 
 |   # Step 2: Open the backup process. | 
 |   sqlite3_backup B ddb main sdb main | 
 |  | 
 |   # Enable IO error simulation. | 
 |   set ::sqlite_io_error_pending $iError | 
 |   set ::sqlite_io_error_persist $bPersist | 
 |  | 
 |   # Step 3: Partially backup the database. If an IO error occurs, check | 
 |   # a few things then skip to the next iteration of the loop. | 
 |   # | 
 |   set rc [B step 100] | 
 |   if {$::sqlite_io_error_hardhit} { | 
 |  | 
 |     do_test backup_ioerr-$iTest.$iError.1 { | 
 |       string match SQLITE_IOERR* $rc | 
 |     } {1} | 
 |     do_test backup_ioerr-$iTest.$iError.2 { | 
 |       list [sqlite3_errcode ddb] [sqlite3_errmsg ddb] | 
 |     } {SQLITE_OK {not an error}} | 
 |  | 
 |     set rc [B finish] | 
 |     do_test backup_ioerr-$iTest.$iError.3 { | 
 |       string match SQLITE_IOERR* $rc | 
 |     } {1} | 
 |  | 
 |     do_test backup_ioerr-$iTest.$iError.4 { | 
 |       sqlite3_errmsg ddb | 
 |     } {disk I/O error} | 
 |  | 
 |     clear_ioerr_simulation | 
 |     sqlite3 ddb bak.db | 
 |     integrity_check backup_ioerr-$iTest.$iError.5 ddb | 
 |  | 
 |     continue | 
 |   } | 
 |  | 
 |   # No IO error was encountered during step 3. Check that backup_step() | 
 |   # returned SQLITE_OK before proceding. | 
 |   do_test backup_ioerr-$iTest.$iError.6 { | 
 |     expr {$rc eq "SQLITE_OK"} | 
 |   } {1} | 
 |  | 
 |   # Step 4: Write to the source database. | 
 |   set rc [catchsql { UPDATE t1 SET b = randstr(1000,1000) WHERE a < 50 } sdb] | 
 |  | 
 |   if {[lindex $rc 0] && $::sqlite_io_error_persist==0} { | 
 |     # The IO error occurred while updating the source database. In this | 
 |     # case the backup should be able to continue. | 
 |     set rc [B step 5000] | 
 |     if { $rc != "SQLITE_IOERR_UNLOCK" } { | 
 |       do_test backup_ioerr-$iTest.$iError.7 { | 
 |         list [B step 5000] [B finish] | 
 |       } {SQLITE_DONE SQLITE_OK} | 
 |  | 
 |       clear_ioerr_simulation | 
 |       test_contents backup_ioerr-$iTest.$iError.8 ddb main sdb main | 
 |       integrity_check backup_ioerr-$iTest.$iError.9 ddb | 
 |     } else { | 
 |       do_test backup_ioerr-$iTest.$iError.10 { | 
 |         B finish | 
 |       } {SQLITE_IOERR_UNLOCK} | 
 |     } | 
 |  | 
 |     clear_ioerr_simulation | 
 |     sqlite3 ddb bak.db | 
 |     integrity_check backup_ioerr-$iTest.$iError.11 ddb | 
 |  | 
 |     continue | 
 |   } | 
 |  | 
 |   # Step 5: Finish the backup operation. If an IO error occurs, check that | 
 |   # it is reported correctly and skip to the next iteration of the loop. | 
 |   # | 
 |   set rc [B step 5000] | 
 |   if {$rc != "SQLITE_DONE"} { | 
 |     do_test backup_ioerr-$iTest.$iError.12 { | 
 |       string match SQLITE_IOERR* $rc | 
 |     } {1} | 
 |     do_test backup_ioerr-$iTest.$iError.13 { | 
 |       list [sqlite3_errcode ddb] [sqlite3_errmsg ddb] | 
 |     } {SQLITE_OK {not an error}} | 
 |  | 
 |     set rc [B finish] | 
 |     do_test backup_ioerr-$iTest.$iError.14 { | 
 |       string match SQLITE_IOERR* $rc | 
 |     } {1} | 
 |     do_test backup_ioerr-$iTest.$iError.15 { | 
 |       sqlite3_errmsg ddb | 
 |     } {disk I/O error} | 
 |  | 
 |     clear_ioerr_simulation | 
 |     sqlite3 ddb bak.db | 
 |     integrity_check backup_ioerr-$iTest.$iError.16 ddb | 
 |  | 
 |     continue | 
 |   } | 
 |  | 
 |   # The backup was successfully completed. | 
 |   # | 
 |   do_test backup_ioerr-$iTest.$iError.17 { | 
 |     list [set rc] [B finish] | 
 |   } {SQLITE_DONE SQLITE_OK} | 
 |  | 
 |   clear_ioerr_simulation | 
 |   sqlite3 sdb test.db | 
 |   sqlite3 ddb bak.db | 
 |  | 
 |   test_contents backup_ioerr-$iTest.$iError.18 ddb main sdb main | 
 |   integrity_check backup_ioerr-$iTest.$iError.19 ddb | 
 |  | 
 |   set bStop [expr $::sqlite_io_error_pending<=0] | 
 | }}}} | 
 |  | 
 | catch { sdb close } | 
 | catch { ddb close } | 
 | finish_test |