#------------------------------------------------------------------------- # Usage: # proc usage {} { set a0 testrunner.tcl puts stderr "Usage: $a0 ?SWITCHES? ?all|veryquick? ?PATTERNS?" puts stderr "" puts stderr "where SWITCHES are:" puts stderr " --jobs NUMBER-OF-JOBS" puts stderr "" puts stderr "Examples:" puts stderr " $a0 # Run veryquick.test tests" puts stderr " $a0 all # Run all tests" puts stderr " $a0 veryquick rtree% # Run all test scripts from veryquick.test that match 'rtree%'" puts stderr " $a0 alter% fts5% # Run all test scripts that match 'alter%' or '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( filename TEXT PRIMARY KEY, -- full path to test script 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 ); 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] source $testdir/testset.tcl # 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/tester.tcl db close proc r_write_db {tcl} { global R 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 scripts [testset_patternlist $R(patternlist)] r_write_db { db eval $R(schema) foreach s $scripts { db eval { INSERT INTO script(filename, state) VALUES ($s, 'ready') } } } } proc get_next_test {} { global R set myid $R(helper_id) r_write_db { set f [db one { SELECT filename FROM script WHERE state='ready' ORDER BY 1 LIMIT 1 }] if {$f!=""} { db eval { UPDATE script SET state='running', testfixtureid=$myid WHERE filename=$f } } } return $f } proc r_set_test_result {filename ms nerr ntest output} { global R set f [file tail $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) $filename } r_write_db { db eval { UPDATE script SET state='done', output=$output, nerr=$nerr, ntest=$ntest, time=$ms WHERE filename=$filename; INSERT INTO msg(msg) VALUES ($msg); } } } set R(iNextMsg) 1 proc r_get_messages {{db ""}} { global R 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 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 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 filename FROM script WHERE nerr>0 } { lappend errlist [file tail $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 filename FROM script WHERE state!='done' } { lappend errlist [file tail $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:[gets $chan]" } if {[eof $chan]} { puts "helper $id is finished" incr ::R(nHelperRunning) -1 close $chan } } if {$R(nHelperRunning)==0} { while { ""!=[set f [get_next_test]] } { set R(output) "" set TC(count) 0 set TC(errors) 0 set ms [slave_test_file $f] r_set_test_result $f $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