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