xref: /freebsd/contrib/sqlite3/autosetup/teaish/tester.tcl (revision 17f0f75308f287efea825457364e2a4de2e107d4)
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