######################################################################## # 2025 April 5 # # The author disclaims copyright to this source code. In place of # a legal notice, here is a blessing: # # * May you do good and not evil. # * May you find forgiveness for yourself and forgive others. # * May you share freely, never taking more than you give. # ######################################################################## # # Helper routines for running tests on teaish extensions # ######################################################################## # ----- @module teaish/tester.tcl ----- # # @section TEA-ish Testing APIs. # # Though these are part of the autosup dir hierarchy, they are not # intended to be run from autosetup code. Rather, they're for use # with/via teaish.tester.tcl and target canonical Tcl only, not JimTcl # (which the autosetup pieces do target). # # @test-current-scope ?lvl? # # Returns the name of the _calling_ proc from ($lvl + 1) levels up the # call stack (where the caller's level will be 1 up from _this_ # call). If $lvl would resolve to global scope "global scope" is # returned and if it would be negative then a string indicating such # is returned (as opposed to throwing an error). # proc test-current-scope {{lvl 0}} { #uplevel [expr {$lvl + 1}] {lindex [info level 0] 0} set ilvl [info level] set offset [expr {$ilvl - $lvl - 1}] if { $offset < 0} { return "invalid scope ($offset)" } elseif { $offset == 0} { return "global scope" } else { return [lindex [info level $offset] 0] } } # @test-msg # # Emits all arugments to stdout. # proc test-msg {args} { puts "$args" } # @test-warn # # Emits all arugments to stderr. # proc test-warn {args} { puts stderr "WARNING: $args" } # # @test-error msg # # Triggers a test-failed error with a string describing the calling # scope and the provided message. # proc test-fail {args} { #puts stderr "ERROR: \[[test-current-scope 1]]: $msg" #exit 1 error "FAIL: \[[test-current-scope 1]]: $args" } array set ::test__Counters {} array set ::test__Config { verbose-assert 0 verbose-affirm 0 } # Internal impl for affirm and assert. # # $args = ?-v? script {msg-on-fail ""} proc test__affert {failMode args} { if {$failMode} { set what assert } else { set what affirm } set verbose $::test__Config(verbose-$what) if {"-v" eq [lindex $args 0]} { lassign $args - script msg if {1 == [llength $args]} { # If -v is the only arg, toggle default verbose mode set ::test__Config(verbose-$what) [expr {!$::test__Config(verbose-$what)}] return } incr verbose } else { lassign $args script msg } incr ::test__Counters($what) if {![uplevel 1 expr [list $script]]} { if {"" eq $msg} { set msg $script } set txt [join [list $what # $::test__Counters($what) "failed:" $msg]] if {$failMode} { puts stderr $txt exit 1 } else { error $txt } } elseif {$verbose} { puts stderr [join [list $what # $::test__Counters($what) "passed:" $script]] } } # # @affirm ?-v? script ?msg? # # Works like a conventional assert method does, but reports failures # using [error] instead of [exit]. If -v is used, it reports passing # assertions to stderr. $script is evaluated in the caller's scope as # an argument to [expr]. # proc affirm {args} { tailcall test__affert 0 {*}$args } # # @assert ?-v? script ?msg? # # Works like [affirm] but exits on error. # proc assert {args} { tailcall test__affert 1 {*}$args } # # @assert-matches ?-e? pattern ?-e? rhs ?msg? # # Equivalent to assert {[string match $pattern $rhs]} except that # if either of those are prefixed with an -e flag, they are eval'd # and their results are used. # proc assert-matches {args} { set evalLhs 0 set evalRhs 0 if {"-e" eq [lindex $args 0]} { incr evalLhs set args [lassign $args -] } set args [lassign $args pattern] if {"-e" eq [lindex $args 0]} { incr evalRhs set args [lassign $args -] } set args [lassign $args rhs msg] if {$evalLhs} { set pattern [uplevel 1 $pattern] } if {$evalRhs} { set rhs [uplevel 1 $rhs] } #puts "***pattern=$pattern\n***rhs=$rhs" tailcall test__affert 1 \ [join [list \[ string match [list $pattern] [list $rhs] \]]] $msg # why does this not work? [list \[ string match [list $pattern] [list $rhs] \]] $msg # "\[string match [list $pattern] [list $rhs]\]" } # # @test-assert testId script ?msg? # # Works like [assert] but emits $testId to stdout first. # proc test-assert {testId script {msg ""}} { puts "test $testId" tailcall test__affert 1 $script $msg } # # @test-expect testId script result # # Runs $script in the calling scope and compares its result to # $result, minus any leading or trailing whitespace. If they differ, # it triggers an [assert]. # proc test-expect {testId script result} { puts "test $testId" set x [string trim [uplevel 1 $script]] set result [string trim $result] tailcall test__affert 0 [list "{$x}" eq "{$result}"] \ "\nEXPECTED: <<$result>>\nGOT: <<$x>>" } # # @test-catch cmd ?...args? # # Runs [cmd ...args], repressing any exception except to possibly log # the failure. Returns 1 if it caught anything, 0 if it didn't. # proc test-catch {cmd args} { if {[catch { uplevel 1 $cmd {*}$args } rc xopts]} { puts "[test-current-scope] ignoring failure of: $cmd [lindex $args 0]: $rc" return 1 } return 0 } # # @test-catch-matching pattern (script|cmd args...) # # Works like test-catch, but it expects its argument(s) to to throw an # error matching the given string (checked with [string match]). If # they do not throw, or the error does not match $pattern, this # function throws, else it returns 1. # # If there is no second argument, the $cmd is assumed to be a script, # and will be eval'd in the caller's scope. # # TODO: add -glob and -regex flags to control matching flavor. # proc test-catch-matching {pattern cmd args} { if {[catch { #puts "**** catch-matching cmd=$cmd args=$args" if {0 == [llength $args]} { uplevel 1 $cmd {*}$args } else { $cmd {*}$args } } rc xopts]} { if {[string match $pattern $rc]} { return 1 } else { error "[test-current-scope] exception does not match {$pattern}: {$rc}" } } error "[test-current-scope] expecting to see an error matching {$pattern}" } if {![array exists ::teaish__BuildFlags]} { array set ::teaish__BuildFlags {} } # # @teaish-build-flag3 flag tgtVar ?dflt? # # If the current build has the configure-time flag named $flag set # then tgtVar is assigned its value and 1 is returned, else tgtVal is # assigned $dflt and 0 is returned. # # Caveat #1: only valid when called in the context of teaish's default # "make test" recipe, e.g. from teaish.test.tcl. It is not valid from # a teaish.tcl configure script because (A) the state it relies on # doesn't fully exist at that point and (B) that level of the API has # more direct access to the build state. This function requires that # an external script have populated its internal state, which is # normally handled via teaish.tester.tcl.in. # # Caveat #2: defines in the style of HAVE_FEATURENAME with a value of # 0 are, by long-standing configure script conventions, treated as # _undefined_ here. # proc teaish-build-flag3 {flag tgtVar {dflt ""}} { upvar $tgtVar tgt if {[info exists ::teaish__BuildFlags($flag)]} { set tgt $::teaish__BuildFlags($flag) return 1; } elseif {0==[array size ::teaish__BuildFlags]} { test-warn \ "\[[test-current-scope]] was called from " \ "[test-current-scope 1] without the build flags imported." } set tgt $dflt return 0 } # # @teaish-build-flag flag ?dflt? # # Convenience form of teaish-build-flag3 which returns the # configure-time-defined value of $flag or "" if it's not defined (or # if it's an empty string). # proc teaish-build-flag {flag {dflt ""}} { set tgt "" teaish-build-flag3 $flag tgt $dflt return $tgt }