Browse Source

Add TFKit v0.0.15

Alois Mahdal 6 years ago
parent
commit
b68b701813

+ 389
- 0
utils/tfkit/doc/README.md View File

@@ -0,0 +1,389 @@
1
+Tests
2
+=====
3
+
4
+Running tests is handled by tfkit/runtests:
5
+
6
+    $ tfkit/runtest [filter]
7
+
8
+*filter* is a regular expression to be applied to sub-test name, running
9
+only the matching ones.  See below for details.
10
+
11
+
12
+Writing tests
13
+-------------
14
+
15
+Tests can be written in any scripting language, although the built-in
16
+framework, written in Bash, provides some useful features for writing
17
+certain kind of relatively simple tests.
18
+
19
+The harness, though, assumes that:
20
+
21
+ *  Any direct sub-directory of `$TF_SUITE` directory ("tests" by default)
22
+    that contains at least *TF_RUN* executable becomes a test,
23
+
24
+ *  basename of this directory becomes the name of the test,
25
+
26
+ *  and return code from running the executable is reported
27
+    as result of the test, according to "Exit status" section below.
28
+
29
+
30
+Naming
31
+------
32
+
33
+Test name should start with name of the module that is tested and
34
+underscore.  If module name contains dots, they should be replaced with
35
+underscores as well.
36
+
37
+    core_sanity
38
+    mod_submod_function
39
+    ini_iniread
40
+
41
+are valid test names.
42
+
43
+
44
+Data
45
+----
46
+
47
+Should the test need any data, just leave it around in the test directory
48
+along with *TF_RUN*.
49
+
50
+Note that before running, the whole test directory is automatically
51
+copied to a temporary location (one per test), and should the test fail,
52
+copied back as a debugging artifact.  For this reason, *do not store
53
+huge amounts of data here*.  If you really need huge data, consider
54
+obtaining it (and throwing it away) within runtime of *TF_RUN*.
55
+
56
+
57
+Exit status
58
+-----------
59
+
60
+We try hard to follow this semantic:
61
+
62
+ *  *Zero* means *OK* -- test has been run and passed.
63
+
64
+ *  *One* means *Failure* -- test has been run but failed (e.g. found
65
+     a bug).
66
+
67
+ *  *Two* means *Bailout* --  test has decided not to run at all.
68
+
69
+ *  *Three* means *Error* -- there was error detected during execution,
70
+     but script was able to clean up properly.
71
+
72
+ *  *Four* means *Panic* -- there was other error but script *was not*
73
+     able to clean up properly.
74
+
75
+ *  Anything else should indicate other uncaught errors, including those
76
+    outside control of the program such as segfaults in the test code
77
+    or test being SIGKILLed.
78
+
79
+Notice that the higher the value is, the worse situation it indicates.
80
+Thus, if a test is composed of several sub-tests, you need to make sure
81
+to always **exit with the highest value** (subtest.sh does take care
82
+of this).
83
+
84
+See *common.sh* for functions and variables to help with handling exit
85
+statuses with this semantic.
86
+
87
+Also see Notes section for more details on exit statuses, including
88
+cheat sheet and dscussuion.
89
+
90
+
91
+Framework
92
+---------
93
+
94
+
95
+### harness.sh ###
96
+
97
+This part is not intended to be used in tests, but rather contains
98
+functions that help govern test discovery, preparation and execution as
99
+is described in previous sections.  Feel free to poke around, of course.
100
+
101
+
102
+### subtest.sh ###
103
+
104
+As name suggests, this file defines few functions to handle subtests
105
+in *TF_RUN*.
106
+
107
+In order to make use of the subtests functionality, you will need to
108
+define two functions yourself:  `tf_enum_subtests` to enumerate names of
109
+tests you want to run, and `tf_do_subtest` with actual test
110
+implementation.
111
+
112
+The minimal *TF_RUN* with two subtests could look like this:
113
+
114
+    #!/bin/bash
115
+
116
+    . $TF_DIR/include/subtest.sh
117
+
118
+    tf_enum_subtests() {
119
+        echo test1
120
+        echo test2
121
+        something && echo test3
122
+    }
123
+
124
+    tf_do_subtest() {
125
+        case $1 in
126
+            test1)  myprog foo ;;
127
+            test2)  myprog bar ;;
128
+            test3)  myprog baz ;;
129
+        esac
130
+    }
131
+
132
+    tf_do_subtests
133
+
134
+At the end, `tf_do_subtests` acts as a launcher of the actual test.
135
+In short, it will
136
+
137
+ 1. run `tf_enum_subtests`, taking each line as name of a subtest;
138
+    for each subtest:
139
+
140
+     1. source *TF_SETUP*, if such file is found,
141
+     2. launch the `tf_do_subtest()` function with subtest name as
142
+        the only argument,
143
+     3. source *TF_CLEANUP*, if such file is found,
144
+
145
+ 2. and finally, report "worst" exit status encountered.
146
+
147
+Note that subtest names need to be single words (`[a-zA-Z0-9_]`).
148
+
149
+
150
+### tools.sh ###
151
+
152
+This file contains various tools and utilities to help with testing.
153
+
154
+Curently there is only one function, `tf_testflt` designed to help write
155
+tests for simple unix filters.
156
+
157
+
158
+#### tf_testflt ####
159
+
160
+The idea is that tester specifies
161
+
162
+ *  test name,
163
+ *  command to launch the system under test,
164
+ *  a data stream to use as STDIN,
165
+ *  and expected STDOUT, STDERR, and exit status.
166
+
167
+and tf_testflt launches the command, collects tha data and evaluates
168
+and reports the result using unified diff.
169
+
170
+In its simplest form:
171
+
172
+    tf_testflt -n foo my_command arg
173
+
174
+the function will run `my_command arg` (not piping anything to it),
175
+and will expect it to finish with exit status 0 and empty both STDERR
176
+and STDOUT.
177
+
178
+Example of full form,
179
+
180
+    tf_testflt -n foo -i foo.in -O foo.stdout -E foo.stderr -S 2 myprog
181
+
182
+will pipe foo.in into `myprog`, expecting exit status of 2, and STDOUT and
183
+STDERR as above.  Notice that parameters specifying expected values are
184
+uppercase, and those specifying input values are lowercase.
185
+
186
+Specifying name is mandatory, because it's used in reporting messages,
187
+and as a basis for naming temporary result files: these are saved in
188
+*results* subdirectory and kept for further reference.
189
+
190
+
191
+### common.sh ###
192
+
193
+This includes simple functions and variables shared between both mentioned
194
+libraries.
195
+
196
+First group is designed to help support the exit status semantic:
197
+
198
+ *  The functions are `tf_exit_pass`, `tf_exit_fail`, `tf_exit_bailout`,
199
+    `tf_exit_error` and `tf_exit_panic` and each take any number of
200
+    parameters that are printed on stderr.
201
+
202
+ *  The variables are `TF_ES_OK`, `TF_ES_FAIL`, `TF_ES_BAILOUT`,
203
+    `TF_ES_ERROR` and `TF_ES_PANIC` and are supposed to be used with
204
+    `return` builtin, e.g. to return from `tf_exit_error`.
205
+
206
+Second group is useful to better control output:  functions `tf_warn`,
207
+`tf_debug` and `tf_think` are used to print stuff on STDERR.  Use of
208
+`tf_warn` is apparent, just as `tf_debug`, the latter being muted if
209
+`TF_DEBUG` is set to `false` (set it to `true` to turn on debugging).
210
+
211
+`tf_think` is used for progress info, and is muted unless `TF_VERBOSE`
212
+is set to `true`, which is by default.
213
+
214
+
215
+### Setup and cleanup ###
216
+
217
+Special files *TF_SETUP* and *TF_CLEANUP* (one of them or both) can be
218
+added along with *TF_RUN*.  These will be sourced before (*TF_SETUP*)
219
+and after every subtest (*TF_CLEANUP*).
220
+
221
+First, if any of these files are missing, it is considered as if the
222
+respective phase succeeded.  Second, if setup phase fails, test will
223
+be skipped and subtest exit status will be *TF_ES_BAILOUT*.   Last,
224
+if cleanup fails (no matter result of setup), subtests aborts with
225
+*TF_ES_PANIC* returned.  Be aware that in this case the actual test
226
+status, albeit useful, is lost.
227
+
228
+When coming from other test frameworks, this may feel harsh, but note
229
+that this has been designed with the idea that if a cleanup fails,
230
+it may render all further tests are automatically unsafe, because the
231
+environment is not as expected.
232
+
233
+To cope with this behavior, try to bear in mind following advice:
234
+
235
+ 1. Make sure you write setup/cleanup procedures with extreme care and
236
+    test them well.
237
+
238
+ 2. Do not do complicated and risky things in the setup/cleanup phases.
239
+
240
+ 3. If you need to do such things, consider doing them in the *TF_RUN*
241
+    instead of doing them for all subtests.
242
+
243
+ 4. You don't need to clean up everything, the contents of the testing dir
244
+    will be moved out from the test system.
245
+
246
+ 5. If there are scenarios you can safely fix or ignore, handle them in
247
+    a robust manner.
248
+
249
+
250
+Notes
251
+-----
252
+
253
+
254
+### bailout vs. `tf_enum_subtests` ###
255
+
256
+One more note to claify relation of bailout and `tf_enum_subtests`.
257
+As you may have noticed, there are two ways how to skip a test:
258
+return prematurely with `TF_ES_BAILOUT`, or suppress enumeration in
259
+`tf_enum_subtests`.  The problem is that the latter does not do anything
260
+to inform upper in the stack that a test has been skipped, which seems to
261
+break the principle described in previous sections.
262
+
263
+Don't confuse these mechanisms, though. Each is supposed to be used
264
+for distinct purpose.  Compare: by using the `tf_enum_subtests` you are
265
+saying that you actually **did not even want** to run the test in the
266
+first place.  By using `TF_ES_BAILOUT`, you are saying that you **wanted**
267
+to run the test but could not.
268
+
269
+A few common cases if that helps you:
270
+
271
+ *  If during the test you find out that for some reason it can't be
272
+    carried out (e.g. an external resource is not available, or
273
+    something outside the SUT is broken), use `TF_ES_BAILOUT`.
274
+
275
+        tf_enum_subtests() {
276
+            echo test1
277
+            echo test2
278
+            echo test3
279
+        }
280
+
281
+        tf_do_subtest() {
282
+            case $1 in
283
+                test1) do_stuff  ;;
284
+                test2) do_other_stuff ;;
285
+                test3) curl -s http://www.example.com/ >file \
286
+                        || return $TF_ES_BAILOUT
287
+                       do_stuff_with file ;;
288
+            esac
289
+        }
290
+
291
+ *  If you want to filter out some sub-tests for some platforms, e.g. a
292
+    test for only 64-bit architectures, or a test only for Mac OS (IOW,
293
+    you can safely say that running this sub-test would be totally
294
+    pointless on this box), use `tf_enum_subtests`--just omit this test
295
+    from enumeration.
296
+
297
+        tf_enum_subtests() {
298
+            echo test1
299
+            echo test2
300
+            if this_is_macos_x; then
301
+                echo test3
302
+            fi
303
+        }
304
+
305
+ *  If you want to disable (comment out test) that you might not have
306
+    implemented yet or is broken (and for some reason you still want
307
+    it to haunt the test code) or something else outside SUT is broken
308
+    and prevents you from running the test, use `tf_enum_subtests` and
309
+    properly comment the reasons in code.
310
+
311
+        tf_enum_subtests() {
312
+            echo test1
313
+            echo test2
314
+        #   echo test3      #FIXME: implement after bz1234
315
+        }
316
+
317
+ *  If in doubt, use `TF_ES_BAILOUT`.
318
+
319
+
320
+### On exit statuses: three and above ###
321
+
322
+The difference in *error*, *panic* and higher values is subtle but
323
+important.  Follow me as I try to explain:
324
+
325
+ 1. If script has changed something on the system outside the working
326
+    directory, it is apparently expected to revert that change.
327
+
328
+ 2. Now if an error occurs, but the code responsible for cleaning up is
329
+    safely run, you can say there was *error but we have recovered*.
330
+
331
+ 3. But if the change can't be reverted safely, we know that we have
332
+    broken something and latter code may lead to weird results (including
333
+    masking bugs(!)), it's time to *panic* (in the code, not in real
334
+    life ;))
335
+
336
+ 4. And then there are corner cases like a bug in the script, OOM kill
337
+    or timeout when the status will be different and not really controlled
338
+    by the script.  Such cases will have to be treated the same way as
339
+    the "panic" case, but...
340
+
341
+ 5. the use of *panic* adds hint that the status has been set consciously
342
+    by the script, albeit exiting "in a hurry"--without proper clean up.
343
+
344
+Unfortunately there will be cases like above but with the error code less
345
+than four.   Example is a Bash script syntax error, which returns 2, or
346
+Python exception which returns 1.  Yes, in such cases the information
347
+conveyed by the exit status is wrong and you should do everything to
348
+avoid it.
349
+
350
+Possibilities like "test has passed but then something blew up" exist,
351
+but conveying this information is responsibility of the test output.
352
+
353
+Following table can be used as a cheat-sheet:
354
+
355
+    .---------------------------------------------------------------.
356
+    | e |    state of         |                                     |
357
+    | s |---------------------| script says                         |
358
+    |   | SUT   | environment |                                     |
359
+    |---|-------|-------------|-------------------------------------|
360
+    | 0 | OK    | safe        | test passed, everything worked fine |
361
+    | 1 | buggy | safe        | test failed, everything worked fine |
362
+    | 2 | ???   | safe        | I decided not to run the test       |
363
+    | 3 | ???   | safe        | Something blew up but I managed to  |
364
+    |   |       |             | clean up (I promise!)               |
365
+    | 4 | ???   | broken      | Something blew up and I rushed out  |
366
+    |   |       |             | in panic                            |
367
+    | * | ???   | broken      | ...nothing (is dead)                |
368
+    '---------------------------------------------------------------'
369
+
370
+As you can see, following this semantic allows us to see both the state
371
+of the system under test (SUT) *and* the environment.
372
+
373
+Following table illustrates how different statuses map to different
374
+scenarios with regard to test result as well as state of the environment:
375
+
376
+    .--------------------------------------------------.
377
+    | environment |  test result   |  test result      |
378
+    |             | pass fail unkn | pass fail unkn    |
379
+    |-------------|----------------|-------------------|
380
+    | clean(ed)   |  0    1    3   |  OK  FAIL ERROR   |
381
+    | untouched   |  ~    ~    2   |  ~    ~   BAILOUT |
382
+    | mess        |  ~    ~    4   |  ~    ~   PANIC   |
383
+    | ?! (trap)   |  ~    ~    5   |  ~    ~   ~       |
384
+    | ?! (sig 9)  |  ~    ~    137 |  ~    ~   ~       |
385
+    | ?! (aliens) |  ~    ~    ?   |  ~    ~   ~       |
386
+    '-------------|----------------|-------------------|
387
+                  |  exit status   |  human-readable   |
388
+                  |                |  name (TF_ES_*)   |
389
+                  '------------------------------------'

