xref: /sqlite-3.40.0/test/testrunner.tcl (revision 615aecea)
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