1 2 3#------------------------------------------------------------------------- 4# Usage: 5# 6proc usage {} { 7 set a0 testrunner.tcl 8 9 puts stderr "Usage: $a0 ?SWITCHES? ?all|veryquick? ?PATTERNS?" 10 puts stderr "" 11 puts stderr "where SWITCHES are:" 12 puts stderr " --jobs NUMBER-OF-JOBS" 13 puts stderr "" 14 puts stderr "Examples:" 15 puts stderr " $a0 # Run veryquick.test tests" 16 puts stderr " $a0 all # Run all tests" 17 puts stderr " $a0 veryquick rtree% # Run all test scripts from veryquick.test that match 'rtree%'" 18 puts stderr " $a0 alter% fts5% # Run all test scripts that match 'alter%' or 'rtree%'" 19 20 exit 1 21} 22#------------------------------------------------------------------------- 23 24#------------------------------------------------------------------------- 25# The database schema used by the testrunner.db database. 26# 27set R(schema) { 28 DROP TABLE IF EXISTS script; 29 DROP TABLE IF EXISTS msg; 30 DROP TABLE IF EXISTS malloc; 31 32 CREATE TABLE script( 33 filename TEXT PRIMARY KEY, -- full path to test script 34 state TEXT CHECK( state IN ('ready', 'running', 'done') ), 35 testfixtureid, -- Id of process that ran script 36 time INTEGER, -- Time in ms 37 nerr INTEGER, -- if 'done', the number of errors 38 ntest INTEGER, -- if 'done', the number of tests 39 output TEXT -- full output of test script 40 ); 41 42 CREATE TABLE malloc( 43 id INTEGER PRIMARY KEY, 44 nmalloc INTEGER, 45 nbyte INTEGER, 46 leaker TEXT 47 ); 48 49 CREATE TABLE msg( 50 id INTEGER PRIMARY KEY, 51 msg TEXT 52 ); 53} 54#------------------------------------------------------------------------- 55 56#------------------------------------------------------------------------- 57# Try to estimate a the number of processes to use. 58# 59# Command [guess_number_of_cores] attempts to glean the number of logical 60# cores. Command [default_njob] returns the default value for the --jobs 61# switch. 62# 63proc guess_number_of_cores {} { 64 set ret 4 65 66 if {$::tcl_platform(os)=="Darwin"} { 67 set cmd "sysctl -n hw.logicalcpu" 68 } else { 69 set cmd "nproc" 70 } 71 catch { 72 set fd [open "|$cmd" r] 73 set ret [gets $fd] 74 close $fd 75 set ret [expr $ret] 76 } 77 return $ret 78} 79 80proc default_njob {} { 81 set nCore [guess_number_of_cores] 82 set nHelper [expr int($nCore*0.75)] 83 expr $nHelper>0 ? $nHelper : 1 84} 85#------------------------------------------------------------------------- 86 87 88set R(dbname) [file normalize testrunner.db] 89set R(logname) [file normalize testrunner.log] 90set R(info_script) [file normalize [info script]] 91set R(timeout) 10000 ;# Default busy-timeout for testrunner. 92set R(nJob) [default_njob] ;# Default number of helper processes 93set R(leaker) "" ;# Name of first script to leak memory 94 95set R(patternlist) [list] 96 97set testdir [file dirname $argv0] 98source $testdir/testset.tcl 99 100# Parse the command line options. There are two ways to invoke this 101# script - to create a helper or coordinator process. If there are 102# no helper processes, the coordinator runs test scripts. 103# 104# To create a helper process: 105# 106# testrunner.tcl helper ID 107# 108# where ID is an integer greater than 0. The process will create and 109# run tests in the "testdir$ID" directory. Helper processes are only 110# created by coordinators - there is no need for a user to create 111# helper processes manually. 112# 113# If the first argument is anything other than "helper", then a coordinator 114# process is started. See the implementation of the [usage] proc above for 115# details. 116# 117switch -- [lindex $argv 0] { 118 helper { 119 set R(helper) 1 120 set R(helper_id) [lindex $argv 1] 121 set argv [list --testdir=testdir$R(helper_id)] 122 } 123 124 default { 125 set R(helper) 0 126 set R(helper_id) 0 127 128 } 129} 130if {$R(helper)==0} { 131 for {set ii 0} {$ii < [llength $argv]} {incr ii} { 132 set a [lindex $argv $ii] 133 set n [string length $a] 134 135 if {[string range $a 0 0]=="-"} { 136 if {($n>2 && [string match "$a*" --jobs]) || $a=="-j"} { 137 incr ii 138 set R(nJob) [lindex $argv $ii] 139 } else { 140 usage 141 } 142 } else { 143 lappend R(patternlist) [string map {% * _ .} $a] 144 } 145 } 146 147 set argv [list] 148} 149 150source $testdir/tester.tcl 151db close 152 153 154proc r_write_db {tcl} { 155 global R 156 sqlite3 db $R(dbname) 157 db timeout $R(timeout) 158 db eval { BEGIN EXCLUSIVE } 159 160 uplevel $tcl 161 162 db eval { COMMIT } 163 db close 164} 165 166proc make_new_testset {} { 167 global R 168 169 set scripts [testset_patternlist $R(patternlist)] 170 r_write_db { 171 db eval $R(schema) 172 foreach s $scripts { 173 db eval { INSERT INTO script(filename, state) VALUES ($s, 'ready') } 174 } 175 } 176} 177 178proc get_next_test {} { 179 global R 180 set myid $R(helper_id) 181 182 r_write_db { 183 set f [db one { 184 SELECT filename FROM script WHERE state='ready' ORDER BY 1 LIMIT 1 185 }] 186 if {$f!=""} { 187 db eval { 188 UPDATE script SET state='running', testfixtureid=$myid WHERE filename=$f 189 } 190 } 191 } 192 193 return $f 194} 195 196proc r_set_test_result {filename ms nerr ntest output} { 197 global R 198 199 set f [file tail $filename] 200 if {$nerr==0} { 201 set msg "$f... Ok" 202 } else { 203 set msg "$f... FAILED - $nerr errors of $ntest tests" 204 } 205 append msg " (${ms}ms)" 206 if {$R(helper)} { 207 append msg " (helper $R(helper_id))" 208 } 209 210 sqlite3_shutdown 211 set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1] 212 set nByte [sqlite3_memory_used] 213 if {($nByte>0 || $nMalloc>0) && $R(leaker)==""} { 214 set R(leaker) $filename 215 } 216 217 r_write_db { 218 db eval { 219 UPDATE script 220 SET state='done', output=$output, nerr=$nerr, ntest=$ntest, time=$ms 221 WHERE filename=$filename; 222 223 INSERT INTO msg(msg) VALUES ($msg); 224 } 225 } 226} 227 228set R(iNextMsg) 1 229proc r_get_messages {{db ""}} { 230 global R 231 232 if {$db==""} { 233 sqlite3 rgmhandle $R(dbname) 234 set dbhandle rgmhandle 235 $dbhandle timeout $R(timeout) 236 } else { 237 set dbhandle $db 238 } 239 240 $dbhandle transaction { 241 set next $R(iNextMsg) 242 set ret [$dbhandle eval {SELECT msg FROM msg WHERE id>=$next}] 243 set R(iNextMsg) [$dbhandle one {SELECT COALESCE(max(id), 0)+1 FROM msg}] 244 } 245 246 if {$db==""} { 247 rgmhandle close 248 } 249 250 set ret 251} 252 253# This is called after all tests have been run to write the leaked memory 254# report into the malloc table of testrunner.db. 255# 256proc r_memory_report {} { 257 global R 258 259 sqlite3_shutdown 260 261 set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1] 262 set nByte [sqlite3_memory_used] 263 set id $R(helper_id) 264 set leaker $R(leaker) 265 266 r_write_db { 267 db eval { 268 INSERT INTO malloc(id, nMalloc, nByte, leaker) 269 VALUES($id, $nMalloc, $nByte, $leaker) 270 } 271 } 272} 273 274 275#-------------------------------------------------------------------------- 276# 277set ::R_INSTALL_PUTS_WRAPPER { 278 proc puts_sts_wrapper {args} { 279 set n [llength $args] 280 if {$n==1 || ($n==2 && [string first [lindex $args 0] -nonewline]==0)} { 281 uplevel puts_into_caller $args 282 } else { 283 # A channel was explicitly specified. 284 uplevel puts_sts_original $args 285 } 286 } 287 rename puts puts_sts_original 288 proc puts {args} { uplevel puts_sts_wrapper $args } 289} 290 291proc r_install_puts_wrapper {} $::R_INSTALL_PUTS_WRAPPER 292proc r_uninstall_puts_wrapper {} { 293 rename puts "" 294 rename puts_sts_original puts 295} 296 297proc slave_test_script {script} { 298 299 # Create the interpreter used to run the test script. 300 interp create tinterp 301 302 # Populate some global variables that tester.tcl expects to see. 303 foreach {var value} [list \ 304 ::argv0 $::argv0 \ 305 ::argv {} \ 306 ::SLAVE 1 \ 307 ] { 308 interp eval tinterp [list set $var $value] 309 } 310 311 # The alias used to access the global test counters. 312 tinterp alias set_test_counter set_test_counter 313 314 # Set up an empty ::cmdlinearg array in the slave. 315 interp eval tinterp [list array set ::cmdlinearg [array get ::cmdlinearg]] 316 317 # Set up the ::G array in the slave. 318 interp eval tinterp [list array set ::G [array get ::G]] 319 interp eval tinterp [list set ::G(runner.tcl) 1] 320 321 interp eval tinterp $::R_INSTALL_PUTS_WRAPPER 322 tinterp alias puts_into_caller puts_into_caller 323 324 # Load the various test interfaces implemented in C. 325 load_testfixture_extensions tinterp 326 327 # Run the test script. 328 set rc [catch { interp eval tinterp $script } msg opt] 329 if {$rc} { 330 puts_into_caller $msg 331 puts_into_caller [dict get $opt -errorinfo] 332 incr ::TC(errors) 333 } 334 335 # Check if the interpreter call [run_thread_tests] 336 if { [interp eval tinterp {info exists ::run_thread_tests_called}] } { 337 set ::run_thread_tests_called 1 338 } 339 340 # Delete the interpreter used to run the test script. 341 interp delete tinterp 342} 343 344proc slave_test_file {zFile} { 345 set tail [file tail $zFile] 346 347 # Remember the value of the shared-cache setting. So that it is possible 348 # to check afterwards that it was not modified by the test script. 349 # 350 ifcapable shared_cache { set scs [sqlite3_enable_shared_cache] } 351 352 # Run the test script in a slave interpreter. 353 # 354 unset -nocomplain ::run_thread_tests_called 355 reset_prng_state 356 set ::sqlite_open_file_count 0 357 set time [time { slave_test_script [list source $zFile] }] 358 set ms [expr [lindex $time 0] / 1000] 359 360 r_install_puts_wrapper 361 362 # Test that all files opened by the test script were closed. Omit this 363 # if the test script has "thread" in its name. The open file counter 364 # is not thread-safe. 365 # 366 if {[info exists ::run_thread_tests_called]==0} { 367 do_test ${tail}-closeallfiles { expr {$::sqlite_open_file_count>0} } {0} 368 } 369 set ::sqlite_open_file_count 0 370 371 # Test that the global "shared-cache" setting was not altered by 372 # the test script. 373 # 374 ifcapable shared_cache { 375 set res [expr {[sqlite3_enable_shared_cache] == $scs}] 376 do_test ${tail}-sharedcachesetting [list set {} $res] 1 377 } 378 379 # Add some info to the output. 380 # 381 output2 "Time: $tail $ms ms" 382 show_memstats 383 384 r_uninstall_puts_wrapper 385 return $ms 386} 387 388proc puts_into_caller {args} { 389 global R 390 if {[llength $args]==1} { 391 append R(output) [lindex $args 0] 392 append R(output) "\n" 393 } else { 394 append R(output) [lindex $args 1] 395 } 396} 397 398#------------------------------------------------------------------------- 399# 400proc r_final_report {} { 401 global R 402 403 sqlite3 db $R(dbname) 404 db timeout $R(timeout) 405 406 set errcode 0 407 408 # Create the text log file. This is just the concatenation of the 409 # 'output' column of the database for every script that was run. 410 set fd [open $R(logname) w] 411 db eval {SELECT output FROM script ORDER BY filename} { 412 puts $fd $output 413 } 414 close $fd 415 416 # Check if any scripts reported errors. If so, print one line noting 417 # how many errors, and another identifying the scripts in which they 418 # occured. Or, if no errors occurred, print out "no errors at all!". 419 sqlite3 db $R(dbname) 420 db timeout $R(timeout) 421 db eval { SELECT sum(nerr) AS nerr, sum(ntest) AS ntest FROM script } { } 422 puts "$nerr errors from $ntest tests." 423 if {$nerr>0} { 424 db eval { SELECT filename FROM script WHERE nerr>0 } { 425 lappend errlist [file tail $filename] 426 } 427 puts "Errors in: $errlist" 428 set errcode 1 429 } 430 431 # Check if any scripts were not run or did not finish. Print out a 432 # line identifying them if there are any. 433 set errlist [list] 434 db eval { SELECT filename FROM script WHERE state!='done' } { 435 lappend errlist [file tail $filename] 436 } 437 if {$errlist!=[list]} { 438 puts "Tests DID NOT FINISH (crashed?): $errlist" 439 set errcode 1 440 } 441 442 set bLeak 0 443 db eval { 444 SELECT id, nmalloc, nbyte, leaker FROM malloc 445 WHERE nmalloc>0 OR nbyte>0 446 } { 447 if {$id==0} { 448 set line "This process " 449 } else { 450 set line "Helper $id " 451 } 452 append line "leaked $nbyte byte in $nmalloc allocations" 453 if {$leaker!=""} { append line " (perhaps in [file tail $leaker])" } 454 puts $line 455 set bLeak 1 456 } 457 if {$bLeak==0} { 458 puts "No leaks - all allocations freed." 459 } 460 461 db close 462 463 puts "Test database is $R(dbname)" 464 puts "Test log file is $R(logname)" 465 if {$errcode} { 466 puts "This test has FAILED." 467 } 468 return $errcode 469} 470 471 472if {$R(helper)==0} { 473 make_new_testset 474} 475 476set R(nHelperRunning) 0 477if {$R(helper)==0 && $R(nJob)>1} { 478 cd $cmdlinearg(TESTFIXTURE_HOME) 479 for {set ii 1} {$ii <= $R(nJob)} {incr ii} { 480 set cmd "[info nameofexec] $R(info_script) helper $ii 2>@1" 481 puts "Launching helper $ii ($cmd)" 482 set chan [open "|$cmd" r] 483 fconfigure $chan -blocking false 484 fileevent $chan readable [list r_helper_readable $ii $chan] 485 incr R(nHelperRunning) 486 } 487 cd $cmdlinearg(testdir) 488} 489 490proc r_helper_readable {id chan} { 491 set data [gets $chan] 492 if {$data!=""} { puts "helper $id:[gets $chan]" } 493 if {[eof $chan]} { 494 puts "helper $id is finished" 495 incr ::R(nHelperRunning) -1 496 close $chan 497 } 498} 499 500if {$R(nHelperRunning)==0} { 501 while { ""!=[set f [get_next_test]] } { 502 set R(output) "" 503 set TC(count) 0 504 set TC(errors) 0 505 set ms [slave_test_file $f] 506 507 r_set_test_result $f $ms $TC(errors) $TC(count) $R(output) 508 509 if {$R(helper)==0} { 510 foreach msg [r_get_messages] { puts $msg } 511 } 512 } 513 514 # Tests are finished - write a record into testrunner.db describing 515 # any memory leaks. 516 r_memory_report 517 518} else { 519 set TTT 0 520 sqlite3 db $R(dbname) 521 db timeout $R(timeout) 522 while {$R(nHelperRunning)>0} { 523 after 250 { incr TTT } 524 vwait TTT 525 foreach msg [r_get_messages db] { puts $msg } 526 } 527 db close 528} 529 530set errcode 0 531if {$R(helper)==0} { 532 set errcode [r_final_report] 533} 534 535exit $errcode 536 537