+ 26
- 0
utils/tfkit/doc/templates/grep_engine/TF_RUN View File

@@ -0,0 +1,26 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/subtest.sh"
4
+. "$TF_DIR/include/tools.sh"
5
+
6
+tf_enum_subtests() {
7
+    echo fixed
8
+    echo basic
9
+    echo extended
10
+#   echo perl       # TODO: write test
11
+}
12
+
13
+tf_name2cmd() {
14
+    local name=$1
15
+    local t_in="test/ALL.stdin"
16
+    local o_out="oracle/$name.stdout"
17
+    local args
18
+    case $name in
19
+        fixed)       args="-F 'The mask *.* matches all.'" ;;
20
+        basic)       args="-G 'he.*'" ;;
21
+        extended)    args="-P '.*og?g'" ;;
22
+    esac
23
+    echo "tf_testflt -n $name -i $t_in -O $o_out grep $args"
24
+}
25
+
26
+tf_do_subtests

+ 3
- 0
utils/tfkit/doc/templates/grep_engine/oracle/basic.stdout View File

@@ -0,0 +1,3 @@
1
+Linda and Nina work together.
2
+The mask *.* matches all.
3
+The mask *.docx matches Word documents.

+ 3
- 0
utils/tfkit/doc/templates/grep_engine/oracle/extended.stdout View File

