OILS / stdlib / testing.ysh View on Github | oilshell.org

202 lines, 153 significant
1# TODO:
2# - [x] Renaming a bit for "privacy"
3# - [x] Renaming for public interface: test-case, test-suite, assert (subject to change)
4# - [x] Rename to use "-"s for procs.
5# - [x] Make test-case and test-suite take a word arg so they don't need parens
6# - [x] error if you try to re-enter run-tests
7# - [x] testing testing: assert fail, exn in assert, exn outside of assert
8# - [-] check for both kinds of exceptions (_status and _error)
9# - [x] tests for tests
10# - [ ] question: naming for variables?
11# - [ ] --tool test support (& spec test case for it)
12# - [ ] Turn the assert expression into a string, to print if it fails
13# - [ ] Use ctx instead of globals, wherever possible
14# - [ ] Merge _status and _error. Every exception should set _error, with the
15# status stored on _error.status. Use _error instead of the icky global
16# _assertion_result
17
18##########
19# Colors #
20##########
21
22const _BOLD = $'\e[1m'
23const _RED = $'\e[31m'
24const _GREEN = $'\e[32m'
25const _YELLOW = $'\e[33m'
26const _PURPLE = $'\e[35m'
27const _CYAN = $'\e[36m'
28const _RESET = $'\e[0;0m'
29
30func _red(text) {
31 if (_test_use_color) {
32 return ("${_BOLD}${_RED}${text}${_RESET}")
33 } else {
34 return (text)
35 }
36}
37func _yellow(text) {
38 if (_test_use_color) {
39 return ("${_YELLOW}${text}${_RESET}")
40 } else {
41 return (text)
42 }
43}
44func _green(text) {
45 if (_test_use_color) {
46 return ("${_YELLOW}${text}${_RESET}")
47 } else {
48 return (text)
49 }
50}
51func _cyan(text) {
52 if (_test_use_color) {
53 return ("${_YELLOW}${text}${_RESET}")
54 } else {
55 return (text)
56 }
57}
58
59############
60# Internal #
61############
62
63var _testing_in_progress = false
64var _test_suite_depth = 0
65var _test_suite_stack = []
66var _tests = {}
67
68var _num_test_fail = 0
69var _num_test_succ = 0
70var _test_use_color = true
71
72var _assertion_result = 0 # 0: no assert, 1: failed, 2: errored
73
74proc _start-test-suite (; name) {
75 call _test_suite_stack->append(name)
76}
77
78proc _end-test-suite {
79 call _test_suite_stack->pop()
80}
81
82proc _test-print-indented (msg) {
83 for _ in (0 .. _test_suite_depth) {
84 printf " "
85 }
86 echo "$msg"
87}
88
89proc _run-test-suite (; suite) {
90 for name, elem in (suite) {
91 if (type (elem) === "Dict") {
92 # It's another suite.
93 var begin = _cyan("begin")
94 _test-print-indented "$begin $name"
95 setglobal _test_suite_depth += 1;
96 _run-test-suite (elem)
97 setglobal _test_suite_depth -= 1;
98 var end = _cyan("end")
99 _test-print-indented "$end"
100 } else {
101 # It's a test case.
102 _run-test-case (name, elem)
103 }
104 }
105}
106
107proc _run-test-case (; name, block) {
108 var test = _yellow("test")
109 for _ in (0 .. _test_suite_depth) {
110 printf " "
111 }
112 printf "$test $name ..."
113 setglobal _assertion_result = 0
114 try {
115 eval (block)
116 }
117 if (_status === 0) {
118 setglobal _num_test_succ += 1
119 var ok = _green(" ok")
120 printf "$ok"
121 printf '\n'
122 } else {
123 setglobal _num_test_fail += 1
124 printf '\n'
125 if (_assertion_result === 1) {
126 # An assertion failed.
127 var fail = _red("assertion FAILED")
128 _test-print-indented " $fail"
129 } elif (_assertion_result === 2) {
130 # There was an exception while evaluating an assertion.
131 var exn_in_assert = _red("assertion ERRORED:")
132 _test-print-indented " $exn_in_assert $_status"
133 } else {
134 # There was an exception in the test case outside of any `assert`.
135 var exn = _red("ERROR:")
136 _test-print-indented " $exn $_status"
137 }
138 }
139}
140
141##########
142# Public #
143##########
144
145proc test-suite (name ; ; ; block) {
146 _start-test-suite (name)
147 eval (block)
148 _end-test-suite
149}
150
151proc test-case (name ; ; ; block) {
152 var test_suite = _tests
153 for suite_name in (_test_suite_stack) {
154 if (not (suite_name in test_suite)) {
155 setvar test_suite[suite_name] = {}
156 }
157 setvar test_suite = test_suite[suite_name]
158 }
159 setvar test_suite[name] = block
160}
161
162proc assert ( ; cond LAZY ) {
163 var success
164 try {
165 setvar success = evalExpr(cond)
166 }
167 if (_status !== 0) {
168 setglobal _assertion_result = 2
169 error "exception in assertion"
170 } elif (not success) {
171 setglobal _assertion_result = 1
172 error "assertion failed"
173 }
174}
175
176proc run-tests {
177 if (_testing_in_progress) {
178 error "Cannot run tests while testing is already in progress"
179 }
180 setglobal _testing_in_progress = true
181 setglobal _num_test_fail = 0
182 setglobal _num_test_succ = 0
183
184 _run-test-suite (_tests)
185 setglobal _tests = {}
186 setglobal _testing_in_progress = false
187
188 var total = _num_test_fail + _num_test_succ
189 if (total === 0) {
190 var na = _yellow("0 tests ran")
191 printf "$na"
192 printf '\n'
193 } elif (_num_test_fail === 0) {
194 var success = _green("$total tests succeeded")
195 printf "$success"
196 printf '\n'
197 } else {
198 var failure = _red("$_num_test_fail / $total tests failed")
199 printf "$failure"
200 printf '\n'
201 }
202}