Home | History | Annotate | Line # | Download | only in teaish
tester.tcl revision 1.1.1.1
      1  1.1  christos ########################################################################
      2  1.1  christos # 2025 April 5
      3  1.1  christos #
      4  1.1  christos # The author disclaims copyright to this source code.  In place of
      5  1.1  christos # a legal notice, here is a blessing:
      6  1.1  christos #
      7  1.1  christos #  * May you do good and not evil.
      8  1.1  christos #  * May you find forgiveness for yourself and forgive others.
      9  1.1  christos #  * May you share freely, never taking more than you give.
     10  1.1  christos #
     11  1.1  christos ########################################################################
     12  1.1  christos #
     13  1.1  christos # Helper routines for running tests on teaish extensions
     14  1.1  christos #
     15  1.1  christos ########################################################################
     16  1.1  christos # ----- @module teaish/tester.tcl -----
     17  1.1  christos #
     18  1.1  christos # @section TEA-ish Testing APIs.
     19  1.1  christos #
     20  1.1  christos # Though these are part of the autosup dir hierarchy, they are not
     21  1.1  christos # intended to be run from autosetup code. Rather, they're for use
     22  1.1  christos # with/via teaish.tester.tcl and target canonical Tcl only, not JimTcl
     23  1.1  christos # (which the autosetup pieces do target).
     24  1.1  christos 
     25  1.1  christos #
     26  1.1  christos # @test-current-scope ?lvl?
     27  1.1  christos #
     28  1.1  christos # Returns the name of the _calling_ proc from ($lvl + 1) levels up the
     29  1.1  christos # call stack (where the caller's level will be 1 up from _this_
     30  1.1  christos # call). If $lvl would resolve to global scope "global scope" is
     31  1.1  christos # returned and if it would be negative then a string indicating such
     32  1.1  christos # is returned (as opposed to throwing an error).
     33  1.1  christos #
     34  1.1  christos proc test-current-scope {{lvl 0}} {
     35  1.1  christos   #uplevel [expr {$lvl + 1}] {lindex [info level 0] 0}
     36  1.1  christos   set ilvl [info level]
     37  1.1  christos   set offset [expr {$ilvl  - $lvl - 1}]
     38  1.1  christos   if { $offset < 0} {
     39  1.1  christos     return "invalid scope ($offset)"
     40  1.1  christos   } elseif { $offset == 0} {
     41  1.1  christos     return "global scope"
     42  1.1  christos   } else {
     43  1.1  christos     return [lindex [info level $offset] 0]
     44  1.1  christos   }
     45  1.1  christos }
     46  1.1  christos 
     47  1.1  christos # @test-msg
     48  1.1  christos #
     49  1.1  christos # Emits all arugments to stdout.
     50  1.1  christos #
     51  1.1  christos proc test-msg {args} {
     52  1.1  christos   puts "$args"
     53  1.1  christos }
     54  1.1  christos 
     55  1.1  christos # @test-warn
     56  1.1  christos #
     57  1.1  christos # Emits all arugments to stderr.
     58  1.1  christos #
     59  1.1  christos proc test-warn {args} {
     60  1.1  christos   puts stderr "WARNING: $args"
     61  1.1  christos }
     62  1.1  christos 
     63  1.1  christos #
     64  1.1  christos # @test-error msg
     65  1.1  christos #
     66  1.1  christos # Triggers a test-failed error with a string describing the calling
     67  1.1  christos # scope and the provided message.
     68  1.1  christos #
     69  1.1  christos proc test-fail {args} {
     70  1.1  christos   #puts stderr "ERROR: \[[test-current-scope 1]]: $msg"
     71  1.1  christos   #exit 1
     72  1.1  christos   error "FAIL: \[[test-current-scope 1]]: $args"
     73  1.1  christos }
     74  1.1  christos 
     75  1.1  christos array set ::test__Counters {}
     76  1.1  christos array set ::test__Config {
     77  1.1  christos   verbose-assert 0 verbose-affirm 0
     78  1.1  christos }
     79  1.1  christos 
     80  1.1  christos # Internal impl for affirm and assert.
     81  1.1  christos #
     82  1.1  christos # $args = ?-v? script {msg-on-fail ""}
     83  1.1  christos proc test__affert {failMode args} {
     84  1.1  christos   if {$failMode} {
     85  1.1  christos     set what assert
     86  1.1  christos   } else {
     87  1.1  christos     set what affirm
     88  1.1  christos   }
     89  1.1  christos   set verbose $::test__Config(verbose-$what)
     90  1.1  christos   if {"-v" eq [lindex $args 0]} {
     91  1.1  christos     lassign $args - script msg
     92  1.1  christos     if {1 == [llength $args]} {
     93  1.1  christos       # If -v is the only arg, toggle default verbose mode
     94  1.1  christos       set ::test__Config(verbose-$what) [expr {!$::test__Config(verbose-$what)}]
     95  1.1  christos       return
     96  1.1  christos     }
     97  1.1  christos     incr verbose
     98  1.1  christos   } else {
     99  1.1  christos     lassign $args script msg
    100  1.1  christos   }
    101  1.1  christos   incr ::test__Counters($what)
    102  1.1  christos   if {![uplevel 1 expr [list $script]]} {
    103  1.1  christos     if {"" eq $msg} {
    104  1.1  christos       set msg $script
    105  1.1  christos     }
    106  1.1  christos     set txt [join [list $what # $::test__Counters($what) "failed:" $msg]]
    107  1.1  christos     if {$failMode} {
    108  1.1  christos       puts stderr $txt
    109  1.1  christos       exit 1
    110  1.1  christos     } else {
    111  1.1  christos       error $txt
    112  1.1  christos     }
    113  1.1  christos   } elseif {$verbose} {
    114  1.1  christos     puts stderr [join [list $what # $::test__Counters($what) "passed:" $script]]
    115  1.1  christos   }
    116  1.1  christos }
    117  1.1  christos 
    118  1.1  christos #
    119  1.1  christos # @affirm ?-v? script ?msg?
    120  1.1  christos #
    121  1.1  christos # Works like a conventional assert method does, but reports failures
    122  1.1  christos # using [error] instead of [exit]. If -v is used, it reports passing
    123  1.1  christos # assertions to stderr. $script is evaluated in the caller's scope as
    124  1.1  christos # an argument to [expr].
    125  1.1  christos #
    126  1.1  christos proc affirm {args} {
    127  1.1  christos   tailcall test__affert 0 {*}$args
    128  1.1  christos }
    129  1.1  christos 
    130  1.1  christos #
    131  1.1  christos # @assert ?-v? script ?msg?
    132  1.1  christos #
    133  1.1  christos # Works like [affirm] but exits on error.
    134  1.1  christos #
    135  1.1  christos proc assert {args} {
    136  1.1  christos   tailcall test__affert 1 {*}$args
    137  1.1  christos }
    138  1.1  christos 
    139  1.1  christos #
    140  1.1  christos # @assert-matches ?-e? pattern ?-e? rhs ?msg?
    141  1.1  christos #
    142  1.1  christos # Equivalent to assert {[string match $pattern $rhs]} except that
    143  1.1  christos # if either of those are prefixed with an -e flag, they are eval'd
    144  1.1  christos # and their results are used.
    145  1.1  christos #
    146  1.1  christos proc assert-matches {args} {
    147  1.1  christos   set evalLhs 0
    148  1.1  christos   set evalRhs 0
    149  1.1  christos   if {"-e" eq [lindex $args 0]} {
    150  1.1  christos     incr evalLhs
    151  1.1  christos     set args [lassign $args -]
    152  1.1  christos   }
    153  1.1  christos   set args [lassign $args pattern]
    154  1.1  christos   if {"-e" eq [lindex $args 0]} {
    155  1.1  christos     incr evalRhs
    156  1.1  christos     set args [lassign $args -]
    157  1.1  christos   }
    158  1.1  christos   set args [lassign $args rhs msg]
    159  1.1  christos 
    160  1.1  christos   if {$evalLhs} {
    161  1.1  christos     set pattern [uplevel 1 $pattern]
    162  1.1  christos   }
    163  1.1  christos   if {$evalRhs} {
    164  1.1  christos     set rhs [uplevel 1 $rhs]
    165  1.1  christos   }
    166  1.1  christos   #puts "***pattern=$pattern\n***rhs=$rhs"
    167  1.1  christos   tailcall test__affert 1 \
    168  1.1  christos     [join [list \[ string match [list $pattern] [list $rhs] \]]] $msg
    169  1.1  christos   # why does this not work? [list \[ string match [list $pattern] [list $rhs] \]] $msg
    170  1.1  christos   # "\[string match [list $pattern] [list $rhs]\]"
    171  1.1  christos }
    172  1.1  christos 
    173  1.1  christos #
    174  1.1  christos # @test-assert testId script ?msg?
    175  1.1  christos #
    176  1.1  christos # Works like [assert] but emits $testId to stdout first.
    177  1.1  christos #
    178  1.1  christos proc test-assert {testId script {msg ""}} {
    179  1.1  christos   puts "test $testId"
    180  1.1  christos   tailcall test__affert 1 $script $msg
    181  1.1  christos }
    182  1.1  christos 
    183  1.1  christos #
    184  1.1  christos # @test-expect testId script result
    185  1.1  christos #
    186  1.1  christos # Runs $script in the calling scope and compares its result to
    187  1.1  christos # $result, minus any leading or trailing whitespace.  If they differ,
    188  1.1  christos # it triggers an [assert].
    189  1.1  christos #
    190  1.1  christos proc test-expect {testId script result} {
    191  1.1  christos   puts "test $testId"
    192  1.1  christos   set x [string trim [uplevel 1 $script]]
    193  1.1  christos   set result [string trim $result]
    194  1.1  christos   tailcall test__affert 0 [list "{$x}" eq "{$result}"] \
    195  1.1  christos     "\nEXPECTED: <<$result>>\nGOT:      <<$x>>"
    196  1.1  christos }
    197  1.1  christos 
    198  1.1  christos #
    199  1.1  christos # @test-catch cmd ?...args?
    200  1.1  christos #
    201  1.1  christos # Runs [cmd ...args], repressing any exception except to possibly log
    202  1.1  christos # the failure. Returns 1 if it caught anything, 0 if it didn't.
    203  1.1  christos #
    204  1.1  christos proc test-catch {cmd args} {
    205  1.1  christos   if {[catch {
    206  1.1  christos     uplevel 1 $cmd {*}$args
    207  1.1  christos   } rc xopts]} {
    208  1.1  christos     puts "[test-current-scope] ignoring failure of: $cmd [lindex $args 0]: $rc"
    209  1.1  christos     return 1
    210  1.1  christos   }
    211  1.1  christos   return 0
    212  1.1  christos }
    213  1.1  christos 
    214  1.1  christos #
    215  1.1  christos # @test-catch-matching pattern (script|cmd args...)
    216  1.1  christos #
    217  1.1  christos # Works like test-catch, but it expects its argument(s) to to throw an
    218  1.1  christos # error matching the given string (checked with [string match]).  If
    219  1.1  christos # they do not throw, or the error does not match $pattern, this
    220  1.1  christos # function throws, else it returns 1.
    221  1.1  christos #
    222  1.1  christos # If there is no second argument, the $cmd is assumed to be a script,
    223  1.1  christos # and will be eval'd in the caller's scope.
    224  1.1  christos #
    225  1.1  christos # TODO: add -glob and -regex flags to control matching flavor.
    226  1.1  christos #
    227  1.1  christos proc test-catch-matching {pattern cmd args} {
    228  1.1  christos   if {[catch {
    229  1.1  christos     #puts "**** catch-matching cmd=$cmd args=$args"
    230  1.1  christos     if {0 == [llength $args]} {
    231  1.1  christos       uplevel 1 $cmd {*}$args
    232  1.1  christos     } else {
    233  1.1  christos       $cmd {*}$args
    234  1.1  christos     }
    235  1.1  christos   } rc xopts]} {
    236  1.1  christos     if {[string match $pattern $rc]} {
    237  1.1  christos       return 1
    238  1.1  christos     } else {
    239  1.1  christos       error "[test-current-scope] exception does not match {$pattern}: {$rc}"
    240  1.1  christos     }
    241  1.1  christos   }
    242  1.1  christos   error "[test-current-scope] expecting to see an error matching {$pattern}"
    243  1.1  christos }
    244  1.1  christos 
    245  1.1  christos if {![array exists ::teaish__BuildFlags]} {
    246  1.1  christos   array set ::teaish__BuildFlags {}
    247  1.1  christos }
    248  1.1  christos 
    249  1.1  christos #
    250  1.1  christos # @teaish-build-flag3 flag tgtVar ?dflt?
    251  1.1  christos #
    252  1.1  christos # If the current build has the configure-time flag named $flag set
    253  1.1  christos # then tgtVar is assigned its value and 1 is returned, else tgtVal is
    254  1.1  christos # assigned $dflt and 0 is returned.
    255  1.1  christos #
    256  1.1  christos # Caveat #1: only valid when called in the context of teaish's default
    257  1.1  christos # "make test" recipe, e.g. from teaish.test.tcl. It is not valid from
    258  1.1  christos # a teaish.tcl configure script because (A) the state it relies on
    259  1.1  christos # doesn't fully exist at that point and (B) that level of the API has
    260  1.1  christos # more direct access to the build state. This function requires that
    261  1.1  christos # an external script have populated its internal state, which is
    262  1.1  christos # normally handled via teaish.tester.tcl.in.
    263  1.1  christos #
    264  1.1  christos # Caveat #2: defines in the style of HAVE_FEATURENAME with a value of
    265  1.1  christos # 0 are, by long-standing configure script conventions, treated as
    266  1.1  christos # _undefined_ here.
    267  1.1  christos #
    268  1.1  christos proc teaish-build-flag3 {flag tgtVar {dflt ""}} {
    269  1.1  christos   upvar $tgtVar tgt
    270  1.1  christos   if {[info exists ::teaish__BuildFlags($flag)]} {
    271  1.1  christos     set tgt $::teaish__BuildFlags($flag)
    272  1.1  christos     return 1;
    273  1.1  christos   } elseif {0==[array size ::teaish__BuildFlags]} {
    274  1.1  christos     test-warn \
    275  1.1  christos       "\[[test-current-scope]] was called from " \
    276  1.1  christos       "[test-current-scope 1] without the build flags imported."
    277  1.1  christos   }
    278  1.1  christos   set tgt $dflt
    279  1.1  christos   return 0
    280  1.1  christos }
    281  1.1  christos 
    282  1.1  christos #
    283  1.1  christos # @teaish-build-flag flag ?dflt?
    284  1.1  christos #
    285  1.1  christos # Convenience form of teaish-build-flag3 which returns the
    286  1.1  christos # configure-time-defined value of $flag or "" if it's not defined (or
    287  1.1  christos # if it's an empty string).
    288  1.1  christos #
    289  1.1  christos proc teaish-build-flag {flag {dflt ""}} {
    290  1.1  christos   set tgt ""
    291  1.1  christos   teaish-build-flag3 $flag tgt $dflt
    292  1.1  christos   return $tgt
    293  1.1  christos }
    294