xref: /sqlite-3.40.0/test/malloc_common.tcl (revision b0ac3e3a)
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