xref: /sqlite-3.40.0/test/testrunner.tcl (revision b1c034b2)
1
2#-------------------------------------------------------------------------
3# Usage:
4#
5proc usage {} {
6  set a0 testrunner.tcl
7
8  set ::argv [list]
9  uplevel [list source $::testdir/permutations.test]
10
11  puts stderr "Usage: $a0 ?SWITCHES? ?PERMUTATION? ?PATTERNS?"
12  puts stderr ""
13  puts stderr "where SWITCHES are:"
14  puts stderr "    --jobs NUMBER-OF-JOBS"
15  puts stderr ""
16  puts stderr "available PERMUTATION values are:"
17  set ii 0
18  foreach name [lsort [array names ::testspec]] {
19    if {($ii % 3)==0} { puts -nonewline stderr "  " }
20    puts -nonewline stderr [format "% -22s" $name]
21    if {($ii % 3)==2} { puts stderr "" }
22    incr ii
23  }
24  puts stderr ""
25  puts stderr ""
26  puts stderr "Examples:"
27  puts stderr " 1) Run the veryquick tests:"
28  puts stderr "      $a0"
29  puts stderr " 2) Run all test scripts in the source tree:"
30  puts stderr "      $a0 full"
31  puts stderr " 2) Run the 'memsubsys1' permutation:"
32  puts stderr "      $a0 memsubsys1"
33  puts stderr " 3) Run all permutations usually run by \[make fulltest\]"
34  puts stderr "      $a0 release"
35  puts stderr " 4) Run all scripts that match the pattern 'select%':"
36  puts stderr "      $a0 select%"
37  puts stderr "      $a0 all select%"
38  puts stderr "      $a0 full select%"
39  puts stderr " 5) Run all scripts that are part of the veryquick permutation and match the pattern 'select%':"
40  puts stderr "      $a0 veryquick select%"
41  puts stderr " 6) Run the 'memsubsys1' permutation, but just those scripts that match 'window%':"
42  puts stderr "      $a0 memsubsys1 window%"
43  puts stderr " 7) Run all the permutations, but only the scripts that match either 'fts5%' or 'rtree%':"
44  puts stderr "      $a0 release fts5% rtree%"
45
46  exit 1
47}
48#-------------------------------------------------------------------------
49
50#-------------------------------------------------------------------------
51# The database schema used by the testrunner.db database.
52#
53set R(schema) {
54  DROP TABLE IF EXISTS script;
55  DROP TABLE IF EXISTS msg;
56  DROP TABLE IF EXISTS malloc;
57
58  CREATE TABLE script(
59    config TEXT,
60    filename TEXT,                -- full path to test script
61    state TEXT CHECK( state IN ('ready', 'running', 'done') ),
62    testfixtureid,                -- Id of process that ran script
63    time INTEGER,                 -- Time in ms
64    nerr INTEGER,                 -- if 'done', the number of errors
65    ntest INTEGER,                -- if 'done', the number of tests
66    output TEXT,                  -- full output of test script
67    PRIMARY KEY(config, filename)
68  );
69
70  CREATE TABLE malloc(
71    id INTEGER PRIMARY KEY,
72    nmalloc INTEGER,
73    nbyte INTEGER,
74    leaker TEXT
75  );
76
77  CREATE TABLE msg(
78    id INTEGER PRIMARY KEY,
79    msg TEXT
80  );
81}
82#-------------------------------------------------------------------------
83
84#-------------------------------------------------------------------------
85# Try to estimate a the number of processes to use.
86#
87# Command [guess_number_of_cores] attempts to glean the number of logical
88# cores. Command [default_njob] returns the default value for the --jobs
89# switch.
90#
91proc guess_number_of_cores {} {
92  set ret 4
93
94  if {$::tcl_platform(os)=="Darwin"} {
95    set cmd "sysctl -n hw.logicalcpu"
96  } else {
97    set cmd "nproc"
98  }
99  catch {
100    set fd [open "|$cmd" r]
101    set ret [gets $fd]
102    close $fd
103    set ret [expr $ret]
104  }
105  return $ret
106}
107
108proc default_njob {} {
109  set nCore [guess_number_of_cores]
110  set nHelper [expr int($nCore*0.75)]
111  expr $nHelper>0 ? $nHelper : 1
112}
113#-------------------------------------------------------------------------
114
115
116set R(dbname) [file normalize testrunner.db]
117set R(logname) [file normalize testrunner.log]
118set R(info_script) [file normalize [info script]]
119set R(timeout) 10000              ;# Default busy-timeout for testrunner.
120set R(nJob)    [default_njob]     ;# Default number of helper processes
121set R(leaker)  ""                 ;# Name of first script to leak memory
122
123set R(patternlist) [list]
124
125set testdir [file dirname $argv0]
126
127# Parse the command line options. There are two ways to invoke this
128# script - to create a helper or coordinator process. If there are
129# no helper processes, the coordinator runs test scripts.
130#
131# To create a helper process:
132#
133#    testrunner.tcl helper ID
134#
135# where ID is an integer greater than 0. The process will create and
136# run tests in the "testdir$ID" directory. Helper processes are only
137# created by coordinators - there is no need for a user to create
138# helper processes manually.
139#
140# If the first argument is anything other than "helper", then a coordinator
141# process is started. See the implementation of the [usage] proc above for
142# details.
143#
144switch -- [lindex $argv 0] {
145  helper {
146    set R(helper) 1
147    set R(helper_id) [lindex $argv 1]
148    set argv [list --testdir=testdir$R(helper_id)]
149  }
150
151  default {
152    set R(helper) 0
153    set R(helper_id) 0
154
155  }
156}
157if {$R(helper)==0} {
158  for {set ii 0} {$ii < [llength $argv]} {incr ii} {
159    set a [lindex $argv $ii]
160    set n [string length $a]
161
162    if {[string range $a 0 0]=="-"} {
163      if {($n>2 && [string match "$a*" --jobs]) || $a=="-j"} {
164        incr ii
165          set R(nJob) [lindex $argv $ii]
166      } else {
167        usage
168      }
169    } else {
170      lappend R(patternlist) [string map {% *} $a]
171    }
172  }
173
174  set argv [list]
175}
176source $testdir/permutations.test
177
178#-------------------------------------------------------------------------
179# Return a list of tests to run. Each element of the list is itself a
180# list of two elements - the name of a permuations.test configuration
181# followed by the full path to a test script. i.e.:
182#
183#    {CONFIG FILENAME} {CONFIG FILENAME} ...
184#
185proc testset_patternlist {patternlist} {
186
187  set first [lindex $patternlist 0]
188  if {$first=="all"} { set first "full" }
189
190  if {$first=="release"} {
191
192    # The following mirrors the set of test suites invoked by "all.test".
193    #
194    set clist {
195      full
196      no_optimization memsubsys1 memsubsys2 singlethread
197      multithread onefile utf16 exclusive persistent_journal
198      persistent_journal_error no_journal no_journal_error
199      autovacuum_ioerr no_mutex_try fullmutex journaltest
200      inmemory_journal pcache0 pcache10 pcache50 pcache90
201      pcache100 prepare mmap
202    }
203    ifcapable rbu { lappend clist rbu }
204    if {$::tcl_platform(platform)=="unix"} {
205      ifcapable !default_autovacuum {
206        lappend clist autovacuum_crash
207      }
208    }
209    set patternlist [lrange $patternlist 1 end]
210
211  } elseif {[info exists ::testspec($first)]} {
212    set clist $first
213    set patternlist [lrange $patternlist 1 end]
214  } elseif { [llength $patternlist]==0 } {
215    set clist veryquick
216  } else {
217    set clist full
218  }
219
220  set testset [list]
221
222  foreach config $clist {
223    catch { array unset O }
224    array set O $::testspec($config)
225    foreach f $O(-files) {
226      if {[file pathtype $f]!="absolute"} {
227        set f [file join $::testdir $f]
228      }
229      lappend testset [list $config [file normalize $f]]
230    }
231  }
232
233  if {[llength $patternlist]>0} {
234    foreach t $testset {
235      set tail [file tail [lindex $t 1]]
236      foreach p $patternlist {
237        if {[string match $p $tail]} {
238          lappend ret $t
239          break;
240        }
241      }
242    }
243  } else {
244    set ret $testset
245  }
246
247  set ret
248}
249#--------------------------------------------------------------------------
250
251
252proc r_write_db {tcl} {
253  global R
254
255  sqlite3_test_control_pending_byte 0x010000
256  sqlite3 db $R(dbname)
257  db timeout $R(timeout)
258  db eval { BEGIN EXCLUSIVE }
259
260  uplevel $tcl
261
262  db eval { COMMIT }
263  db close
264}
265
266proc make_new_testset {} {
267  global R
268
269  set tests [testset_patternlist $R(patternlist)]
270  r_write_db {
271    db eval $R(schema)
272    foreach t $tests {
273      foreach {c s} $t {}
274      db eval {
275        INSERT INTO script(config, filename, state) VALUES ($c, $s, 'ready')
276      }
277    }
278  }
279}
280
281# Find the next job in the database and mark it as 'running'. Then return
282# a list consisting of the
283#
284#   CONFIG FILENAME
285#
286# pair for the test.
287#
288proc get_next_test {} {
289  global R
290  set myid $R(helper_id)
291
292  r_write_db {
293    set f ""
294    set c ""
295    db eval {
296      SELECT config, filename FROM script WHERE state='ready'
297      ORDER BY config!='full', config, filename LIMIT 1
298    } {
299      set c $config
300      set f $filename
301    }
302    if {$f!=""} {
303      db eval {
304        UPDATE script SET state='running', testfixtureid=$myid
305        WHERE (config, filename) = ($c, $f)
306      }
307    }
308  }
309
310  if {$f==""} { return "" }
311  list $c $f
312}
313
314proc r_testname {config filename} {
315  set name [file tail $filename]
316  if {$config!="" && $config!="full" && $config!="veryquick"} {
317    set name "$config-$name"
318  }
319  return $name
320}
321
322proc r_set_test_result {config filename ms nerr ntest output} {
323  global R
324
325  set f [r_testname $config $filename]
326  if {$nerr==0} {
327    set msg "$f... Ok"
328  } else {
329    set msg "$f... FAILED - $nerr errors of $ntest tests"
330  }
331  append msg " (${ms}ms)"
332  if {$R(helper)} {
333    append msg " (helper $R(helper_id))"
334  }
335
336  sqlite3_shutdown
337  set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1]
338  set nByte   [sqlite3_memory_used]
339  if {($nByte>0 || $nMalloc>0) && $R(leaker)==""} {
340    set R(leaker) $f
341  }
342
343  r_write_db {
344    db eval {
345      UPDATE script
346        SET state='done', output=$output, nerr=$nerr, ntest=$ntest, time=$ms
347      WHERE (config, filename)=($config, $filename);
348
349      INSERT INTO msg(msg) VALUES ($msg);
350    }
351  }
352}
353
354set R(iNextMsg) 1
355proc r_get_messages {{db ""}} {
356  global R
357
358  sqlite3_test_control_pending_byte 0x010000
359
360  if {$db==""} {
361    sqlite3 rgmhandle $R(dbname)
362    set dbhandle rgmhandle
363    $dbhandle timeout $R(timeout)
364  } else {
365    set dbhandle $db
366  }
367
368  $dbhandle transaction {
369    set next $R(iNextMsg)
370    set ret [$dbhandle eval {SELECT msg FROM msg WHERE id>=$next}]
371    set R(iNextMsg) [$dbhandle one {SELECT COALESCE(max(id), 0)+1 FROM msg}]
372  }
373
374  if {$db==""} {
375    rgmhandle close
376  }
377
378  set ret
379}
380
381# This is called after all tests have been run to write the leaked memory
382# report into the malloc table of testrunner.db.
383#
384proc r_memory_report {} {
385  global R
386
387  sqlite3_shutdown
388
389  set nMalloc [lindex [sqlite3_status SQLITE_STATUS_MALLOC_COUNT 0] 1]
390  set nByte   [sqlite3_memory_used]
391  set id $R(helper_id)
392  set leaker $R(leaker)
393
394  r_write_db {
395    db eval {
396      INSERT INTO malloc(id, nMalloc, nByte, leaker)
397        VALUES($id, $nMalloc, $nByte, $leaker)
398    }
399  }
400}
401
402
403#--------------------------------------------------------------------------
404#
405set ::R_INSTALL_PUTS_WRAPPER {
406  proc puts_sts_wrapper {args} {
407    set n [llength $args]
408    if {$n==1 || ($n==2 && [string first [lindex $args 0] -nonewline]==0)} {
409      uplevel puts_into_caller $args
410    } else {
411      # A channel was explicitly specified.
412      uplevel puts_sts_original $args
413    }
414  }
415  rename puts puts_sts_original
416  proc puts {args} { uplevel puts_sts_wrapper $args }
417}
418
419proc r_install_puts_wrapper {} $::R_INSTALL_PUTS_WRAPPER
420proc r_uninstall_puts_wrapper {} {
421  rename puts ""
422  rename puts_sts_original puts
423}
424
425proc slave_test_script {script} {
426
427  # Create the interpreter used to run the test script.
428  interp create tinterp
429
430  # Populate some global variables that tester.tcl expects to see.
431  foreach {var value} [list              \
432    ::argv0 $::argv0                     \
433    ::argv  {}                           \
434    ::SLAVE 1                            \
435  ] {
436    interp eval tinterp [list set $var $value]
437  }
438
439  # The alias used to access the global test counters.
440  tinterp alias set_test_counter set_test_counter
441
442  # Set up an empty ::cmdlinearg array in the slave.
443  interp eval tinterp [list array set ::cmdlinearg [array get ::cmdlinearg]]
444
445  # Set up the ::G array in the slave.
446  interp eval tinterp [list array set ::G [array get ::G]]
447  interp eval tinterp [list set ::G(runner.tcl) 1]
448
449  interp eval tinterp $::R_INSTALL_PUTS_WRAPPER
450  tinterp alias puts_into_caller puts_into_caller
451
452  # Load the various test interfaces implemented in C.
453  load_testfixture_extensions tinterp
454
455  # Run the test script.
456  set rc [catch { interp eval tinterp $script } msg opt]
457  if {$rc} {
458    puts_into_caller $msg
459    puts_into_caller [dict get $opt -errorinfo]
460    incr ::TC(errors)
461  }
462
463  # Check if the interpreter call [run_thread_tests]
464  if { [interp eval tinterp {info exists ::run_thread_tests_called}] } {
465    set ::run_thread_tests_called 1
466  }
467
468  # Delete the interpreter used to run the test script.
469  interp delete tinterp
470}
471
472proc slave_test_file {zFile} {
473  set tail [file tail $zFile]
474
475  # Remember the value of the shared-cache setting. So that it is possible
476  # to check afterwards that it was not modified by the test script.
477  #
478  ifcapable shared_cache { set scs [sqlite3_enable_shared_cache] }
479
480  # Run the test script in a slave interpreter.
481  #
482  unset -nocomplain ::run_thread_tests_called
483  reset_prng_state
484  set ::sqlite_open_file_count 0
485  set time [time { slave_test_script [list source $zFile] }]
486  set ms [expr [lindex $time 0] / 1000]
487
488  r_install_puts_wrapper
489
490  # Test that all files opened by the test script were closed. Omit this
491  # if the test script has "thread" in its name. The open file counter
492  # is not thread-safe.
493  #
494  if {[info exists ::run_thread_tests_called]==0} {
495    do_test ${tail}-closeallfiles { expr {$::sqlite_open_file_count>0} } {0}
496  }
497  set ::sqlite_open_file_count 0
498
499  # Test that the global "shared-cache" setting was not altered by
500  # the test script.
501  #
502  ifcapable shared_cache {
503    set res [expr {[sqlite3_enable_shared_cache] == $scs}]
504    do_test ${tail}-sharedcachesetting [list set {} $res] 1
505  }
506
507  # Add some info to the output.
508  #
509  output2 "Time: $tail $ms ms"
510  show_memstats
511
512  r_uninstall_puts_wrapper
513  return $ms
514}
515
516proc puts_into_caller {args} {
517  global R
518  if {[llength $args]==1} {
519    append R(output) [lindex $args 0]
520    append R(output) "\n"
521  } else {
522    append R(output) [lindex $args 1]
523  }
524}
525
526#-------------------------------------------------------------------------
527#
528proc r_final_report {} {
529  global R
530
531  sqlite3_test_control_pending_byte 0x010000
532  sqlite3 db $R(dbname)
533
534  db timeout $R(timeout)
535
536  set errcode 0
537
538  # Create the text log file. This is just the concatenation of the
539  # 'output' column of the database for every script that was run.
540  set fd [open $R(logname) w]
541  db eval {SELECT output FROM script ORDER BY config!='full',config,filename} {
542    puts $fd $output
543  }
544  close $fd
545
546  # Check if any scripts reported errors. If so, print one line noting
547  # how many errors, and another identifying the scripts in which they
548  # occured. Or, if no errors occurred, print out "no errors at all!".
549  sqlite3 db $R(dbname)
550  db timeout $R(timeout)
551  db eval { SELECT sum(nerr) AS nerr, sum(ntest) AS ntest FROM script } { }
552  puts "$nerr errors from $ntest tests."
553  if {$nerr>0} {
554    db eval { SELECT config, filename FROM script WHERE nerr>0 } {
555      lappend errlist [r_testname $config $filename]
556    }
557    puts "Errors in: $errlist"
558    set errcode 1
559  }
560
561  # Check if any scripts were not run or did not finish. Print out a
562  # line identifying them if there are any.
563  set errlist [list]
564  db eval { SELECT config, filename FROM script WHERE state!='done' } {
565    lappend errlist [r_testname $config $filename]
566  }
567  if {$errlist!=[list]} {
568    puts "Tests DID NOT FINISH (crashed?): $errlist"
569    set errcode 1
570  }
571
572  set bLeak 0
573  db eval {
574    SELECT id, nmalloc, nbyte, leaker FROM malloc
575      WHERE nmalloc>0 OR nbyte>0
576  } {
577    if {$id==0} {
578      set line "This process "
579    } else {
580      set line "Helper $id "
581    }
582    append line "leaked $nbyte byte in $nmalloc allocations"
583    if {$leaker!=""} { append line " (perhaps in [file tail $leaker])" }
584    puts $line
585    set bLeak 1
586  }
587  if {$bLeak==0} {
588    puts "No leaks - all allocations freed."
589  }
590
591  db close
592
593  puts "Test database is $R(dbname)"
594  puts "Test log file is $R(logname)"
595  if {$errcode} {
596    puts "This test has FAILED."
597  }
598  return $errcode
599}
600
601
602if {$R(helper)==0} {
603  make_new_testset
604}
605
606set R(nHelperRunning) 0
607if {$R(helper)==0 && $R(nJob)>1} {
608  cd $cmdlinearg(TESTFIXTURE_HOME)
609  for {set ii 1} {$ii <= $R(nJob)} {incr ii} {
610    set cmd "[info nameofexec] $R(info_script) helper $ii 2>@1"
611    puts "Launching helper $ii ($cmd)"
612    set chan [open "|$cmd" r]
613    fconfigure $chan -blocking false
614    fileevent $chan readable [list r_helper_readable $ii $chan]
615    incr R(nHelperRunning)
616  }
617  cd $cmdlinearg(testdir)
618}
619
620proc r_helper_readable {id chan} {
621  set data [gets $chan]
622  if {$data!=""} { puts "helper $id:$data" }
623  if {[eof $chan]} {
624    puts "helper $id is finished"
625    incr ::R(nHelperRunning) -1
626    close $chan
627  }
628}
629
630if {$R(nHelperRunning)==0} {
631  while { ""!=[set t [get_next_test]] } {
632    set R(output) ""
633    set TC(count) 0
634    set TC(errors) 0
635
636    foreach {config filename} $t {}
637
638    array set O $::testspec($config)
639    set ::G(perm:name)         $config
640    set ::G(perm:prefix)       $O(-prefix)
641    set ::G(isquick)           1
642    set ::G(perm:dbconfig)     $O(-dbconfig)
643    set ::G(perm:presql)       $O(-presql)
644
645    eval $O(-initialize)
646    set ms [slave_test_file $filename]
647    eval $O(-shutdown)
648
649    unset -nocomplain ::G(perm:sqlite3_args)
650    unset ::G(perm:name)
651    unset ::G(perm:prefix)
652    unset ::G(perm:dbconfig)
653    unset ::G(perm:presql)
654
655    r_set_test_result $config $filename $ms $TC(errors) $TC(count) $R(output)
656
657    if {$R(helper)==0} {
658      foreach msg [r_get_messages] { puts $msg }
659    }
660  }
661
662  # Tests are finished - write a record into testrunner.db describing
663  # any memory leaks.
664  r_memory_report
665
666} else {
667  set TTT 0
668  sqlite3 db $R(dbname)
669  db timeout $R(timeout)
670  while {$R(nHelperRunning)>0} {
671    after 250 { incr TTT }
672    vwait TTT
673    foreach msg [r_get_messages db] { puts $msg }
674  }
675  db close
676}
677
678set errcode 0
679if {$R(helper)==0} {
680  set errcode [r_final_report]
681}
682
683exit $errcode
684
685