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