Index: third_party/sqlite/src/test/tester.tcl |
diff --git a/third_party/sqlite/src/test/tester.tcl b/third_party/sqlite/src/test/tester.tcl |
index bae10530c6b30a3ed6a0d733c7523652fb26dc7e..dad22661bdeb89c130b335d29a05cdfdcb5f90ff 100644 |
--- a/third_party/sqlite/src/test/tester.tcl |
+++ b/third_party/sqlite/src/test/tester.tcl |
@@ -14,18 +14,24 @@ |
# $Id: tester.tcl,v 1.143 2009/04/09 01:23:49 drh Exp $ |
#------------------------------------------------------------------------- |
-# The commands provided by the code in this file to help with creating |
+# The commands provided by the code in this file to help with creating |
# test cases are as follows: |
# |
# Commands to manipulate the db and the file-system at a high level: |
# |
+# is_relative_file |
+# test_pwd |
+# get_pwd |
# copy_file FROM TO |
-# drop_all_table ?DB? |
+# delete_file FILENAME |
+# drop_all_tables ?DB? |
+# forcecopy FROM TO |
# forcedelete FILENAME |
# |
# Test the capability of the SQLite version built into the interpreter to |
# determine if a specific test can be run: |
# |
+# capable EXPR |
# ifcapable EXPR |
# |
# Calulate checksums based on database contents: |
@@ -36,6 +42,7 @@ |
# |
# Commands to execute/explain SQL statements: |
# |
+# memdbsql SQL |
# stepsql DB SQL |
# execsql2 SQL |
# explain_no_trace SQL |
@@ -48,14 +55,16 @@ |
# do_ioerr_test TESTNAME ARGS... |
# crashsql ARGS... |
# integrity_check TESTNAME ?DB? |
+# verify_ex_errcode TESTNAME EXPECTED ?DB? |
# do_test TESTNAME SCRIPT EXPECTED |
# do_execsql_test TESTNAME SQL EXPECTED |
# do_catchsql_test TESTNAME SQL EXPECTED |
+# do_timed_execsql_test TESTNAME SQL EXPECTED |
# |
# Commands providing a lower level interface to the global test counters: |
# |
# set_test_counter COUNTER ?VALUE? |
-# omit_test TESTNAME REASON |
+# omit_test TESTNAME REASON ?APPEND? |
# fail_test TESTNAME |
# incr_ntest |
# |
@@ -73,7 +82,7 @@ |
# presql |
# |
-# Set the precision of FP arithmatic used by the interpreter. And |
+# Set the precision of FP arithmatic used by the interpreter. And |
# configure SQLite to take database file locks on the page that begins |
# 64KB into the database file instead of the one 1GB in. This means |
# the code that handles that special case can be tested without creating |
@@ -83,7 +92,7 @@ set tcl_precision 15 |
sqlite3_test_control_pending_byte 0x0010000 |
-# If the pager codec is available, create a wrapper for the [sqlite3] |
+# If the pager codec is available, create a wrapper for the [sqlite3] |
# command that appends "-key {xyzzy}" to the command line. i.e. this: |
# |
# sqlite3 db test.db |
@@ -115,14 +124,235 @@ if {[info command sqlite_orig]==""} { |
} |
set res |
} else { |
- # This command is not opening a new database connection. Pass the |
- # arguments through to the C implemenation as the are. |
+ # This command is not opening a new database connection. Pass the |
+ # arguments through to the C implementation as the are. |
# |
uplevel 1 sqlite_orig $args |
} |
} |
} |
+proc getFileRetries {} { |
+ if {![info exists ::G(file-retries)]} { |
+ # |
+ # NOTE: Return the default number of retries for [file] operations. A |
+ # value of zero or less here means "disabled". |
+ # |
+ return [expr {$::tcl_platform(platform) eq "windows" ? 50 : 0}] |
+ } |
+ return $::G(file-retries) |
+} |
+ |
+proc getFileRetryDelay {} { |
+ if {![info exists ::G(file-retry-delay)]} { |
+ # |
+ # NOTE: Return the default number of milliseconds to wait when retrying |
+ # failed [file] operations. A value of zero or less means "do not |
+ # wait". |
+ # |
+ return 100; # TODO: Good default? |
+ } |
+ return $::G(file-retry-delay) |
+} |
+ |
+# Return the string representing the name of the current directory. On |
+# Windows, the result is "normalized" to whatever our parent command shell |
+# is using to prevent case-mismatch issues. |
+# |
+proc get_pwd {} { |
+ if {$::tcl_platform(platform) eq "windows"} { |
+ # |
+ # NOTE: Cannot use [file normalize] here because it would alter the |
+ # case of the result to what Tcl considers canonical, which would |
+ # defeat the purpose of this procedure. |
+ # |
+ return [string map [list \\ /] \ |
+ [string trim [exec -- $::env(ComSpec) /c echo %CD%]]] |
+ } else { |
+ return [pwd] |
+ } |
+} |
+ |
+# Copy file $from into $to. This is used because some versions of |
+# TCL for windows (notably the 8.4.1 binary package shipped with the |
+# current mingw release) have a broken "file copy" command. |
+# |
+proc copy_file {from to} { |
+ do_copy_file false $from $to |
+} |
+ |
+proc forcecopy {from to} { |
+ do_copy_file true $from $to |
+} |
+ |
+proc do_copy_file {force from to} { |
+ set nRetry [getFileRetries] ;# Maximum number of retries. |
+ set nDelay [getFileRetryDelay] ;# Delay in ms before retrying. |
+ |
+ # On windows, sometimes even a [file copy -force] can fail. The cause is |
+ # usually "tag-alongs" - programs like anti-virus software, automatic backup |
+ # tools and various explorer extensions that keep a file open a little longer |
+ # than we expect, causing the delete to fail. |
+ # |
+ # The solution is to wait a short amount of time before retrying the copy. |
+ # |
+ if {$nRetry > 0} { |
+ for {set i 0} {$i<$nRetry} {incr i} { |
+ set rc [catch { |
+ if {$force} { |
+ file copy -force $from $to |
+ } else { |
+ file copy $from $to |
+ } |
+ } msg] |
+ if {$rc==0} break |
+ if {$nDelay > 0} { after $nDelay } |
+ } |
+ if {$rc} { error $msg } |
+ } else { |
+ if {$force} { |
+ file copy -force $from $to |
+ } else { |
+ file copy $from $to |
+ } |
+ } |
+} |
+ |
+# Check if a file name is relative |
+# |
+proc is_relative_file { file } { |
+ return [expr {[file pathtype $file] != "absolute"}] |
+} |
+ |
+# If the VFS supports using the current directory, returns [pwd]; |
+# otherwise, it returns only the provided suffix string (which is |
+# empty by default). |
+# |
+proc test_pwd { args } { |
+ if {[llength $args] > 0} { |
+ set suffix1 [lindex $args 0] |
+ if {[llength $args] > 1} { |
+ set suffix2 [lindex $args 1] |
+ } else { |
+ set suffix2 $suffix1 |
+ } |
+ } else { |
+ set suffix1 ""; set suffix2 "" |
+ } |
+ ifcapable curdir { |
+ return "[get_pwd]$suffix1" |
+ } else { |
+ return $suffix2 |
+ } |
+} |
+ |
+# Delete a file or directory |
+# |
+proc delete_file {args} { |
+ do_delete_file false {*}$args |
+} |
+ |
+proc forcedelete {args} { |
+ do_delete_file true {*}$args |
+} |
+ |
+proc do_delete_file {force args} { |
+ set nRetry [getFileRetries] ;# Maximum number of retries. |
+ set nDelay [getFileRetryDelay] ;# Delay in ms before retrying. |
+ |
+ foreach filename $args { |
+ # On windows, sometimes even a [file delete -force] can fail just after |
+ # a file is closed. The cause is usually "tag-alongs" - programs like |
+ # anti-virus software, automatic backup tools and various explorer |
+ # extensions that keep a file open a little longer than we expect, causing |
+ # the delete to fail. |
+ # |
+ # The solution is to wait a short amount of time before retrying the |
+ # delete. |
+ # |
+ if {$nRetry > 0} { |
+ for {set i 0} {$i<$nRetry} {incr i} { |
+ set rc [catch { |
+ if {$force} { |
+ file delete -force $filename |
+ } else { |
+ file delete $filename |
+ } |
+ } msg] |
+ if {$rc==0} break |
+ if {$nDelay > 0} { after $nDelay } |
+ } |
+ if {$rc} { error $msg } |
+ } else { |
+ if {$force} { |
+ file delete -force $filename |
+ } else { |
+ file delete $filename |
+ } |
+ } |
+ } |
+} |
+ |
+if {$::tcl_platform(platform) eq "windows"} { |
+ proc do_remove_win32_dir {args} { |
+ set nRetry [getFileRetries] ;# Maximum number of retries. |
+ set nDelay [getFileRetryDelay] ;# Delay in ms before retrying. |
+ |
+ foreach dirName $args { |
+ # On windows, sometimes even a [remove_win32_dir] can fail just after |
+ # a directory is emptied. The cause is usually "tag-alongs" - programs |
+ # like anti-virus software, automatic backup tools and various explorer |
+ # extensions that keep a file open a little longer than we expect, |
+ # causing the delete to fail. |
+ # |
+ # The solution is to wait a short amount of time before retrying the |
+ # removal. |
+ # |
+ if {$nRetry > 0} { |
+ for {set i 0} {$i < $nRetry} {incr i} { |
+ set rc [catch { |
+ remove_win32_dir $dirName |
+ } msg] |
+ if {$rc == 0} break |
+ if {$nDelay > 0} { after $nDelay } |
+ } |
+ if {$rc} { error $msg } |
+ } else { |
+ remove_win32_dir $dirName |
+ } |
+ } |
+ } |
+ |
+ proc do_delete_win32_file {args} { |
+ set nRetry [getFileRetries] ;# Maximum number of retries. |
+ set nDelay [getFileRetryDelay] ;# Delay in ms before retrying. |
+ |
+ foreach fileName $args { |
+ # On windows, sometimes even a [delete_win32_file] can fail just after |
+ # a file is closed. The cause is usually "tag-alongs" - programs like |
+ # anti-virus software, automatic backup tools and various explorer |
+ # extensions that keep a file open a little longer than we expect, |
+ # causing the delete to fail. |
+ # |
+ # The solution is to wait a short amount of time before retrying the |
+ # delete. |
+ # |
+ if {$nRetry > 0} { |
+ for {set i 0} {$i < $nRetry} {incr i} { |
+ set rc [catch { |
+ delete_win32_file $fileName |
+ } msg] |
+ if {$rc == 0} break |
+ if {$nDelay > 0} { after $nDelay } |
+ } |
+ if {$rc} { error $msg } |
+ } else { |
+ delete_win32_file $fileName |
+ } |
+ } |
+ } |
+} |
+ |
proc execpresql {handle args} { |
trace remove execution $handle enter [list execpresql $handle] |
if {[info exists ::G(perm:presql)]} { |
@@ -144,8 +374,8 @@ proc do_not_use_codec {} { |
# |
if {[info exists cmdlinearg]==0} { |
- # Parse any options specified in the $argv array. This script accepts the |
- # following options: |
+ # Parse any options specified in the $argv array. This script accepts the |
+ # following options: |
# |
# --pause |
# --soft-heap-limit=NN |
@@ -154,7 +384,10 @@ if {[info exists cmdlinearg]==0} { |
# --backtrace=N |
# --binarylog=N |
# --soak=N |
+ # --file-retries=N |
+ # --file-retry-delay=N |
# --start=[$permutation:]$testfile |
+ # --match=$pattern |
# |
set cmdlinearg(soft-heap-limit) 0 |
set cmdlinearg(maxerror) 1000 |
@@ -162,13 +395,16 @@ if {[info exists cmdlinearg]==0} { |
set cmdlinearg(backtrace) 10 |
set cmdlinearg(binarylog) 0 |
set cmdlinearg(soak) 0 |
- set cmdlinearg(start) "" |
+ set cmdlinearg(file-retries) 0 |
+ set cmdlinearg(file-retry-delay) 0 |
+ set cmdlinearg(start) "" |
+ set cmdlinearg(match) "" |
set leftover [list] |
foreach a $argv { |
switch -regexp -- $a { |
{^-+pause$} { |
- # Wait for user input before continuing. This is to give the user an |
+ # Wait for user input before continuing. This is to give the user an |
# opportunity to connect profiling tools to the process. |
puts -nonewline "Press RETURN to begin..." |
flush stdout |
@@ -197,6 +433,14 @@ if {[info exists cmdlinearg]==0} { |
foreach {dummy cmdlinearg(soak)} [split $a =] break |
set ::G(issoak) $cmdlinearg(soak) |
} |
+ {^-+file-retries=.+$} { |
+ foreach {dummy cmdlinearg(file-retries)} [split $a =] break |
+ set ::G(file-retries) $cmdlinearg(file-retries) |
+ } |
+ {^-+file-retry-delay=.+$} { |
+ foreach {dummy cmdlinearg(file-retry-delay)} [split $a =] break |
+ set ::G(file-retry-delay) $cmdlinearg(file-retry-delay) |
+ } |
{^-+start=.+$} { |
foreach {dummy cmdlinearg(start)} [split $a =] break |
@@ -207,6 +451,12 @@ if {[info exists cmdlinearg]==0} { |
} |
if {$::G(start:file) == ""} {unset ::G(start:file)} |
} |
+ {^-+match=.+$} { |
+ foreach {dummy cmdlinearg(match)} [split $a =] break |
+ |
+ set ::G(match) $cmdlinearg(match) |
+ if {$::G(match) == ""} {unset ::G(match)} |
+ } |
default { |
lappend leftover $a |
} |
@@ -217,8 +467,8 @@ if {[info exists cmdlinearg]==0} { |
# Install the malloc layer used to inject OOM errors. And the 'automatic' |
# extensions. This only needs to be done once for the process. |
# |
- sqlite3_shutdown |
- install_malloc_faultsim 1 |
+ sqlite3_shutdown |
+ install_malloc_faultsim 1 |
sqlite3_initialize |
autoinstall_test_functions |
@@ -246,9 +496,9 @@ sqlite3_soft_heap_limit $cmdlinearg(soft-heap-limit) |
# |
proc reset_db {} { |
catch {db close} |
- file delete -force test.db |
- file delete -force test.db-journal |
- file delete -force test.db-wal |
+ forcedelete test.db |
+ forcedelete test.db-journal |
+ forcedelete test.db-wal |
sqlite3 db ./test.db |
set ::DB [sqlite3_connection_pointer db] |
if {[info exists ::SETUP_SQL]} { |
@@ -274,6 +524,7 @@ if {0==[info exists ::SLAVE]} { |
set TC(count) 0 |
set TC(fail_list) [list] |
set TC(omit_list) [list] |
+ set TC(warn_list) [list] |
proc set_test_counter {counter args} { |
if {[llength $args]} { |
@@ -285,9 +536,11 @@ if {0==[info exists ::SLAVE]} { |
# Record the fact that a sequence of tests were omitted. |
# |
-proc omit_test {name reason} { |
+proc omit_test {name reason {append 1}} { |
set omitList [set_test_counter omit_list] |
- lappend omitList [list $name $reason] |
+ if {$append} { |
+ lappend omitList [list $name $reason] |
+ } |
set_test_counter omit_list $omitList |
} |
@@ -306,6 +559,18 @@ proc fail_test {name} { |
} |
} |
+# Remember a warning message to be displayed at the conclusion of all testing |
+# |
+proc warning {msg {append 1}} { |
+ puts "Warning: $msg" |
+ set warnList [set_test_counter warn_list] |
+ if {$append} { |
+ lappend warnList $msg |
+ } |
+ set_test_counter warn_list $warnList |
+} |
+ |
+ |
# Increment the number of tests run |
# |
proc incr_ntest {} { |
@@ -313,17 +578,16 @@ proc incr_ntest {} { |
} |
-# Invoke the do_test procedure to run a single test |
+# Invoke the do_test procedure to run a single test |
# |
proc do_test {name cmd expected} { |
- |
global argv cmdlinearg |
fix_testname name |
sqlite3_memdebug_settitle $name |
-# if {[llength $argv]==0} { |
+# if {[llength $argv]==0} { |
# set go 1 |
# } else { |
# set go 0 |
@@ -342,34 +606,122 @@ proc do_test {name cmd expected} { |
incr_ntest |
puts -nonewline $name... |
flush stdout |
- if {[catch {uplevel #0 "$cmd;\n"} result]} { |
- puts "\nError: $result" |
- fail_test $name |
- } elseif {[string compare $result $expected]} { |
- puts "\nExpected: \[$expected\]\n Got: \[$result\]" |
- fail_test $name |
+ |
+ if {![info exists ::G(match)] || [string match $::G(match) $name]} { |
+ if {[catch {uplevel #0 "$cmd;\n"} result]} { |
+ puts "\nError: $result" |
+ fail_test $name |
+ } else { |
+ if {[regexp {^~?/.*/$} $expected]} { |
+ # "expected" is of the form "/PATTERN/" then the result if correct if |
+ # regular expression PATTERN matches the result. "~/PATTERN/" means |
+ # the regular expression must not match. |
+ if {[string index $expected 0]=="~"} { |
+ set re [string range $expected 2 end-1] |
+ if {[string index $re 0]=="*"} { |
+ # If the regular expression begins with * then treat it as a glob instead |
+ set ok [string match $re $result] |
+ } else { |
+ set re [string map {# {[-0-9.]+}} $re] |
+ set ok [regexp $re $result] |
+ } |
+ set ok [expr {!$ok}] |
+ } else { |
+ set re [string range $expected 1 end-1] |
+ if {[string index $re 0]=="*"} { |
+ # If the regular expression begins with * then treat it as a glob instead |
+ set ok [string match $re $result] |
+ } else { |
+ set re [string map {# {[-0-9.]+}} $re] |
+ set ok [regexp $re $result] |
+ } |
+ } |
+ } elseif {[regexp {^~?\*.*\*$} $expected]} { |
+ # "expected" is of the form "*GLOB*" then the result if correct if |
+ # glob pattern GLOB matches the result. "~/GLOB/" means |
+ # the glob must not match. |
+ if {[string index $expected 0]=="~"} { |
+ set e [string range $expected 1 end] |
+ set ok [expr {![string match $e $result]}] |
+ } else { |
+ set ok [string match $expected $result] |
+ } |
+ } else { |
+ set ok [expr {[string compare $result $expected]==0}] |
+ } |
+ if {!$ok} { |
+ # if {![info exists ::testprefix] || $::testprefix eq ""} { |
+ # error "no test prefix" |
+ # } |
+ puts "\nExpected: \[$expected\]\n Got: \[$result\]" |
+ fail_test $name |
+ } else { |
+ puts " Ok" |
+ } |
+ } |
} else { |
- puts " Ok" |
+ puts " Omitted" |
+ omit_test $name "pattern mismatch" 0 |
} |
flush stdout |
} |
+proc catchcmd {db {cmd ""}} { |
+ global CLI |
+ set out [open cmds.txt w] |
+ puts $out $cmd |
+ close $out |
+ set line "exec $CLI $db < cmds.txt" |
+ set rc [catch { eval $line } msg] |
+ list $rc $msg |
+} |
+ |
+proc filepath_normalize {p} { |
+ # test cases should be written to assume "unix"-like file paths |
+ if {$::tcl_platform(platform)!="unix"} { |
+ # lreverse*2 as a hack to remove any unneeded {} after the string map |
+ lreverse [lreverse [string map {\\ /} [regsub -nocase -all {[a-z]:[/\\]+} $p {/}]]] |
+ } { |
+ set p |
+ } |
+} |
+proc do_filepath_test {name cmd expected} { |
+ uplevel [list do_test $name [ |
+ subst -nocommands { filepath_normalize [ $cmd ] } |
+ ] [filepath_normalize $expected]] |
+} |
+ |
+proc realnum_normalize {r} { |
+ # different TCL versions display floating point values differently. |
+ string map {1.#INF inf Inf inf .0e e} [regsub -all {(e[+-])0+} $r {\1}] |
+} |
+proc do_realnum_test {name cmd expected} { |
+ uplevel [list do_test $name [ |
+ subst -nocommands { realnum_normalize [ $cmd ] } |
+ ] [realnum_normalize $expected]] |
+} |
+ |
proc fix_testname {varname} { |
upvar $varname testname |
- if {[info exists ::testprefix] |
+ if {[info exists ::testprefix] |
&& [string is digit [string range $testname 0 0]] |
} { |
set testname "${::testprefix}-$testname" |
} |
} |
- |
+ |
proc do_execsql_test {testname sql {result {}}} { |
fix_testname testname |
- uplevel do_test $testname [list "execsql {$sql}"] [list [list {*}$result]] |
+ uplevel do_test [list $testname] [list "execsql {$sql}"] [list [list {*}$result]] |
} |
proc do_catchsql_test {testname sql result} { |
fix_testname testname |
- uplevel do_test $testname [list "catchsql {$sql}"] [list $result] |
+ uplevel do_test [list $testname] [list "catchsql {$sql}"] [list $result] |
+} |
+proc do_timed_execsql_test {testname sql {result {}}} { |
+ fix_testname testname |
+ uplevel do_test [list $testname] [list "execsql_timed {$sql}"]\ |
+ [list [list {*}$result]] |
} |
proc do_eqp_test {name sql res} { |
uplevel do_execsql_test $name [list "EXPLAIN QUERY PLAN $sql"] [list $res] |
@@ -448,7 +800,7 @@ proc delete_all_data {} { |
} |
} |
-# Run an SQL script. |
+# Run an SQL script. |
# Return the number of microseconds per statement. |
# |
proc speed_trial {name numstmt units sql} { |
@@ -511,6 +863,7 @@ proc speed_trial_summary {name} { |
# |
proc finish_test {} { |
catch {db close} |
+ catch {db1 close} |
catch {db2 close} |
catch {db3 close} |
if {0==[info exists ::SLAVE]} { finalize_testing } |
@@ -534,9 +887,31 @@ proc finalize_testing {} { |
set nTest [incr_ntest] |
set nErr [set_test_counter errors] |
- puts "$nErr errors out of $nTest tests" |
- if {$nErr>0} { |
- puts "Failures on these tests: [set_test_counter fail_list]" |
+ set nKnown 0 |
+ if {[file readable known-problems.txt]} { |
+ set fd [open known-problems.txt] |
+ set content [read $fd] |
+ close $fd |
+ foreach x $content {set known_error($x) 1} |
+ foreach x [set_test_counter fail_list] { |
+ if {[info exists known_error($x)]} {incr nKnown} |
+ } |
+ } |
+ if {$nKnown>0} { |
+ puts "[expr {$nErr-$nKnown}] new errors and $nKnown known errors\ |
+ out of $nTest tests" |
+ } else { |
+ puts "$nErr errors out of $nTest tests" |
+ } |
+ if {$nErr>$nKnown} { |
+ puts -nonewline "Failures on these tests:" |
+ foreach x [set_test_counter fail_list] { |
+ if {![info exists known_error($x)]} {puts -nonewline " $x"} |
+ } |
+ puts "" |
+ } |
+ foreach warning [set_test_counter warn_list] { |
+ puts "Warning: $warning" |
} |
run_thread_tests 1 |
if {[llength $omitList]>0} { |
@@ -597,10 +972,10 @@ proc finalize_testing {} { |
} |
} |
foreach f [glob -nocomplain test.db-*-journal] { |
- file delete -force $f |
+ forcedelete $f |
} |
foreach f [glob -nocomplain test.db-mj*] { |
- file delete -force $f |
+ forcedelete $f |
} |
exit [expr {$nErr>0}] |
} |
@@ -645,6 +1020,14 @@ proc execsql {sql {db db}} { |
# puts "SQL = $sql" |
uplevel [list $db eval $sql] |
} |
+proc execsql_timed {sql {db db}} { |
+ set tm [time { |
+ set x [uplevel [list $db eval $sql]] |
+ } 1] |
+ set tm [lindex $tm 0] |
+ puts -nonewline " ([expr {$tm*0.001}]ms) " |
+ set x |
+} |
# Execute SQL and catch exceptions. |
# |
@@ -668,6 +1051,89 @@ proc explain {sql {db db}} { |
} |
} |
+proc explain_i {sql {db db}} { |
+ puts "" |
+ puts "addr opcode p1 p2 p3 p4 p5 #" |
+ puts "---- ------------ ------ ------ ------ ---------------- -- -" |
+ |
+ |
+ # Set up colors for the different opcodes. Scheme is as follows: |
+ # |
+ # Red: Opcodes that write to a b-tree. |
+ # Blue: Opcodes that reposition or seek a cursor. |
+ # Green: The ResultRow opcode. |
+ # |
+ if { [catch {fconfigure stdout -mode}]==0 } { |
+ set R "\033\[31;1m" ;# Red fg |
+ set G "\033\[32;1m" ;# Green fg |
+ set B "\033\[34;1m" ;# Red fg |
+ set D "\033\[39;0m" ;# Default fg |
+ } else { |
+ set R "" |
+ set G "" |
+ set B "" |
+ set D "" |
+ } |
+ foreach opcode { |
+ Seek SeekGe SeekGt SeekLe SeekLt NotFound Last Rewind |
+ NoConflict Next Prev VNext VPrev VFilter |
+ SorterSort SorterNext |
+ } { |
+ set color($opcode) $B |
+ } |
+ foreach opcode {ResultRow} { |
+ set color($opcode) $G |
+ } |
+ foreach opcode {IdxInsert Insert Delete IdxDelete} { |
+ set color($opcode) $R |
+ } |
+ |
+ set bSeenGoto 0 |
+ $db eval "explain $sql" {} { |
+ set x($addr) 0 |
+ set op($addr) $opcode |
+ |
+ if {$opcode == "Goto" && ($bSeenGoto==0 || ($p2 > $addr+10))} { |
+ set linebreak($p2) 1 |
+ set bSeenGoto 1 |
+ } |
+ |
+ if {$opcode=="Next" || $opcode=="Prev" |
+ || $opcode=="VNext" || $opcode=="VPrev" |
+ || $opcode=="SorterNext" |
+ } { |
+ for {set i $p2} {$i<$addr} {incr i} { |
+ incr x($i) 2 |
+ } |
+ } |
+ |
+ if {$opcode == "Goto" && $p2<$addr && $op($p2)=="Yield"} { |
+ for {set i [expr $p2+1]} {$i<$addr} {incr i} { |
+ incr x($i) 2 |
+ } |
+ } |
+ |
+ if {$opcode == "Halt" && $comment == "End of coroutine"} { |
+ set linebreak([expr $addr+1]) 1 |
+ } |
+ } |
+ |
+ $db eval "explain $sql" {} { |
+ if {[info exists linebreak($addr)]} { |
+ puts "" |
+ } |
+ set I [string repeat " " $x($addr)] |
+ |
+ set col "" |
+ catch { set col $color($opcode) } |
+ |
+ puts [format {%-4d %s%s%-12.12s%s %-6d %-6d %-6d % -17s %s %s} \ |
+ $addr $I $col $opcode $D $p1 $p2 $p3 $p4 $p5 $comment |
+ ] |
+ } |
+ puts "---- ------------ ------ ------ ------ ---------------- -- -" |
+} |
+ |
# Show the VDBE program for an SQL statement but omit the Trace |
# opcode at the beginning. This procedure can be used to prove |
# that different SQL statements generate exactly the same VDBE code. |
@@ -690,6 +1156,15 @@ proc execsql2 {sql} { |
return $result |
} |
+# Use a temporary in-memory database to execute SQL statements |
+# |
+proc memdbsql {sql} { |
+ sqlite3 memdb :memory: |
+ set result [memdb eval $sql] |
+ memdb close |
+ return $result |
+} |
+ |
# Use the non-callback API to execute multiple SQL statements |
# |
proc stepsql {dbptr sql} { |
@@ -715,30 +1190,6 @@ proc stepsql {dbptr sql} { |
return $r |
} |
-# Delete a file or directory |
-# |
-proc forcedelete {args} { |
- foreach filename $args { |
- # On windows, sometimes even a [file delete -force] can fail just after |
- # a file is closed. The cause is usually "tag-alongs" - programs like |
- # anti-virus software, automatic backup tools and various explorer |
- # extensions that keep a file open a little longer than we expect, causing |
- # the delete to fail. |
- # |
- # The solution is to wait a short amount of time before retrying the |
- # delete. |
- # |
- set nRetry 50 ;# Maximum number of retries. |
- set nDelay 100 ;# Delay in ms before retrying. |
- for {set i 0} {$i<$nRetry} {incr i} { |
- set rc [catch {file delete -force $filename} msg] |
- if {$rc==0} break |
- after $nDelay |
- } |
- if {$rc} { error $msg } |
- } |
-} |
- |
# Do an integrity check of the entire database |
# |
proc integrity_check {name {db db}} { |
@@ -747,6 +1198,23 @@ proc integrity_check {name {db db}} { |
} |
} |
+# Check the extended error code |
+# |
+proc verify_ex_errcode {name expected {db db}} { |
+ do_test $name [list sqlite3_extended_errcode $db] $expected |
+} |
+ |
+ |
+# Return true if the SQL statement passed as the second argument uses a |
+# statement transaction. |
+# |
+proc sql_uses_stmt {db sql} { |
+ set stmt [sqlite3_prepare $db $sql -1 dummy] |
+ set uses [uses_stmt_journal $stmt] |
+ sqlite3_finalize $stmt |
+ return $uses |
+} |
+ |
proc fix_ifcapable_expr {expr} { |
set ret "" |
set state 0 |
@@ -766,6 +1234,12 @@ proc fix_ifcapable_expr {expr} { |
return $ret |
} |
+# Returns non-zero if the capabilities are present; zero otherwise. |
+# |
+proc capable {expr} { |
+ set e [fix_ifcapable_expr $expr]; return [expr ($e)] |
+} |
+ |
# Evaluate a boolean expression of capabilities. If true, execute the |
# code. Omit the code if false. |
# |
@@ -792,7 +1266,7 @@ proc ifcapable {expr code {else ""} {elsecode ""}} { |
# boolean, indicating whether or not the process actually crashed or |
# reported some other error. The second element in the returned list is the |
# error message. This is "child process exited abnormally" if the crash |
-# occured. |
+# occurred. |
# |
# crashsql -delay CRASHDELAY -file CRASHFILE ?-blocksize BLOCKSIZE? $sql |
# |
@@ -801,17 +1275,19 @@ proc crashsql {args} { |
set blocksize "" |
set crashdelay 1 |
set prngseed 0 |
+ set opendb { sqlite3 db test.db -vfs crash } |
set tclbody {} |
set crashfile "" |
set dc "" |
set sql [lindex $args end] |
- |
+ |
for {set ii 0} {$ii < [llength $args]-1} {incr ii 2} { |
set z [lindex $args $ii] |
set n [string length $z] |
set z2 [lindex $args [expr $ii+1]] |
if {$n>1 && [string first $z -delay]==0} {set crashdelay $z2} \ |
+ elseif {$n>1 && [string first $z -opendb]==0} {set opendb $z2} \ |
elseif {$n>1 && [string first $z -seed]==0} {set prngseed $z2} \ |
elseif {$n>1 && [string first $z -file]==0} {set crashfile $z2} \ |
elseif {$n>1 && [string first $z -tclbody]==0} {set tclbody $z2} \ |
@@ -824,16 +1300,16 @@ proc crashsql {args} { |
error "Compulsory option -file missing" |
} |
- # $crashfile gets compared to the native filename in |
+ # $crashfile gets compared to the native filename in |
# cfSync(), which can be different then what TCL uses by |
# default, so here we force it to the "nativename" format. |
- set cfile [string map {\\ \\\\} [file nativename [file join [pwd] $crashfile]]] |
+ set cfile [string map {\\ \\\\} [file nativename [file join [get_pwd] $crashfile]]] |
set f [open crash.tcl w] |
puts $f "sqlite3_crash_enable 1" |
puts $f "sqlite3_crashparams $blocksize $dc $crashdelay $cfile" |
puts $f "sqlite3_test_control_pending_byte $::sqlite_pending_byte" |
- puts $f "sqlite3 db test.db -vfs crash" |
+ puts $f $opendb |
# This block sets the cache size of the main database to 10 |
# pages. This is done in case the build is configured to omit |
@@ -841,6 +1317,7 @@ proc crashsql {args} { |
puts $f {db eval {SELECT * FROM sqlite_master;}} |
puts $f {set bt [btree_from_db db]} |
puts $f {btree_set_cache_size $bt 10} |
+ |
if {$prngseed} { |
set seed [expr {$prngseed%10007+1}] |
# puts seed=$seed |
@@ -859,7 +1336,7 @@ proc crashsql {args} { |
set r [catch { |
exec [info nameofexec] crash.tcl >@stdout |
} msg] |
- |
+ |
# Windows/ActiveState TCL returns a slightly different |
# error message. We map that to the expected message |
# so that we don't have to change all of the test |
@@ -869,14 +1346,33 @@ proc crashsql {args} { |
set msg "child process exited abnormally" |
} |
} |
- |
+ |
lappend r $msg |
} |
+proc run_ioerr_prep {} { |
+ set ::sqlite_io_error_pending 0 |
+ catch {db close} |
+ catch {db2 close} |
+ catch {forcedelete test.db} |
+ catch {forcedelete test.db-journal} |
+ catch {forcedelete test2.db} |
+ catch {forcedelete test2.db-journal} |
+ set ::DB [sqlite3 db test.db; sqlite3_connection_pointer db] |
+ sqlite3_extended_result_codes $::DB $::ioerropts(-erc) |
+ if {[info exists ::ioerropts(-tclprep)]} { |
+ eval $::ioerropts(-tclprep) |
+ } |
+ if {[info exists ::ioerropts(-sqlprep)]} { |
+ execsql $::ioerropts(-sqlprep) |
+ } |
+ expr 0 |
+} |
+ |
# Usage: do_ioerr_test <test number> <options...> |
# |
# This proc is used to implement test cases that check that IO errors |
-# are correctly handled. The first argument, <test number>, is an integer |
+# are correctly handled. The first argument, <test number>, is an integer |
# used to name the tests executed by this proc. Options are as follows: |
# |
# -tclprep TCL script to run to prepare test. |
@@ -906,14 +1402,30 @@ proc do_ioerr_test {testname args} { |
# a couple of obscure IO errors that do not return them. |
set ::ioerropts(-erc) 0 |
+ # Create a single TCL script from the TCL and SQL specified |
+ # as the body of the test. |
+ set ::ioerrorbody {} |
+ if {[info exists ::ioerropts(-tclbody)]} { |
+ append ::ioerrorbody "$::ioerropts(-tclbody)\n" |
+ } |
+ if {[info exists ::ioerropts(-sqlbody)]} { |
+ append ::ioerrorbody "db eval {$::ioerropts(-sqlbody)}" |
+ } |
+ |
+ save_prng_state |
+ if {$::ioerropts(-cksum)} { |
+ run_ioerr_prep |
+ eval $::ioerrorbody |
+ set ::goodcksum [cksum] |
+ } |
+ |
set ::go 1 |
#reset_prng_state |
- save_prng_state |
for {set n $::ioerropts(-start)} {$::go} {incr n} { |
set ::TN $n |
incr ::ioerropts(-count) -1 |
if {$::ioerropts(-count)<0} break |
- |
+ |
# Skip this IO error if it was specified with the "-exclude" option. |
if {[info exists ::ioerropts(-exclude)]} { |
if {[lsearch $::ioerropts(-exclude) $n]!=-1} continue |
@@ -922,30 +1434,15 @@ proc do_ioerr_test {testname args} { |
restore_prng_state |
} |
- # Delete the files test.db and test2.db, then execute the TCL and |
+ # Delete the files test.db and test2.db, then execute the TCL and |
# SQL (in that order) to prepare for the test case. |
do_test $testname.$n.1 { |
- set ::sqlite_io_error_pending 0 |
- catch {db close} |
- catch {db2 close} |
- catch {file delete -force test.db} |
- catch {file delete -force test.db-journal} |
- catch {file delete -force test2.db} |
- catch {file delete -force test2.db-journal} |
- set ::DB [sqlite3 db test.db; sqlite3_connection_pointer db] |
- sqlite3_extended_result_codes $::DB $::ioerropts(-erc) |
- if {[info exists ::ioerropts(-tclprep)]} { |
- eval $::ioerropts(-tclprep) |
- } |
- if {[info exists ::ioerropts(-sqlprep)]} { |
- execsql $::ioerropts(-sqlprep) |
- } |
- expr 0 |
+ run_ioerr_prep |
} {0} |
# Read the 'checksum' of the database. |
if {$::ioerropts(-cksum)} { |
- set checksum [cksum] |
+ set ::checksum [cksum] |
} |
# Set the Nth IO error to fail. |
@@ -953,20 +1450,10 @@ proc do_ioerr_test {testname args} { |
set ::sqlite_io_error_persist $::ioerropts(-persist) |
set ::sqlite_io_error_pending $n |
}] $n |
- |
- # Create a single TCL script from the TCL and SQL specified |
- # as the body of the test. |
- set ::ioerrorbody {} |
- if {[info exists ::ioerropts(-tclbody)]} { |
- append ::ioerrorbody "$::ioerropts(-tclbody)\n" |
- } |
- if {[info exists ::ioerropts(-sqlbody)]} { |
- append ::ioerrorbody "db eval {$::ioerropts(-sqlbody)}" |
- } |
- # Execute the TCL Script created in the above block. If |
- # there are at least N IO operations performed by SQLite as |
- # a result of the script, the Nth will fail. |
+ # Execute the TCL script created for the body of this test. If |
+ # at least N IO operations performed by SQLite as a result of |
+ # the script, the Nth will fail. |
do_test $testname.$n.3 { |
set ::sqlite_io_error_hit 0 |
set ::sqlite_io_error_hardhit 0 |
@@ -1019,12 +1506,12 @@ proc do_ioerr_test {testname args} { |
set ::sqlite_io_error_hit 0 |
set ::sqlite_io_error_pending 0 |
- # Check that no page references were leaked. There should be |
- # a single reference if there is still an active transaction, |
+ # Check that no page references were leaked. There should be |
+ # a single reference if there is still an active transaction, |
# or zero otherwise. |
# |
# UPDATE: If the IO error occurs after a 'BEGIN' but before any |
- # locks are established on database files (i.e. if the error |
+ # locks are established on database files (i.e. if the error |
# occurs while attempting to detect a hot-journal file), then |
# there may 0 page references and an active transaction according |
# to [sqlite3_get_autocommit]. |
@@ -1040,7 +1527,7 @@ proc do_ioerr_test {testname args} { |
} {1} |
} |
- # If there is an open database handle and no open transaction, |
+ # If there is an open database handle and no open transaction, |
# and the pager is not running in exclusive-locking mode, |
# check that the pager is in "unlocked" state. Theoretically, |
# if a call to xUnlock() failed due to an IO error the underlying |
@@ -1062,7 +1549,7 @@ proc do_ioerr_test {testname args} { |
} |
} |
- # If an IO error occured, then the checksum of the database should |
+ # If an IO error occurred, then the checksum of the database should |
# be the same as before the script that caused the IO error was run. |
# |
if {$::go && $::sqlite_io_error_hardhit && $::ioerropts(-cksum)} { |
@@ -1070,8 +1557,15 @@ proc do_ioerr_test {testname args} { |
catch {db close} |
catch {db2 close} |
set ::DB [sqlite3 db test.db; sqlite3_connection_pointer db] |
- cksum |
- } $checksum |
+ set nowcksum [cksum] |
+ set res [expr {$nowcksum==$::checksum || $nowcksum==$::goodcksum}] |
+ if {$res==0} { |
+ puts "now=$nowcksum" |
+ puts "the=$::checksum" |
+ puts "fwd=$::goodcksum" |
+ } |
+ set res |
+ } 1 |
} |
set ::sqlite_io_error_hardhit 0 |
@@ -1137,7 +1631,7 @@ proc allcksum {{db db}} { |
} |
# Generate a checksum based on the contents of a single database with |
-# a database connection. The name of the database is $dbname. |
+# a database connection. The name of the database is $dbname. |
# Examples of $dbname are "temp" or "main". |
# |
proc dbcksum {db dbname} { |
@@ -1205,24 +1699,6 @@ proc memdebug_log_sql {{filename mallocs.sql}} { |
close $fd |
} |
-# Copy file $from into $to. This is used because some versions of |
-# TCL for windows (notably the 8.4.1 binary package shipped with the |
-# current mingw release) have a broken "file copy" command. |
-# |
-proc copy_file {from to} { |
- if {$::tcl_platform(platform)=="unix"} { |
- file copy -force $from $to |
- } else { |
- set f [open $from] |
- fconfigure $f -translation binary |
- set t [open $to w] |
- fconfigure $t -translation binary |
- puts -nonewline $t [read $f [file size $from]] |
- close $t |
- close $f |
- } |
-} |
- |
# Drop all tables in database [db] |
proc drop_all_tables {{db db}} { |
ifcapable trigger&&foreignkey { |
@@ -1249,8 +1725,8 @@ proc drop_all_tables {{db db}} { |
#------------------------------------------------------------------------- |
# If a test script is executed with global variable $::G(perm:name) set to |
-# "wal", then the tests are run in WAL mode. Otherwise, they should be run |
-# in rollback mode. The following Tcl procs are used to make this less |
+# "wal", then the tests are run in WAL mode. Otherwise, they should be run |
+# in rollback mode. The following Tcl procs are used to make this less |
# intrusive: |
# |
# wal_set_journal_mode ?DB? |
@@ -1265,9 +1741,9 @@ proc drop_all_tables {{db db}} { |
# Otherwise (if not running a WAL permutation) this is a no-op. |
# |
# wal_is_wal_mode |
-# |
+# |
# Returns true if this test should be run in WAL mode. False otherwise. |
-# |
+# |
proc wal_is_wal_mode {} { |
expr {[permutation] eq "wal"} |
} |
@@ -1368,10 +1844,10 @@ proc slave_test_file {zFile} { |
} |
set ::sqlite_open_file_count 0 |
- # Test that the global "shared-cache" setting was not altered by |
+ # Test that the global "shared-cache" setting was not altered by |
# the test script. |
# |
- ifcapable shared_cache { |
+ ifcapable shared_cache { |
set res [expr {[sqlite3_enable_shared_cache] == $scs}] |
do_test ${tail}-sharedcachesetting [list set {} $res] 1 |
} |
@@ -1404,7 +1880,7 @@ proc db_save {} { |
foreach f [glob -nocomplain sv_test.db*] { forcedelete $f } |
foreach f [glob -nocomplain test.db*] { |
set f2 "sv_$f" |
- file copy -force $f $f2 |
+ forcecopy $f $f2 |
} |
} |
proc db_save_and_close {} { |
@@ -1416,7 +1892,7 @@ proc db_restore {} { |
foreach f [glob -nocomplain test.db*] { forcedelete $f } |
foreach f2 [glob -nocomplain sv_test.db*] { |
set f [string range $f2 3 end] |
- file copy -force $f2 $f |
+ forcecopy $f2 $f |
} |
} |
proc db_restore_and_reopen {{dbfile test.db}} { |
@@ -1426,7 +1902,7 @@ proc db_restore_and_reopen {{dbfile test.db}} { |
} |
proc db_delete_and_reopen {{file test.db}} { |
catch { db close } |
- foreach f [glob -nocomplain test.db*] { file delete -force $f } |
+ foreach f [glob -nocomplain test.db*] { forcedelete $f } |
sqlite3 db $file |
} |
@@ -1434,5 +1910,14 @@ proc db_delete_and_reopen {{file test.db}} { |
# to non-zero, then set the global variable $AUTOVACUUM to 1. |
set AUTOVACUUM $sqlite_options(default_autovacuum) |
+# Make sure the FTS enhanced query syntax is disabled. |
+set sqlite_fts3_enable_parentheses 0 |
+ |
+# During testing, assume that all database files are well-formed. The |
+# few test cases that deliberately corrupt database files should rescind |
+# this setting by invoking "database_can_be_corrupt" |
+# |
+database_never_corrupt |
+ |
source $testdir/thread_common.tcl |
source $testdir/malloc_common.tcl |