| |
| #------------------------------------------------------------------------- |
| # Usage: |
| # |
| proc usage {} { |
| set a0 testrunner.tcl |
| |
| set ::argv [list] |
| uplevel [list source $::testdir/permutations.test] |
| |
| puts stderr "Usage: $a0 ?SWITCHES? ?PERMUTATION? ?PATTERNS?" |
| puts stderr "" |
| puts stderr "where SWITCHES are:" |
| puts stderr " --jobs NUMBER-OF-JOBS" |
| puts stderr "" |
| puts stderr "available PERMUTATION values are:" |
| set ii 0 |
| foreach name [lsort [array names ::testspec]] { |
| if {($ii % 3)==0} { puts -nonewline stderr " " } |
| puts -nonewline stderr [format "% -22s" $name] |
| if {($ii % 3)==2} { puts stderr "" } |
| incr ii |
| } |
| puts stderr "" |
| puts stderr "" |
| puts stderr "Examples:" |
| puts stderr " 1) Run the veryquick tests:" |
| puts stderr " $a0" |
| puts stderr " 2) Run all test scripts in the source tree:" |
| puts stderr " $a0 full" |
| puts stderr " 2) Run the 'memsubsys1' permutation:" |
| puts stderr " $a0 memsubsys1" |
| puts stderr " 3) Run all permutations usually run by \[make fulltest\]" |
| puts stderr " $a0 release" |
| puts stderr " 4) Run all scripts that match the pattern 'select%':" |
| puts stderr " $a0 select%" |
| puts stderr " $a0 all select%" |
| puts stderr " $a0 full select%" |
| puts stderr " 5) Run all scripts that are part of the veryquick permutation and match the pattern 'select%':" |
| puts stderr " $a0 veryquick select%" |
| puts stderr " 6) Run the 'memsubsys1' permutation, but just those scripts that match 'window%':" |
| puts stderr " $a0 memsubsys1 window%" |
| puts stderr " 7) Run all the permutations, but only the scripts that match either 'fts5%' or 'rtree%':" |
| puts stderr " $a0 release fts5% rtree%" |
| |
| exit 1 |
| } |
| #------------------------------------------------------------------------- |
| |
| #------------------------------------------------------------------------- |
| # The database schema used by the testrunner.db database. |
| # |
| set R(schema) { |
| DROP TABLE IF EXISTS script; |
| DROP TABLE IF EXISTS msg; |
| DROP TABLE IF EXISTS malloc; |
| |
| CREATE TABLE script( |
| config TEXT, |
| filename TEXT, -- full path to test script |
| slow BOOLEAN, -- true if script is "slow" |
| state TEXT CHECK( state IN ('ready', 'running', 'done') ), |
| testfixtureid, -- Id of process that ran script |
| time INTEGER, -- Time in ms |
| nerr INTEGER, -- if 'done', the number of errors |
| ntest INTEGER, -- if 'done', the number of tests |
| output TEXT, -- full output of test script |
| PRIMARY KEY(config, filename) |
| ); |
| |
| CREATE TABLE malloc( |
| id INTEGER PRIMARY KEY, |
| nmalloc INTEGER, |
| nbyte INTEGER, |
| leaker TEXT |
| ); |
| |
| CREATE TABLE msg( |
| id INTEGER PRIMARY KEY, |
| msg TEXT |
| ); |
| } |
| #------------------------------------------------------------------------- |
| |
| #------------------------------------------------------------------------- |
| # Try to estimate a the number of processes to use. |
| # |
| # Command [guess_number_of_cores] attempts to glean the number of logical |
| # cores. Command [default_njob] returns the default value for the --jobs |
| # switch. |
| # |
| proc guess_number_of_cores {} { |
| set ret 4 |
| |
| if {$::tcl_platform(os)=="Darwin"} { |
| set cmd "sysctl -n hw.logicalcpu" |
| } else { |
| set cmd "nproc" |
| } |
| catch { |
| set fd [open "|$cmd" r] |
| set ret [gets $fd] |
| close $fd |
| set ret [expr $ret] |
| } |
| return $ret |
| } |
| |
| proc default_njob {} { |
| set nCore [guess_number_of_cores] |
| set nHelper [expr int($nCore*0.75)] |
| expr $nHelper>0 ? $nHelper : 1 |
| } |
| #------------------------------------------------------------------------- |
| |
| |
| set R(dbname) [file normalize testrunner.db] |
| set R(logname) [file normalize testrunner.log] |
| set R(info_script) [file normalize [info script]] |
| set R(timeout) 10000 ;# Default busy-timeout for testrunner. |
| set R(nJob) [default_njob] ;# Default number of helper processes |
| set R(leaker) "" ;# Name of first script to leak memory |
| |
| set R(patternlist) [list] |
| |
| set testdir [file dirname $argv0] |
| |
| # Parse the command line options. There are two ways to invoke this |
| # script - to create a helper or coordinator process. If there are |
| # no helper processes, the coordinator runs test scripts. |
| # |
| # To create a helper process: |
| # |
| # testrunner.tcl helper ID |
| # |
| # where ID is an integer greater than 0. The process will create and |
| # run tests in the "testdir$ID" directory. Helper processes are only |
| # created by coordinators - there is no need for a user to create |
| # helper processes manually. |
| # |
| # If the first argument is anything other than "helper", then a coordinator |
| # process is started. See the implementation of the [usage] proc above for |
| # details. |
| # |
| switch -- [lindex $argv 0] { |
| helper { |
| set R(helper) 1 |
| set R(helper_id) [lindex $argv 1] |
| set argv [list --testdir=testdir$R(helper_id)] |
| } |
| |
| default { |
| set R(helper) 0 |
| set R(helper_id) 0 |
| |
| } |
| } |
| if {$R(helper)==0} { |
| for {set ii 0} {$ii < [llength $argv]} {incr ii} { |
| set a [lindex $argv $ii] |
| set n [string length $a] |
| |
| if {[string range $a 0 0]=="-"} { |
| if {($n>2 && [string match "$a*" --jobs]) || $a=="-j"} { |
| incr ii |
| set R(nJob) [lindex $argv $ii] |
| } else { |
| usage |
| } |
| } else { |
| lappend R(patternlist) [string map {% *} $a] |
| } |
| } |
| |
| set argv [list] |
| } |
| source $testdir/permutations.test |
| |
| #------------------------------------------------------------------------- |
| # Return a list of tests to run. Each element of the list is itself a |
| # list of two elements - the name of a permuations.test configuration |
| # followed by the full path to a test script. i.e.: |
| # |
| # {CONFIG FILENAME} {CONFIG FILENAME} ... |
| # |
| proc testset_patternlist {patternlist} { |
| |
| set first [lindex $patternlist 0] |
| if {$first=="all"} { set first "full" } |
| |
| if {$first=="release"} { |
| |
| # The following mirrors the set of test suites invoked by "all.test". |
| # |
| set clist { |
| full |
| no_optimization memsubsys1 memsubsys2 singlethread |
| multithread onefile utf16 exclusive persistent_journal |
| persistent_journal_error no_journal no_journal_error |
| autovacuum_ioerr no_mutex_try fullmutex journaltest |
| inmemory_journal pcache0 pcache10 pcache50 pcache90 |
| pcache100 prepare mmap |
| } |
| ifcapable rbu { lappend clist rbu } |
| if {$::tcl_platform(platform)=="unix"} { |
| ifcapable !default_autovacuum { |
| lappend clist autovacuum_crash |
| } |
| } |
| set patternlist [lrange $patternlist 1 end] |
| |
| } elseif {[info exists ::testspec($first)]} { |
| set clist $first |
| set patternlist [lrange $patternlist 1 end] |
| } elseif { [llength $patternlist]==0 } { |
| set clist veryquick |
| } else { |
| set clist full |
| } |
| |
| set testset [list] |
| |
| foreach config $clist { |
| catch { array unset O } |
| array set O $::testspec($config) |
| foreach f $O(-files) { |
| if {[file pathtype $f]!="absolute"} { |
| set f [file join $::testdir $f] |
| } |
| lappend testset [list $config [file normalize $f]] |
| } |
| } |
| |
| if {[llength $patternlist]>0} { |
| foreach t $testset { |
| set tail [file tail [lindex $t 1]] |
| foreach p $patternlist { |
| if {[string match $p $tail]} { |
| lappend ret $t |
| break; |
| } |
| } |
| } |
| } else { |
| set ret $testset |
| } |
| |
| set ret |
| } |
| #-------------------------------------------------------------------------- |
| |
| |
| proc r_write_db {tcl} { |
| global R |
| |
| sqlite3_test_control_pending_byte 0x010000 |
| sqlite3 db $R(dbname) |
| db timeout $R(timeout) |
| db eval { BEGIN EXCLUSIVE } |
| |
| uplevel $tcl |
| |
| db eval { COMMIT } |
| db close |
| } |
| |
| proc make_new_testset {} { |
| global R |
| |
| set tests [testset_patternlist $R(patternlist)] |
| r_write_db { |
| db eval $R(schema) |
| foreach t $tests { |
| foreach {c s} $t {} |
| set slow 0 |
| |
| set fd [open $s] |
| for {set ii 0} {$ii<100 && ![eof $fd]} {incr ii} { |
| set line [gets $fd] |
| if {[string match -nocase *testrunner:* $line]} { |
| regexp -nocase {.*testrunner:(.*)} $line -> properties |
| foreach p $properties { |
| if {$p=="slow"} { set slow 1 } |
| } |
| } |
| } |
| close $fd |
| |
| db eval { |
| INSERT INTO script(config, filename, slow, state) |
| VALUES ($c, $s, $slow, 'ready') |
| } |
| } |
| } |
| } |
| |
| # Find the next job in the database and mark it as 'running'. Then return |
| # a list consisting of the |
| # |
| # CONFIG FILENAME |
| # |
| # pair for the test. |
| # |
| proc get_next_test {} { |
| global R |
| set myid $R(helper_id) |
| |
| r_write_db { |
| set f "" |
| set c "" |
| db eval { |
| SELECT config, filename FROM script WHERE state='ready' |
| ORDER BY |
| (slow * (($myid+1) % 2)) DESC, |
| config!='full', |
| config, |
| filename |
| LIMIT 1 |
| } { |
| set c $config |
| set f $filename |
| } |
| if {$f!=""} { |
| db eval { |
| UPDATE script SET state='running', testfixtureid=$myid |
| WHERE (config, filename) = ($c, $f) |
| } |
| } |
| } |
| |
| if {$f==""} { return "" } |
| list $c $f |
| } |
| |
| proc r_testname {config filename} { |
| set name [file tail $filename] |
| if {$config!="" && $config!="full" && $config!="veryquick"} { |
| set name "$config-$name" |
| } |
| return $name |
| } |
| |
| proc r_set_test_result {config filename ms nerr ntest output} { |
| global R |
| |
| set f [r_testname $config $filename] |
| if {$nerr==0} { |
| set msg "$f... Ok" |
| } else { |
| set msg "$f... FAILED - $nerr errors of $ntest tests" |
| } |
| append msg " (${ms}ms)" |
| if {$R(helper)} { |
| append msg " (helper $R(helper_id))" |
| } |
| |
| sqlite3_shutdown |
| set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1] |
| set nByte [sqlite3_memory_used] |
| if {($nByte>0 || $nMalloc>0) && $R(leaker)==""} { |
| set R(leaker) $f |
| } |
| |
| r_write_db { |
| db eval { |
| UPDATE script |
| SET state='done', output=$output, nerr=$nerr, ntest=$ntest, time=$ms |
| WHERE (config, filename)=($config, $filename); |
| |
| INSERT INTO msg(msg) VALUES ($msg); |
| } |
| } |
| } |
| |
| set R(iNextMsg) 1 |
| proc r_get_messages {{db ""}} { |
| global R |
| |
| sqlite3_test_control_pending_byte 0x010000 |
| |
| if {$db==""} { |
| sqlite3 rgmhandle $R(dbname) |
| set dbhandle rgmhandle |
| $dbhandle timeout $R(timeout) |
| } else { |
| set dbhandle $db |
| } |
| |
| $dbhandle transaction { |
| set next $R(iNextMsg) |
| set ret [$dbhandle eval {SELECT msg FROM msg WHERE id>=$next}] |
| set R(iNextMsg) [$dbhandle one {SELECT COALESCE(max(id), 0)+1 FROM msg}] |
| } |
| |
| if {$db==""} { |
| rgmhandle close |
| } |
| |
| set ret |
| } |
| |
| # This is called after all tests have been run to write the leaked memory |
| # report into the malloc table of testrunner.db. |
| # |
| proc r_memory_report {} { |
| global R |
| |
| sqlite3_shutdown |
| |
| set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1] |
| set nByte [sqlite3_memory_used] |
| set id $R(helper_id) |
| set leaker $R(leaker) |
| |
| r_write_db { |
| db eval { |
| INSERT INTO malloc(id, nMalloc, nByte, leaker) |
| VALUES($id, $nMalloc, $nByte, $leaker) |
| } |
| } |
| } |
| |
| |
| #-------------------------------------------------------------------------- |
| # |
| set ::R_INSTALL_PUTS_WRAPPER { |
| proc puts_sts_wrapper {args} { |
| set n [llength $args] |
| if {$n==1 || ($n==2 && [string first [lindex $args 0] -nonewline]==0)} { |
| uplevel puts_into_caller $args |
| } else { |
| # A channel was explicitly specified. |
| uplevel puts_sts_original $args |
| } |
| } |
| rename puts puts_sts_original |
| proc puts {args} { uplevel puts_sts_wrapper $args } |
| } |
| |
| proc r_install_puts_wrapper {} $::R_INSTALL_PUTS_WRAPPER |
| proc r_uninstall_puts_wrapper {} { |
| rename puts "" |
| rename puts_sts_original puts |
| } |
| |
| proc slave_test_script {script} { |
| |
| # Create the interpreter used to run the test script. |
| interp create tinterp |
| |
| # Populate some global variables that tester.tcl expects to see. |
| foreach {var value} [list \ |
| ::argv0 $::argv0 \ |
| ::argv {} \ |
| ::SLAVE 1 \ |
| ] { |
| interp eval tinterp [list set $var $value] |
| } |
| |
| # The alias used to access the global test counters. |
| tinterp alias set_test_counter set_test_counter |
| |
| # Set up an empty ::cmdlinearg array in the slave. |
| interp eval tinterp [list array set ::cmdlinearg [array get ::cmdlinearg]] |
| |
| # Set up the ::G array in the slave. |
| interp eval tinterp [list array set ::G [array get ::G]] |
| interp eval tinterp [list set ::G(runner.tcl) 1] |
| |
| interp eval tinterp $::R_INSTALL_PUTS_WRAPPER |
| tinterp alias puts_into_caller puts_into_caller |
| |
| # Load the various test interfaces implemented in C. |
| load_testfixture_extensions tinterp |
| |
| # Run the test script. |
| set rc [catch { interp eval tinterp $script } msg opt] |
| if {$rc} { |
| puts_into_caller $msg |
| puts_into_caller [dict get $opt -errorinfo] |
| incr ::TC(errors) |
| } |
| |
| # Check if the interpreter call [run_thread_tests] |
| if { [interp eval tinterp {info exists ::run_thread_tests_called}] } { |
| set ::run_thread_tests_called 1 |
| } |
| |
| # Delete the interpreter used to run the test script. |
| interp delete tinterp |
| } |
| |
| proc slave_test_file {zFile} { |
| set tail [file tail $zFile] |
| |
| # Remember the value of the shared-cache setting. So that it is possible |
| # to check afterwards that it was not modified by the test script. |
| # |
| ifcapable shared_cache { set scs [sqlite3_enable_shared_cache] } |
| |
| # Run the test script in a slave interpreter. |
| # |
| unset -nocomplain ::run_thread_tests_called |
| reset_prng_state |
| set ::sqlite_open_file_count 0 |
| set time [time { slave_test_script [list source $zFile] }] |
| set ms [expr [lindex $time 0] / 1000] |
| |
| r_install_puts_wrapper |
| |
| # Test that all files opened by the test script were closed. Omit this |
| # if the test script has "thread" in its name. The open file counter |
| # is not thread-safe. |
| # |
| if {[info exists ::run_thread_tests_called]==0} { |
| do_test ${tail}-closeallfiles { expr {$::sqlite_open_file_count>0} } {0} |
| } |
| set ::sqlite_open_file_count 0 |
| |
| # Test that the global "shared-cache" setting was not altered by |
| # the test script. |
| # |
| ifcapable shared_cache { |
| set res [expr {[sqlite3_enable_shared_cache] == $scs}] |
| do_test ${tail}-sharedcachesetting [list set {} $res] 1 |
| } |
| |
| # Add some info to the output. |
| # |
| output2 "Time: $tail $ms ms" |
| show_memstats |
| |
| r_uninstall_puts_wrapper |
| return $ms |
| } |
| |
| proc puts_into_caller {args} { |
| global R |
| if {[llength $args]==1} { |
| append R(output) [lindex $args 0] |
| append R(output) "\n" |
| } else { |
| append R(output) [lindex $args 1] |
| } |
| } |
| |
| #------------------------------------------------------------------------- |
| # |
| proc r_final_report {} { |
| global R |
| |
| sqlite3_test_control_pending_byte 0x010000 |
| sqlite3 db $R(dbname) |
| |
| db timeout $R(timeout) |
| |
| set errcode 0 |
| |
| # Create the text log file. This is just the concatenation of the |
| # 'output' column of the database for every script that was run. |
| set fd [open $R(logname) w] |
| db eval {SELECT output FROM script ORDER BY config!='full',config,filename} { |
| puts $fd $output |
| } |
| close $fd |
| |
| # Check if any scripts reported errors. If so, print one line noting |
| # how many errors, and another identifying the scripts in which they |
| # occured. Or, if no errors occurred, print out "no errors at all!". |
| sqlite3 db $R(dbname) |
| db timeout $R(timeout) |
| db eval { SELECT sum(nerr) AS nerr, sum(ntest) AS ntest FROM script } { } |
| puts "$nerr errors from $ntest tests." |
| if {$nerr>0} { |
| db eval { SELECT config, filename FROM script WHERE nerr>0 } { |
| lappend errlist [r_testname $config $filename] |
| } |
| puts "Errors in: $errlist" |
| set errcode 1 |
| } |
| |
| # Check if any scripts were not run or did not finish. Print out a |
| # line identifying them if there are any. |
| set errlist [list] |
| db eval { SELECT config, filename FROM script WHERE state!='done' } { |
| lappend errlist [r_testname $config $filename] |
| } |
| if {$errlist!=[list]} { |
| puts "Tests DID NOT FINISH (crashed?): $errlist" |
| set errcode 1 |
| } |
| |
| set bLeak 0 |
| db eval { |
| SELECT id, nmalloc, nbyte, leaker FROM malloc |
| WHERE nmalloc>0 OR nbyte>0 |
| } { |
| if {$id==0} { |
| set line "This process " |
| } else { |
| set line "Helper $id " |
| } |
| append line "leaked $nbyte byte in $nmalloc allocations" |
| if {$leaker!=""} { append line " (perhaps in [file tail $leaker])" } |
| puts $line |
| set bLeak 1 |
| } |
| if {$bLeak==0} { |
| puts "No leaks - all allocations freed." |
| } |
| |
| db close |
| |
| puts "Test database is $R(dbname)" |
| puts "Test log file is $R(logname)" |
| if {$errcode} { |
| puts "This test has FAILED." |
| } |
| return $errcode |
| } |
| |
| |
| if {$R(helper)==0} { |
| make_new_testset |
| } |
| |
| set R(nHelperRunning) 0 |
| if {$R(helper)==0 && $R(nJob)>1} { |
| cd $cmdlinearg(TESTFIXTURE_HOME) |
| for {set ii 1} {$ii <= $R(nJob)} {incr ii} { |
| set cmd "[info nameofexec] $R(info_script) helper $ii 2>@1" |
| puts "Launching helper $ii ($cmd)" |
| set chan [open "|$cmd" r] |
| fconfigure $chan -blocking false |
| fileevent $chan readable [list r_helper_readable $ii $chan] |
| incr R(nHelperRunning) |
| } |
| cd $cmdlinearg(testdir) |
| } |
| |
| proc r_helper_readable {id chan} { |
| set data [gets $chan] |
| if {$data!=""} { puts "helper $id:$data" } |
| if {[eof $chan]} { |
| puts "helper $id is finished" |
| incr ::R(nHelperRunning) -1 |
| close $chan |
| } |
| } |
| |
| if {$R(nHelperRunning)==0} { |
| while { ""!=[set t [get_next_test]] } { |
| set R(output) "" |
| set TC(count) 0 |
| set TC(errors) 0 |
| |
| foreach {config filename} $t {} |
| |
| array set O $::testspec($config) |
| set ::G(perm:name) $config |
| set ::G(perm:prefix) $O(-prefix) |
| set ::G(isquick) 1 |
| set ::G(perm:dbconfig) $O(-dbconfig) |
| set ::G(perm:presql) $O(-presql) |
| |
| eval $O(-initialize) |
| set ms [slave_test_file $filename] |
| eval $O(-shutdown) |
| |
| unset -nocomplain ::G(perm:sqlite3_args) |
| unset ::G(perm:name) |
| unset ::G(perm:prefix) |
| unset ::G(perm:dbconfig) |
| unset ::G(perm:presql) |
| |
| r_set_test_result $config $filename $ms $TC(errors) $TC(count) $R(output) |
| |
| if {$R(helper)==0} { |
| foreach msg [r_get_messages] { puts $msg } |
| } |
| } |
| |
| # Tests are finished - write a record into testrunner.db describing |
| # any memory leaks. |
| r_memory_report |
| |
| } else { |
| set TTT 0 |
| sqlite3 db $R(dbname) |
| db timeout $R(timeout) |
| while {$R(nHelperRunning)>0} { |
| after 250 { incr TTT } |
| vwait TTT |
| foreach msg [r_get_messages db] { puts $msg } |
| } |
| db close |
| } |
| |
| set errcode 0 |
| if {$R(helper)==0} { |
| set errcode [r_final_report] |
| } |
| |
| exit $errcode |
| |