-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathunittest.subr
397 lines (317 loc) · 11.8 KB
/
unittest.subr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# Copyright 2014 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Google Inc. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
shtk_import cli
shtk_import list
# Current expectation of the test regarding failure conditions.
_Shtk_Unittest_ExpectFailure=no
# Names of all the registered test cases.
#
# Upon program startup, this variable tracks all standalone test cases.
# However, when we process fixtures, this starts tracking the test cases within
# the currently-running fixture instead.
_Shtk_Unittest_TestCases=
# Names of all the registered test fixtures.
_Shtk_Unittest_TestFixtures=
# Functions to bring into the global namespace within the tests.
_Shtk_Unittest_Use="delayed_fail fail set_expect_failure skip"
# Hooks that can be overriden by the user.
one_time_setup() { true; }
one_time_teardown() { true; }
# Prepares for the execution of the given fully-qualified test case.
#
# This involves bringing all the required functions into the current namespace,
# creating a work directory for the test and entering the work directory.
#
# This function must be called from the subprocess used to run the test.
#
# The parent process must use _shtk_unittest_leave_test to clean up after
# execution.
_shtk_unittest_enter_test() {
local name="${1}"; shift
shtk_cli_info "Testing ${name}..."
_Shtk_Unittest_ExpectFailure=no
for function_name in ${_Shtk_Unittest_Use}; do
eval "${function_name}() { " \
"shtk_unittest_${function_name} \"\${@}\"; }"
done
mkdir "${name}"
cd "${name}"
HOME="$(pwd)"; export HOME
TMPDIR="$(pwd)"; export TMPDIR
}
# Cleans up after executing a test case and computes its result.
#
# This function must be called from the parent process of a process that
# previously called _shtk_unittest_enter_test.
#
# The name parameter provides the fully-qualified name of the test case to
# clean up and the exit_code parameter holds the exit code of the
# subprocess used to run the test.
#
# Returns true if the test case exited successfully; false otherwise.
_shtk_unittest_leave_test() {
local name="${1}"; shift
local exit_code="${1}"; shift
local delayed_failures=0
[ ! -f "${name}/result.delayed-fail" ] \
|| delayed_failures="$(cat "${name}/result.delayed-fail")"
local expected_failure=no
[ ! -f "${name}/result.expect-fail" ] || expected_failure=yes
local skipped=no
[ ! -f "${name}/result.skipped" ] || skipped=yes
local result
if [ "${expected_failure}" = no ]; then
if [ ${exit_code} -eq 0 ]; then
if [ "${delayed_failures}" -gt 0 ]; then
result=delayed-fail
elif [ "${skipped}" = yes ]; then
result=skip
else
result=pass
fi
else
result=fail
fi
else
if [ ${exit_code} -eq 0 ]; then
if [ "${delayed_failures}" -gt 0 ]; then
result=expected-failure
else
shtk_cli_warning "Expected failure but none found"
result=fail
fi
else
result=expected-failure
fi
fi
rm -rf "${name}"
case "${result}" in
delayed-fail)
shtk_cli_warning "Testing ${name}... FAILED" \
"(${delayed_failures} delayed failures)"
return 1
;;
expected-failure)
shtk_cli_info "Testing ${name}... EXPECTED FAILURE"
return 0
;;
fail)
shtk_cli_warning "Testing ${name}... FAILED"
return 1
;;
pass)
shtk_cli_info "Testing ${name}... PASSED"
return 0
;;
skip)
shtk_cli_warning "Testing ${name}... SKIPPED"
return 0
;;
esac
shtk_cli_error "Failed to determine test result"
}
# Defines assert and expect functions for a given name.
#
# For a given _shtk_unittest_check_${name} function, a pair of
# shtk_unittest_assert_${name} and shtk_unittest_expect_${name} functions are
# defined. The former implements a fatal test and the latter a delayed test.
#
# A _shtk_unittest_check_${name} function must exist. This function must
# take the name of the wrapper function as the first argument, the name of a
# "fail function" as the second argument, and then all of the check-specific
# arguments (including, possibly, options).
_shtk_unittest_register_check() {
local name="${1}"; shift
eval "shtk_unittest_assert_${name}() {
_shtk_unittest_check_${name} shtk_unittest_assert_${name} \
shtk_unittest_fail \"\${@}\"
}"
eval "shtk_unittest_expect_${name}() {
_shtk_unittest_check_${name} shtk_unittest_expect_${name} \
shtk_unittest_delayed_fail \"\${@}\"
}"
_Shtk_Unittest_Use="${_Shtk_Unittest_Use} assert_${name} expect_${name}"
}
# Executes a test case within a fixture.
#
# Returns true if the test case passes; false otherwise.
_shtk_unittest_run_fixture_test() {
local fixture="${1}"; shift
local basename="${1}"; shift
local name="${fixture}__${basename}"
shtk_list_contains "${basename}" ${_Shtk_Unittest_TestCases} \
|| shtk_cli_error "Attempting to run unregistered test case ${name}"
(
_shtk_unittest_enter_test "${name}"
local failed=no
if { setup; true; }; then
( "${basename}_test"; true ) || failed=yes
{ teardown; true; } || failed=yes
else
failed=yes
fi
[ "${failed}" = no ]
)
_shtk_unittest_leave_test "${name}" "${?}"
}
# Executes a standalone test case.
#
# Returns true if the test case passes; false otherwise.
_shtk_unittest_run_standalone_test() {
local name="${1}"; shift
shtk_list_contains "${name}" ${_Shtk_Unittest_TestCases} \
|| shtk_cli_error "Attempting to run unregistered test case ${name}"
(
_shtk_unittest_enter_test "${name}"
"${name}_test"
true
)
_shtk_unittest_leave_test "${name}" "${?}"
}
shtk_unittest_add_fixture() {
local name="${1}"; shift
shtk_list_contains "${name}" ${_Shtk_Unittest_TestFixtures} \
&& shtk_cli_error "Duplicate test fixture found: ${name}"
eval "${name}_fixture() { shtk_cli_error '${name}_fixture not defined'; }"
_Shtk_Unittest_TestFixtures="${_Shtk_Unittest_TestFixtures} ${name}"
}
shtk_unittest_add_test() {
local name="${1}"; shift
shtk_list_contains "${name}" ${_Shtk_Unittest_TestCases} \
&& shtk_cli_error "Duplicate test case found: ${name}"
eval "${name}_test() { shtk_cli_error '${name}_test not defined'; }"
_Shtk_Unittest_TestCases="${_Shtk_Unittest_TestCases} ${name}"
}
shtk_unittest_delayed_fail() {
[ "${_Shtk_Unittest_ExpectFailure}" = no ] \
|| shtk_cli_error "delayed_fail does not support expected failures"
# TODO(jmmv): Consider using ${LINENO} if available.
shtk_cli_warning "Delayed failure: ${@}"
# Because the test case runs in a subshell but we need to recover the number
# of delayed failures from the main process, we have to persist this
# information in the file system.
local count=
if [ -f result.delayed-fail ]; then
count="$(cat result.delayed-fail)"
else
count=0
fi
echo "$((${count} + 1))" >result.delayed-fail || \
shtk_cli_error "Failed to record delayed failure"
}
shtk_unittest_fail() {
# TODO(jmmv): Consider using ${LINENO} if available.
if [ "${_Shtk_Unittest_ExpectFailure}" = no ]; then
shtk_cli_error "${@}"
else
shtk_cli_error "Expected failure:" "${@}"
fi
}
shtk_unittest_main() {
shtk_cli_set_help_command "man 1 shtk_unittest"
local filters=
local OPTIND
while getopts :t: arg "${@}"; do
case "${arg}" in
t) # Test filter.
filters="${filters} ${OPTARG}"
;;
:)
shtk_cli_usage_error "Missing argument to option -${OPTARG}"
;;
\?)
shtk_cli_usage_error "Unknown option -${OPTARG}"
;;
esac
done
shift $((${OPTIND} - 1))
OPTIND=1 # Should not be necessary due to the 'local' above.
[ "${#}" -eq 0 ] || shtk_cli_usage_error "No command-line arguments allowed"
[ -n "${_Shtk_Unittest_TestFixtures}" -o \
-n "${_Shtk_Unittest_TestCases}" ] \
|| shtk_cli_error "No test cases defined; did you" \
"call shtk_unittest_add_fixture or shtk_unittest_add_test?"
local passed=0
local failed=0
one_time_setup || shtk_cli_error "Failed to set up test program;" \
"one_time_setup failed"
for name in ${_Shtk_Unittest_TestCases}; do
[ -z "${filters}" ] || shtk_list_contains "${name}" ${filters} \
|| continue
if _shtk_unittest_run_standalone_test "${name}"; then
passed=$((${passed} + 1))
else
failed=$((${failed} + 1))
fi
done
for fixture in ${_Shtk_Unittest_TestFixtures}; do
_Shtk_Unittest_TestCases=
setup() { true; }
teardown() { true; }
eval ${fixture}_fixture
for name in ${_Shtk_Unittest_TestCases}; do
[ -z "${filters}" ] \
|| shtk_list_contains "${fixture}__${name}" ${filters} \
|| continue
if _shtk_unittest_run_fixture_test "${fixture}" "${name}"; then
passed=$((${passed} + 1))
else
failed=$((${failed} + 1))
fi
done
done
one_time_teardown || shtk_cli_error "Failed to clean up test program;" \
"one_time_teardown failed"
local total=$((${passed} + ${failed}))
[ ${total} -gt 0 ] || shtk_cli_error "No tests were run"
if [ ${failed} -eq 0 ]; then
shtk_cli_info "Ran ${total} tests; ALL PASSED"
return 0
else
shtk_cli_warning "Ran ${total} tests; ${failed} FAILED"
return 1
fi
}
shtk_unittest_set_expect_failure() {
_Shtk_Unittest_ExpectFailure=yes
touch result.expect-fail
}
shtk_unittest_skip() {
# TODO(jmmv): Consider using ${LINENO} if available.
if [ "${_Shtk_Unittest_ExpectFailure}" = no ]; then
shtk_cli_warning "${@}"
touch result.skipped
exit 0
else
shtk_cli_error "Attempted to skip a test while expecting a failure"
fi
}
# All unittest core functionality defined; let's pull in all checks!
shtk_import unittest/commands
shtk_import unittest/files
shtk_import unittest/operators