@@ -0,0 +1,3 @@
1
+Alice uses Google.
2
+Bob wears goggles.
3
+Linda and Nina work together.

+ 1
- 0
utils/tfkit/doc/templates/grep_engine/oracle/fixed.stdout View File

@@ -0,0 +1 @@
1
+The mask *.* matches all.

+ 6
- 0
utils/tfkit/doc/templates/grep_engine/test/ALL.stdin View File

@@ -0,0 +1,6 @@
1
+Alice uses Google.
2
+Bob wears goggles.
3
+Joe and John are friends.
4
+Linda and Nina work together.
5
+The mask *.* matches all.
6
+The mask *.docx matches Word documents.

+ 37
- 0
utils/tfkit/doc/templates/simple/TF_RUN View File

@@ -0,0 +1,37 @@
1
+#!/bin/bash
2
+#shellcheck disable=SC1090
3
+
4
+. "$TF_DIR/include/subtest.sh"
5
+. "$TF_DIR/include/tools.sh"
6
+
7
+tf_enum_subtests() {
8
+    echo one
9
+    echo two
10
+    echo three
11
+}
12
+
13
+my_sut() {
14
+    local word=$1
15
+    declare -A map
16
+    map[one]=1
17
+    map[two]=2
18
+    map[three]=33
19
+    echo "${map[$word]}"
20
+}
21
+
22
+mkoracle() {
23
+    mkdir -p oracle
24
+    case $subtest in
25
+        one)    echo 1 ;;
26
+        two)    echo 2 ;;
27
+        three)  echo 3 ;;
28
+    esac > "oracle/$subtest.stdout"
29
+}
30
+
31
+tf_do_subtest() {
32
+    local subtest=$1
33
+    mkoracle
34
+    tf_testflt -n "$subtest" -O "oracle/$subtest.stdout" "my_sut $subtest"
35
+}
36
+
37
+tf_do_subtests

