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