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