+ 142
- 0
utils/tfkit/include/common.sh View File

@@ -0,0 +1,142 @@
1
+#
2
+# Variables to hold exit status semantic
3
+#
4
+TF_ES_OK=0
5
+TF_ES_FAIL=1
6
+TF_ES_BAILOUT=2
7
+TF_ES_ERROR=3
8
+TF_ES_PANIC=4
9
+
10
+#
11
+# Color definition variables
12
+#
13
+TF_COLOR_BLACK="\033[0;30m"
14
+TF_COLOR_RED="\033[0;31m"
15
+TF_COLOR_GREEN="\033[0;32m"
16
+TF_COLOR_YELLOW="\033[0;33m"
17
+TF_COLOR_BLUE="\033[0;34m"
18
+TF_COLOR_MAGENTA="\033[0;35m"
19
+TF_COLOR_CYAN="\033[0;36m"
20
+TF_COLOR_WHITE="\033[0;37m"
21
+TF_COLOR_LBLACK="\033[1;30m"
22
+TF_COLOR_LRED="\033[1;31m"
23
+TF_COLOR_LGREEN="\033[1;32m"
24
+TF_COLOR_LYELLOW="\033[1;33m"
25
+TF_COLOR_LBLUE="\033[1;34m"
26
+TF_COLOR_LMAGENTA="\033[1;35m"
27
+TF_COLOR_LCYAN="\033[1;36m"
28
+TF_COLOR_LWHITE="\033[1;37m"
29
+TF_COLOR_NONE="\033[1;0m"
30
+
31
+
32
+_tf__is_word() {
33
+    #
34
+    # Check if $1 contains only alphanumeric chars or _
35
+    #
36
+    local tainted=$1    # "dirty" version
37
+    local clean         # clean version
38
+    clean=$(tr -c -d '_[:alnum:]' <<< "$tainted")
39
+    test "$tainted" = "$clean"
40
+}
41
+
42
+tf_exit_ok() {
43
+    #
44
+    # Exit with OK status
45
+    #
46
+    exit $TF_ES_OK
47
+}
48
+
49
+tf_exit_fail() {
50
+    #
51
+    # Warn $1 and exit with status FAIL
52
+    #
53
+    tf_warn "$@"
54
+    exit $TF_ES_FAIL
55
+}
56
+
57
+tf_exit_bailout() {
58
+    #
59
+    # Warn $1 and exit with status FAIL
60
+    #
61
+    tf_warn "$@"
62
+    exit $TF_ES_BAILOUT
63
+}
64
+
65
+tf_exit_error() {
66
+    #
67
+    # Warn $1 and exit with status FAIL
68
+    #
69
+    tf_warn "$@"
70
+    exit $TF_ES_ERROR
71
+}
72
+
73
+tf_exit_panic() {
74
+    #
75
+    # Warn $1 and exit with status FAIL
76
+    #
77
+    tf_warn "$@"
78
+    exit $TF_ES_PANIC
79
+}
80
+
81
+tf_debug() {
82
+    #
83
+    # Emit debug message
84
+    #
85
+    $TF_DEBUG || return 0
86
+    local msg
87
+    for msg in "$@";
88
+    do
89
+        $TF_COLOR && echo -ne "$TF_COLOR_CYAN" >&2
90
+        echo "||| $msg" >&2;
91
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
92
+    done
93
+}
94
+
95
+tf_debugv() {
96
+    #
97
+    # Emit debug message showing value of each named variable
98
+    #
99
+    local varname
100
+    local declare_str
101
+    for varname in "$@";
102
+    do
103
+        if ! _tf__is_word "$varname";
104
+        then
105
+            tf_warn "tf_debugv: unsafe value skipped: $varname";
106
+            continue
107
+        fi
108
+        if declare_str=$(declare -p "$varname" 2>/dev/null);
109
+        then
110
+            tf_debug "${declare_str#declare ?? }"
111
+        else
112
+            tf_debug "$varname #Unset"
113
+        fi
114
+    done
115
+}
116
+
117
+tf_think() {
118
+    #
119
+    # Emit status/progress message
120
+    #
121
+    $TF_VERBOSE || return 0
122
+    local msg
123
+    for msg in "$@";
124
+    do
125
+        $TF_COLOR && echo -ne "$TF_COLOR_LBLACK" >&2
126
+        echo "$pfx$msg$sfx" >&2;
127
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
128
+    done
129
+}
130
+
131
+tf_warn() {
132
+    #
133
+    # Emit warning
134
+    #
135
+    local msg
136
+    for msg in "$@";
137
+    do
138
+        $TF_COLOR && echo -ne "$TF_COLOR_LRED" >&2
139
+        echo "$msg" >&2;
140
+        $TF_COLOR && echo -ne "$TF_COLOR_NONE" >&2
141
+    done
142
+}

