1# 2007 May 05 2# 3# The author disclaims copyright to this source code. In place of 4# a legal notice, here is a blessing: 5# 6# May you do good and not evil. 7# May you find forgiveness for yourself and forgive others. 8# May you share freely, never taking more than you give. 9# 10#*********************************************************************** 11# 12# This file contains common code used by many different malloc tests 13# within the test suite. 14# 15# $Id: malloc_common.tcl,v 1.22 2008/09/23 16:41:30 danielk1977 Exp $ 16 17# If we did not compile with malloc testing enabled, then do nothing. 18# 19ifcapable builtin_test { 20 set MEMDEBUG 1 21} else { 22 set MEMDEBUG 0 23 return 0 24} 25 26# Transient and persistent OOM errors: 27# 28set FAULTSIM(oom-transient) [list \ 29 -injectstart {oom_injectstart 0} \ 30 -injectstop oom_injectstop \ 31 -injecterrlist {{1 {out of memory}}} \ 32] 33set FAULTSIM(oom-persistent) [list \ 34 -injectstart {oom_injectstart 1000000} \ 35 -injectstop oom_injectstop \ 36 -injecterrlist {{1 {out of memory}}} \ 37] 38 39# Transient and persistent IO errors: 40# 41set FAULTSIM(ioerr-transient) [list \ 42 -injectstart {ioerr_injectstart 0} \ 43 -injectstop ioerr_injectstop \ 44 -injecterrlist {{1 {disk I/O error}}} \ 45] 46set FAULTSIM(ioerr-persistent) [list \ 47 -injectstart {ioerr_injectstart 1} \ 48 -injectstop ioerr_injectstop \ 49 -injecterrlist {{1 {disk I/O error}}} \ 50] 51 52# Transient and persistent SHM errors: 53# 54set FAULTSIM(shmerr-transient) [list \ 55 -injectinstall shmerr_injectinstall \ 56 -injectstart {shmerr_injectstart 0} \ 57 -injectstop shmerr_injectstop \ 58 -injecterrlist {{1 {disk I/O error}}} \ 59 -injectuninstall shmerr_injectuninstall \ 60] 61set FAULTSIM(shmerr-persistent) [list \ 62 -injectinstall shmerr_injectinstall \ 63 -injectstart {shmerr_injectstart 1} \ 64 -injectstop shmerr_injectstop \ 65 -injecterrlist {{1 {disk I/O error}}} \ 66 -injectuninstall shmerr_injectuninstall \ 67] 68 69 70 71#-------------------------------------------------------------------------- 72# Usage do_faultsim_test NAME ?OPTIONS...? 73# 74# -faults List of fault types to simulate. 75# 76# -prep Script to execute before -body. 77# 78# -body Script to execute (with fault injection). 79# 80# -test Script to execute after -body. 81# 82proc do_faultsim_test {name args} { 83 global FAULTSIM 84 85 set DEFAULT(-faults) [array names FAULTSIM] 86 set DEFAULT(-prep) "" 87 set DEFAULT(-body) "" 88 set DEFAULT(-test) "" 89 90 array set O [array get DEFAULT] 91 array set O $args 92 foreach o [array names O] { 93 if {[info exists DEFAULT($o)]==0} { error "unknown option: $o" } 94 } 95 96 set faultlist [list] 97 foreach f $O(-faults) { 98 set flist [array names FAULTSIM $f] 99 if {[llength $flist]==0} { error "unknown fault: $f" } 100 set faultlist [concat $faultlist $flist] 101 } 102 103 set testspec [list -prep $O(-prep) -body $O(-body) -test $O(-test)] 104 foreach f [lsort -unique $faultlist] { 105 eval do_one_faultsim_test "$name-$f" $FAULTSIM($f) $testspec 106 } 107} 108 109#------------------------------------------------------------------------- 110# Procedures to save and restore the current file-system state: 111# 112# faultsim_save 113# faultsim_save_and_close 114# faultsim_restore_and_reopen 115# faultsim_delete_and_reopen 116# 117proc faultsim_save {} { 118 foreach f [glob -nocomplain sv_test.db*] { file delete -force $f } 119 foreach f [glob -nocomplain test.db*] { 120 set f2 "sv_$f" 121 file copy -force $f $f2 122 } 123} 124proc faultsim_save_and_close {} { 125 faultsim_save 126 catch { db close } 127 return "" 128} 129proc faultsim_restore_and_reopen {} { 130 catch { db close } 131 foreach f [glob -nocomplain test.db*] { file delete -force $f } 132 foreach f2 [glob -nocomplain sv_test.db*] { 133 set f [string range $f2 3 end] 134 file copy -force $f2 $f 135 } 136 sqlite3 db test.db 137 sqlite3_extended_result_codes db 1 138 sqlite3_db_config_lookaside db 0 0 0 139} 140 141proc faultsim_integrity_check {{db db}} { 142 set ic [$db eval { PRAGMA integrity_check }] 143 if {$ic != "ok"} { error "Integrity check: $ic" } 144} 145 146proc faultsim_delete_and_reopen {{file test.db}} { 147 catch { db close } 148 foreach f [glob -nocomplain test.db*] { file delete -force $f } 149 sqlite3 db test.db 150} 151 152 153# The following procs are used as [do_one_faultsim_test] callbacks when 154# injecting OOM faults into test cases. 155# 156proc oom_injectstart {nRepeat iFail} { 157 sqlite3_memdebug_fail $iFail -repeat $nRepeat 158} 159proc oom_injectstop {} { 160 sqlite3_memdebug_fail -1 161} 162 163# The following procs are used as [do_one_faultsim_test] callbacks when 164# injecting IO error faults into test cases. 165# 166proc ioerr_injectstart {persist iFail} { 167 set ::sqlite_io_error_persist $persist 168 set ::sqlite_io_error_pending $iFail 169} 170proc ioerr_injectstop {} { 171 set sv $::sqlite_io_error_hit 172 set ::sqlite_io_error_persist 0 173 set ::sqlite_io_error_pending 0 174 set ::sqlite_io_error_hardhit 0 175 set ::sqlite_io_error_hit 0 176 set ::sqlite_io_error_pending 0 177 return $sv 178} 179 180# The following procs are used as [do_one_faultsim_test] callbacks when 181# injecting shared-memory related error faults into test cases. 182# 183proc shmerr_injectinstall {} { 184 testvfs shmfault -default true 185} 186proc shmerr_injectuninstall {} { 187 catch {db close} 188 catch {db2 close} 189 shmfault delete 190} 191proc shmerr_injectstart {persist iFail} { 192 shmfault ioerr $iFail $persist 193} 194proc shmerr_injectstop {} { 195 shmfault ioerr 0 0 196} 197 198# This command is not called directly. It is used by the 199# [faultsim_test_result] command created by [do_faultsim_test] and used 200# by -test scripts. 201# 202proc faultsim_test_result_int {args} { 203 upvar testrc testrc testresult testresult testnfail testnfail 204 set t [list $testrc $testresult] 205 set r $args 206 if { ($testnfail==0 && $t != [lindex $r 0]) || [lsearch $r $t]<0 } { 207 error "nfail=$testnfail rc=$testrc result=$testresult" 208 } 209} 210 211#-------------------------------------------------------------------------- 212# Usage do_one_faultsim_test NAME ?OPTIONS...? 213# 214# The first argument, <test number>, is used as a prefix of the test names 215# taken by tests executed by this command. Options are as follows. All 216# options take a single argument. 217# 218# -injectstart Script to enable fault-injection. 219# 220# -injectstop Script to disable fault-injection. 221# 222# -injecterrlist List of generally acceptable test results (i.e. error 223# messages). Example: [list {1 {out of memory}}] 224# 225# -injectinstall 226# 227# -injectuninstall 228# 229# -prep Script to execute before -body. 230# 231# -body Script to execute (with fault injection). 232# 233# -test Script to execute after -body. 234# 235proc do_one_faultsim_test {testname args} { 236 237 set DEFAULT(-injectstart) "expr" 238 set DEFAULT(-injectstop) "expr 0" 239 set DEFAULT(-injecterrlist) [list] 240 set DEFAULT(-injectinstall) "" 241 set DEFAULT(-injectuninstall) "" 242 set DEFAULT(-prep) "" 243 set DEFAULT(-body) "" 244 set DEFAULT(-test) "" 245 246 array set O [array get DEFAULT] 247 array set O $args 248 foreach o [array names O] { 249 if {[info exists DEFAULT($o)]==0} { error "unknown option: $o" } 250 } 251 252 proc faultsim_test_proc {testrc testresult testnfail} $O(-test) 253 proc faultsim_test_result {args} " 254 uplevel faultsim_test_result_int \$args [list $O(-injecterrlist)] 255 " 256 257 eval $O(-injectinstall) 258 259 set stop 0 260 for {set iFail 1} {!$stop} {incr iFail} { 261 262 # Evaluate the -prep script. 263 # 264 eval $O(-prep) 265 266 # Start the fault-injection. Run the -body script. Stop the fault 267 # injection. Local var $nfail is set to the total number of faults 268 # injected into the system this trial. 269 # 270 eval $O(-injectstart) $iFail 271 set rc [catch $O(-body) res] 272 set nfail [eval $O(-injectstop)] 273 274 # Run the -test script. If it throws no error, consider this trial 275 # sucessful. If it does throw an error, cause a [do_test] test to 276 # fail (and print out the unexpected exception thrown by the -test 277 # script at the same time). 278 # 279 set rc [catch [list faultsim_test_proc $rc $res $nfail] res] 280 if {$rc == 0} {set res ok} 281 do_test $testname.$iFail [list list $rc $res] {0 ok} 282 283 # If no faults where injected this trial, don't bother running 284 # any more. This test is finished. 285 # 286 if {$nfail==0} { set stop 1 } 287 } 288 289 eval $O(-injectuninstall) 290} 291 292# Usage: do_malloc_test <test number> <options...> 293# 294# The first argument, <test number>, is an integer used to name the 295# tests executed by this proc. Options are as follows: 296# 297# -tclprep TCL script to run to prepare test. 298# -sqlprep SQL script to run to prepare test. 299# -tclbody TCL script to run with malloc failure simulation. 300# -sqlbody TCL script to run with malloc failure simulation. 301# -cleanup TCL script to run after the test. 302# 303# This command runs a series of tests to verify SQLite's ability 304# to handle an out-of-memory condition gracefully. It is assumed 305# that if this condition occurs a malloc() call will return a 306# NULL pointer. Linux, for example, doesn't do that by default. See 307# the "BUGS" section of malloc(3). 308# 309# Each iteration of a loop, the TCL commands in any argument passed 310# to the -tclbody switch, followed by the SQL commands in any argument 311# passed to the -sqlbody switch are executed. Each iteration the 312# Nth call to sqliteMalloc() is made to fail, where N is increased 313# each time the loop runs starting from 1. When all commands execute 314# successfully, the loop ends. 315# 316proc do_malloc_test {tn args} { 317 array unset ::mallocopts 318 array set ::mallocopts $args 319 320 if {[string is integer $tn]} { 321 set tn malloc-$tn 322 } 323 if {[info exists ::mallocopts(-start)]} { 324 set start $::mallocopts(-start) 325 } else { 326 set start 0 327 } 328 if {[info exists ::mallocopts(-end)]} { 329 set end $::mallocopts(-end) 330 } else { 331 set end 50000 332 } 333 save_prng_state 334 335 foreach ::iRepeat {0 10000000} { 336 set ::go 1 337 for {set ::n $start} {$::go && $::n <= $end} {incr ::n} { 338 339 # If $::iRepeat is 0, then the malloc() failure is transient - it 340 # fails and then subsequent calls succeed. If $::iRepeat is 1, 341 # then the failure is persistent - once malloc() fails it keeps 342 # failing. 343 # 344 set zRepeat "transient" 345 if {$::iRepeat} {set zRepeat "persistent"} 346 restore_prng_state 347 foreach file [glob -nocomplain test.db-mj*] {file delete -force $file} 348 349 do_test ${tn}.${zRepeat}.${::n} { 350 351 # Remove all traces of database files test.db and test2.db 352 # from the file-system. Then open (empty database) "test.db" 353 # with the handle [db]. 354 # 355 catch {db close} 356 catch {file delete -force test.db} 357 catch {file delete -force test.db-journal} 358 catch {file delete -force test.db-wal} 359 catch {file delete -force test2.db} 360 catch {file delete -force test2.db-journal} 361 catch {file delete -force test2.db-wal} 362 if {[info exists ::mallocopts(-testdb)]} { 363 file copy $::mallocopts(-testdb) test.db 364 } 365 catch { sqlite3 db test.db } 366 if {[info commands db] ne ""} { 367 sqlite3_extended_result_codes db 1 368 } 369 sqlite3_db_config_lookaside db 0 0 0 370 371 # Execute any -tclprep and -sqlprep scripts. 372 # 373 if {[info exists ::mallocopts(-tclprep)]} { 374 eval $::mallocopts(-tclprep) 375 } 376 if {[info exists ::mallocopts(-sqlprep)]} { 377 execsql $::mallocopts(-sqlprep) 378 } 379 380 # Now set the ${::n}th malloc() to fail and execute the -tclbody 381 # and -sqlbody scripts. 382 # 383 sqlite3_memdebug_fail $::n -repeat $::iRepeat 384 set ::mallocbody {} 385 if {[info exists ::mallocopts(-tclbody)]} { 386 append ::mallocbody "$::mallocopts(-tclbody)\n" 387 } 388 if {[info exists ::mallocopts(-sqlbody)]} { 389 append ::mallocbody "db eval {$::mallocopts(-sqlbody)}" 390 } 391 392 # The following block sets local variables as follows: 393 # 394 # isFail - True if an error (any error) was reported by sqlite. 395 # nFail - The total number of simulated malloc() failures. 396 # nBenign - The number of benign simulated malloc() failures. 397 # 398 set isFail [catch $::mallocbody msg] 399 set nFail [sqlite3_memdebug_fail -1 -benigncnt nBenign] 400 # puts -nonewline " (isFail=$isFail nFail=$nFail nBenign=$nBenign) " 401 402 # If one or more mallocs failed, run this loop body again. 403 # 404 set go [expr {$nFail>0}] 405 406 if {($nFail-$nBenign)==0} { 407 if {$isFail} { 408 set v2 $msg 409 } else { 410 set isFail 1 411 set v2 1 412 } 413 } elseif {!$isFail} { 414 set v2 $msg 415 } elseif { 416 [info command db]=="" || 417 [db errorcode]==7 || 418 $msg=="out of memory" 419 } { 420 set v2 1 421 } else { 422 set v2 $msg 423 puts [db errorcode] 424 } 425 lappend isFail $v2 426 } {1 1} 427 428 if {[info exists ::mallocopts(-cleanup)]} { 429 catch [list uplevel #0 $::mallocopts(-cleanup)] msg 430 } 431 } 432 } 433 unset ::mallocopts 434 sqlite3_memdebug_fail -1 435} 436 437 438#------------------------------------------------------------------------- 439# This proc is used to test a single SELECT statement. Parameter $name is 440# passed a name for the test case (i.e. "fts3_malloc-1.4.1") and parameter 441# $sql is passed the text of the SELECT statement. Parameter $result is 442# set to the expected output if the SELECT statement is successfully 443# executed using [db eval]. 444# 445# Example: 446# 447# do_select_test testcase-1.1 "SELECT 1+1, 1+2" {1 2} 448# 449# If global variable DO_MALLOC_TEST is set to a non-zero value, or if 450# it is not defined at all, then OOM testing is performed on the SELECT 451# statement. Each OOM test case is said to pass if either (a) executing 452# the SELECT statement succeeds and the results match those specified 453# by parameter $result, or (b) TCL throws an "out of memory" error. 454# 455# If DO_MALLOC_TEST is defined and set to zero, then the SELECT statement 456# is executed just once. In this case the test case passes if the results 457# match the expected results passed via parameter $result. 458# 459proc do_select_test {name sql result} { 460 uplevel [list doPassiveTest 0 $name $sql [list 0 $result]] 461} 462 463proc do_restart_select_test {name sql result} { 464 uplevel [list doPassiveTest 1 $name $sql [list 0 $result]] 465} 466 467proc do_error_test {name sql error} { 468 uplevel [list doPassiveTest 0 $name $sql [list 1 $error]] 469} 470 471proc doPassiveTest {isRestart name sql catchres} { 472 if {![info exists ::DO_MALLOC_TEST]} { set ::DO_MALLOC_TEST 1 } 473 474 switch $::DO_MALLOC_TEST { 475 0 { # No malloc failures. 476 do_test $name [list set {} [uplevel [list catchsql $sql]]] $catchres 477 return 478 } 479 1 { # Simulate transient failures. 480 set nRepeat 1 481 set zName "transient" 482 set nStartLimit 100000 483 set nBackup 1 484 } 485 2 { # Simulate persistent failures. 486 set nRepeat 1 487 set zName "persistent" 488 set nStartLimit 100000 489 set nBackup 1 490 } 491 3 { # Simulate transient failures with extra brute force. 492 set nRepeat 100000 493 set zName "ridiculous" 494 set nStartLimit 1 495 set nBackup 10 496 } 497 } 498 499 # The set of acceptable results from running [catchsql $sql]. 500 # 501 set answers [list {1 {out of memory}} $catchres] 502 set str [join $answers " OR "] 503 504 set nFail 1 505 for {set iLimit $nStartLimit} {$nFail} {incr iLimit} { 506 for {set iFail 1} {$nFail && $iFail<=$iLimit} {incr iFail} { 507 for {set iTest 0} {$iTest<$nBackup && ($iFail-$iTest)>0} {incr iTest} { 508 509 if {$isRestart} { sqlite3 db test.db } 510 511 sqlite3_memdebug_fail [expr $iFail-$iTest] -repeat $nRepeat 512 set res [uplevel [list catchsql $sql]] 513 if {[lsearch -exact $answers $res]>=0} { set res $str } 514 set testname "$name.$zName.$iFail" 515 do_test "$name.$zName.$iLimit.$iFail" [list set {} $res] $str 516 517 set nFail [sqlite3_memdebug_fail -1 -benigncnt nBenign] 518 } 519 } 520 } 521} 522 523 524#------------------------------------------------------------------------- 525# Test a single write to the database. In this case a "write" is a 526# DELETE, UPDATE or INSERT statement. 527# 528# If OOM testing is performed, there are several acceptable outcomes: 529# 530# 1) The write succeeds. No error is returned. 531# 532# 2) An "out of memory" exception is thrown and: 533# 534# a) The statement has no effect, OR 535# b) The current transaction is rolled back, OR 536# c) The statement succeeds. This can only happen if the connection 537# is in auto-commit mode (after the statement is executed, so this 538# includes COMMIT statements). 539# 540# If the write operation eventually succeeds, zero is returned. If a 541# transaction is rolled back, non-zero is returned. 542# 543# Parameter $name is the name to use for the test case (or test cases). 544# The second parameter, $tbl, should be the name of the database table 545# being modified. Parameter $sql contains the SQL statement to test. 546# 547proc do_write_test {name tbl sql} { 548 if {![info exists ::DO_MALLOC_TEST]} { set ::DO_MALLOC_TEST 1 } 549 550 # Figure out an statement to get a checksum for table $tbl. 551 db eval "SELECT * FROM $tbl" V break 552 set cksumsql "SELECT md5sum([join [concat rowid $V(*)] ,]) FROM $tbl" 553 554 # Calculate the initial table checksum. 555 set cksum1 [db one $cksumsql] 556 557 if {$::DO_MALLOC_TEST } { 558 set answers [list {1 {out of memory}} {0 {}}] 559 if {$::DO_MALLOC_TEST==1} { 560 set modes {100000 transient} 561 } else { 562 set modes {1 persistent} 563 } 564 } else { 565 set answers [list {0 {}}] 566 set modes [list 0 nofail] 567 } 568 set str [join $answers " OR "] 569 570 foreach {nRepeat zName} $modes { 571 for {set iFail 1} 1 {incr iFail} { 572 if {$::DO_MALLOC_TEST} {sqlite3_memdebug_fail $iFail -repeat $nRepeat} 573 574 set res [uplevel [list catchsql $sql]] 575 set nFail [sqlite3_memdebug_fail -1 -benigncnt nBenign] 576 if {$nFail==0} { 577 do_test $name.$zName.$iFail [list set {} $res] {0 {}} 578 return 579 } else { 580 if {[lsearch $answers $res]>=0} { 581 set res $str 582 } 583 do_test $name.$zName.$iFail [list set {} $res] $str 584 set cksum2 [db one $cksumsql] 585 if {$cksum1 != $cksum2} return 586 } 587 } 588 } 589} 590