+ 124
- 0
utils/tfkit/include/harness.sh View File

@@ -0,0 +1,124 @@
1
+#!/bin/bash
2
+# tfkit - Shellfu's movable test framework
3
+# See LICENSE file for copyright and license details.
4
+
5
+. "$TF_DIR/include/common.sh"
6
+
7
+
8
+__tf_collect_if_needed() {
9
+    #
10
+    # Collect artifact if exit status suggests it
11
+    #
12
+    # Use test exit status $1 to help decide if artifacts are
13
+    # needed, and collect them if so.
14
+    #
15
+    # If TF_COLLECT is set to "always", collect regardless of
16
+    # the status.  If set to "never", do not collect anything.
17
+    # The default setting, "auto" collects unless status is 0
18
+    # or 1 (pass or bailout); in that case do nothing.
19
+    #
20
+    local tes=$1    # test exit status
21
+    local will      # should we collect artifacts?
22
+    case "$TF_COLLECT:$tes" in
23
+        always:*)   will=true ;;
24
+        never:*)    will=false ;;
25
+        auto:0)     will=false ;;
26
+        auto:2)     will=false ;;
27
+        auto:*)     will=true ;;
28
+        *)          tf_exit_bailout "bad value of TF_COLLECT: $TF_COLLECT" ;;
29
+    esac
30
+    $will || return 0
31
+    mkdir -p "$artifact_dir/$stamp"
32
+    cp -r "$tmpdir"/* "$artifact_dir/$stamp"
33
+}
34
+
35
+__tf_header() {
36
+    #
37
+    # create header to add to output before test
38
+    #
39
+    local hdrline                       # each line from generator
40
+    local hdrgen="$TF_SUITE/TF_HEADER"  # header generator script
41
+    tf_think "#"
42
+    tf_think "# ---"
43
+    tf_think "# tfkit:"
44
+    if test -x "$hdrgen";
45
+    then
46
+        tf_think "#     sut:"
47
+        $hdrgen \
48
+          | while IFS= read -r hdrline;
49
+            do
50
+                test -n "$hdrline" || break
51
+                tf_think "#         $hdrline"
52
+            done
53
+    else
54
+        tf_think "#     hint: >"
55
+        tf_think "#         Add $hdrgen executable for own header."
56
+        tf_think "#         It should output YAML (\"key: value\" pairs) about"
57
+        tf_think "#         your SUT ('version' at least')."
58
+    fi
59
+    tf_think "#     run:"
60
+    tf_think "#         start_time: $(date -Iseconds)"
61
+    test -n "$TF_FILTER_TEST" \
62
+     && tf_think "#         TF_FILTER_TEST: $TF_FILTER_TEST"
63
+    test -n "$TF_FILTER_SUBTEST" \
64
+     && tf_think "#         TF_FILTER_SUBTEST: $TF_FILTER_SUBTEST"
65
+    tf_think "#"
66
+    tf_think ""
67
+}
68
+
69
+tf_enum_tests() {
70
+    #
71
+    # List what looks like test; relative to $TF_SUITE
72
+    #
73
+    tf_debug "TF_SUITE='$TF_SUITE'"
74
+    test -d "$TF_SUITE" || return 0
75
+    find -L \
76
+        "$TF_SUITE" \
77
+        -mindepth 2 \
78
+        -maxdepth 2 \
79
+        -type f \
80
+        -perm /111 \
81
+        -name TF_RUN \
82
+      | cut -d/ -f2
83
+}
84
+
85
+tf_run_tests() {
86
+    #
87
+    # Discover and run tests
88
+    #
89
+    local es=0          # overall exit status
90
+    local tmpdir=""     # test temporary dir
91
+    local tname=""      # test name
92
+    local tes=0         # test result
93
+    local stamp=""      # test stamp to use as artifact name
94
+    local tf_dir tf_suite   # to keep absolute paths for TF_RUN
95
+    local artifact_dir  # where to keep artifacts
96
+    artifact_dir="$(readlink -f "$TF_ARTIFACTS")"
97
+    __tf_header
98
+    tf_debug "TF_VERSION='$TF_VERSION'"
99
+    tf_dir="$(readlink -f "$TF_DIR")"
100
+    tf_suite="$(readlink -f "$TF_SUITE")"
101
+    es=0
102
+    for tname in $(tf_enum_tests | grep -e "$TF_FILTER_TEST");
103
+    do
104
+        tf_think "... $tname"
105
+        tmpdir=$(mktemp -d)
106
+        stamp=$(date "+artifacts-$tname-%Y%m%d-%H%M%S")
107
+        cp -r "$TF_SUITE/$tname/"* "$tmpdir"
108
+        pushd "$tmpdir" >/dev/null
109
+            TF_DIR="$tf_dir" TF_SUITE=$tf_suite TF_TEST="$tname" \
110
+                ./TF_RUN
111
+            tes=$?
112
+            __tf_collect_if_needed $tes
113
+            test $tes -gt $es && es=$tes
114
+        popd >/dev/null
115
+        rm -rf "$tmpdir"
116
+        if test $tes -eq 0;
117
+        then
118
+            tf_think "''' $tname ($tes)"
119
+        else
120
+            tf_warn "??? $tname ($tes)"
121
+        fi
122
+    done
123
+    return $es
124
+}

+ 84
- 0
utils/tfkit/include/subtest.sh View File

@@ -0,0 +1,84 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/common.sh"
4
+
5
+tf_enum_subtests() {
6
+    #
7
+    # Stub: enumerate subtests
8
+    #
9
+    tf_warn "implement tf_enum_subtests()!"
10
+    return "$TF_ES_ERROR"
11
+}
12
+
13
+tf_do_subtest() {
14
+    #
15
+    # Stub: perform test named $1
16
+    #
17
+    tf_warn "implement tf_do_subtest()!"
18
+    return "$TF_ES_ERROR"
19
+}
20
+
21
+_tf_do_subtest() {
22
+    #
23
+    # Run single subtest inc. setup/cleanup if present
24
+    #
25
+    local subtname="$1"     # this subtest name
26
+    local ses=0             # subtest exit status
27
+    local setup=true        # setup command
28
+    local cleanup=true      # cleanup command
29
+    if test -f TF_SETUP;
30
+    then
31
+        setup=". TF_SETUP"
32
+        bash -n TF_SETUP || {
33
+            tf_warn "synax errors in TF_SETUP, skipping"
34
+            return "$TF_ES_ERROR"
35
+        }
36
+    fi
37
+    if test -f TF_CLEANUP;
38
+    then
39
+        setup=". TF_CLEANUP"
40
+        bash -n TF_CLEANUP || {
41
+            tf_warn "synax errors in TF_CLEANUP, skipping"
42
+            return "$TF_ES_ERROR"
43
+        }
44
+    fi
45
+    if $setup;
46
+    then
47
+        tf_do_subtest "$subtname"; ses=$?
48
+    else
49
+        tf_warn "setup phase failed, skipping: $subtname"
50
+        ses=$TF_ES_ERROR
51
+    fi
52
+    if ! $cleanup;
53
+    then
54
+        tf_warn "cleanup phase failed: $subtname"
55
+        ses=$TF_ES_PANIC
56
+    fi
57
+    return "$ses"
58
+}
59
+
60
+tf_do_subtests() {
61
+    #
62
+    # Run all subtests and return highest status
63
+    #
64
+    local es=0              # final exit status ("worst" of subtests)
65
+    local subtname=""       # one subtest name
66
+    local tes=""            # one subtest exit status
67
+    local enumd=TF_ENUMERATED_SUBTESTS
68
+    local fltrd=TF_FILTERED_SUBTESTS
69
+    tf_enum_subtests >$enumd    || { tf_warn "error enumerating subtests"; return "$TF_ES_BAILOUT"; }
70
+    test -s $enumd              || { tf_warn "no subtests enumerated";     return "$TF_ES_BAILOUT"; }
71
+    grep -e "$TF_FILTER_SUBTEST" $enumd > $fltrd
72
+    test -s $fltrd  || tf_debug "TF_FILTER_SUBTEST ate everything: $TF_FILTER_SUBTEST"
73
+
74
+    for subtname in $(<$fltrd);
75
+    do
76
+        tf_think "::: $TF_TEST::$subtname"
77
+        TF_SUBTEST=$subtname _tf_do_subtest "$subtname";
78
+        tes=$?
79
+        test $tes -gt $es              && es=$tes
80
+        test $tes -gt "$TF_ES_OK"      && tf_warn "!!! $TF_TEST::$subtname ($tes)"
81
+        test $tes -gt "$TF_ES_BAILOUT" && break
82
+    done
83
+    return $es
84
+}

+ 86
- 0
utils/tfkit/include/tools.sh View File

@@ -0,0 +1,86 @@
1
+#!/bin/bash
2
+
3
+. "$TF_DIR/include/common.sh"
4
+
5
+# 1. exec: [test] -> [result]
6
+# 2. eval:           [result] == [oracle]
7
+
8
+tf_testflt() {
9
+    #
10
+    # Run a simple test for a unix filter
11
+    #
12
+    #     tf_testflt -n foo [-i foo.stdin] \
13
+    #                [-O foo.stdout] [-E foo.stderr] [-S 3] \
14
+    #                cmd arg...
15
+    #
16
+    # Will drop *result/NAME.stdout* and *result/NAME.stderr* (intentionally
17
+    # not cleaning up).
18
+    #
19
+
20
+    # defaults
21
+    #
22
+    local t_in="/dev/null"      # test: stdin
23
+    local t_name=""             # test: name
24
+                                # command is "$@" after arg parsing
25
+    local t_es="0"              # final test exit status
26
+    local o_out="/dev/null"     # oracle: stdout
27
+    local o_err="/dev/null"     # oracle: stderr
28
+    local o_es="0"              # oralce: exit status
29
+    local r_out r_err r_es      # result: ^ ^ ^ those 3
30
+
31
+    local diff=diff
32
+    type colordiff >/dev/null 2>/dev/null && diff=colordiff
33
+
34
+    # get args
35
+    #
36
+    local orig_args="$0 $*"
37
+    tf_debug "orig_args=$orig_args"
38
+    local arg_err=false
39
+    while true; do case "$1" in
40
+        -i) t_in="$2";          shift 2 || { arg_err=true; break; } ;;
41
+        -n) t_name="$2";        shift 2 || { arg_err=true; break; } ;;
42
+        -O) o_out="$2";         shift 2 || { arg_err=true; break; } ;;
43
+        -E) o_err="$2";         shift 2 || { arg_err=true; break; } ;;
44
+        -S) o_es="$2";          shift 2 || { arg_err=true; break; } ;;
45
+        --)                     shift; break ;;
46
+        "")                            break ;;
47
+        -*) tf_warn "wrong testcli arg: $1"; return $TF_ES_BAILOUT ;;
48
+        *)                             break ;;
49
+    esac done
50
+    $arg_err && { tf_warn "error parsing arguments: $orig_args"; return $TF_ES_BAILOUT; }
51
+    tf_debug "t_in='$t_in'"
52
+    tf_debug "t_name='$t_name'"
53
+    tf_debug "o_out='$o_out'"
54
+    tf_debug "o_err='$o_err'"
55
+    tf_debug "o_es='$o_es'"
56
+    tf_debug "test command: $*"
57
+    test "$t_in" = "-" && t_in=/dev/stdin   # works better for check below
58
+    test -z "$t_name"  && { tf_warn "missing test name"             ; return $TF_ES_BAILOUT; }
59
+    test -z "$1"       && { tf_warn "missing test command"          ; return $TF_ES_BAILOUT; }
60
+    test -r "$t_in"    || { tf_warn "missing input file: $t_in"     ; return $TF_ES_BAILOUT; }
61
+    test -e "$o_out"   || { tf_warn "missing oracle stdout: $o_out" ; return $TF_ES_BAILOUT; }
62
+    test -e "$o_err"   || { tf_warn "missing oracle stderr: $o_err" ; return $TF_ES_BAILOUT; }
63
+    test "$o_es" -ge 0 || { tf_warn "invalid oracle status: $o_es"  ; return $TF_ES_BAILOUT; }
64
+
65
+    # prepare
66
+    #
67
+    mkdir -p result
68
+    r_out="result/$t_name.stdout"
69
+    r_err="result/$t_name.stderr"
70
+    tf_debug "r_out='$r_out'"
71
+    tf_debug "r_err='$r_err'"
72
+    touch "$r_out" || { tf_warn "cannot create tmp file: $r_out" ; return $TF_ES_BAILOUT; }
73
+    touch "$r_err" || { tf_warn "cannot create tmp file: $r_err" ; return $TF_ES_PANIC; }
74
+
75
+    # run
76
+    #
77
+    ( <"$t_in" eval "$@" >"$r_out" 2>"$r_err" ); r_es=$?
78
+    tf_debug "r_es='$r_es'"
79
+
80
+    # eval/report/exit
81
+    #
82
+    test $r_es = $o_es || { tf_warn "bad exit status: $r_es (need $o_es)" ; t_es=$TF_ES_FAIL; }
83
+    $diff -u "$o_err" "$r_err" || t_es=$TF_ES_FAIL
84
+    $diff -u "$o_out" "$r_out" || t_es=$TF_ES_FAIL
85
+    return $t_es
86
+}

+ 95
- 0
utils/tfkit/runtests View File

@@ -0,0 +1,95 @@
1
+#!/bin/bash
2
+# tfkit - Shellfu's movable test framework
3
+# See LICENSE file for copyright and license details.
4
+
5
+TF_VERSION="0.0.15"
6
+
7
+die() {
8
+    echo "$@" && exit 9
9
+}
10
+
11
+usage() {
12
+    echo "usage: $(basename "$0") [-c|-C] [-t tests_re] [-s subtest_re] [-p binpath] [-v] [-d]" >&2
13
+    exit 2
14
+}
15
+
16
+version() {
17
+    echo "TFKit (A trivial test kit) $TF_VERSION"
18
+    exit 0
19
+}
20
+
21
+LC_ALL=C
22
+
23
+#
24
+# Artifact directory path
25
+#
26
+TF_ARTIFACTS="${TF_ARTIFACTS:-artifacts}"
27
+
28
+#
29
+# Artifact collection mode
30
+#
31
+# 'always' to always collect, 'never` to never collect and 'auto'
32
+# to collect only on failed tests.
33
+#
34
+TF_COLLECT="${TF_COLLECT:-auto}"
35
+
36
+#
37
+# Enable color?
38
+#
39
+TF_COLOR=${TF_COLOR:-true}
40
+
41
+#
42
+# Turn on debug mode?
43
+#
44
+TF_DEBUG="${TF_DEBUG:-false}"
45
+
46
+#
47
+# Location of own directory
48
+#
49
+TF_DIR=${TF_DIR:-$(dirname "$0")}
50
+
51
+#
52
+# Regex (BRE) to filter subtests based on name
53
+#
54
+TF_FILTER_SUBTEST=${TF_FILTER_SUBTEST:-}
55
+
56
+#
57
+# Regex (BRE) to filter tests based on name
58
+#
59
+TF_FILTER_TEST=${TF_FILTER_TEST:-}
60
+
61
+#
62
+# Location of test suite
63
+#
64
+TF_SUITE="${TF_SUITE:-tests}"
65
+
66
+#
67
+# Turn on verbosity?
68
+#
69
+TF_VERBOSE=${TF_VERBOSE:-true}
70
+
71
+while true; do case "$1" in
72
+    -c|--collect)           TF_COLLECT=always;          shift ;;
73
+    -C|--no-collect)        TF_COLLECT=never;           shift ;;
74
+    -d|--debug)             TF_DEBUG=true;              shift ;;
75
+    -p|--prefix)            export PATH="$(readlink -f "$2")/bin:$PATH"
76
+                                                        shift 2 || usage ;;
77
+    -s|--filter-subtest)    TF_FILTER_SUBTEST="$2";     shift 2 || usage ;;
78
+    -t|--filter-test)       TF_FILTER_TEST="$2";        shift 2 || usage ;;
79
+    -v|--verbose)           TF_VERBOSE=true;            shift ;;
80
+    --version-semver)       echo "$TF_VERSION"; exit 0 ;;
81
+    --version)              version ;;
82
+    "") break ;;
83
+    *)  usage ;;
84
+esac done
85
+
86
+export LC_ALL
87
+export TF_DIR TF_SUITE TF_ARTIFACTS
88
+export TF_COLLECT TF_FILTER_SUBTEST TF_FILTER_TEST
89
+export TF_DEBUG TF_VERBOSE
90
+export TF_VERSION
91
+
92
+. "$TF_DIR/include/harness.sh" \
93
+ || die "cannot import harness; is TF_DIR set properly?: $TF_DIR"
94
+
95
+time tf_run_tests

+ 18
- 0
utils/tfkit/tfkit.mk View File

@@ -0,0 +1,18 @@
1
+# tfkit - Shellfu's movable test framework
2
+# See LICENSE file for copyright and license details.
3
+#
4
+# To use, set TF_DIR (make macro) in your Makefile and
5
+# include this file.
6
+#
7
+# Adds `test` target.
8
+#
9
+# Note that running "runtests" directly should have the same
10
+# effect but you will need to set TF_DIR and/or TF_SUITE properly
11
+# unless they are default (see README)
12
+
13
+export TF_DIR
14
+
15
+test:
16
+	@$(TF_DIR)/runtests
17
+
18
+.PHONY: test