-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathzgold
executable file
·336 lines (303 loc) · 9.79 KB
/
zgold
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
#!/bin/zsh -f
typeset -r help_message='USAGE
o zgold [--help|-h]
Display this help message.
o zgold (--version|-v)
Display the version.
o zgold <config-file> [<sub-command>]
<config-file>
Config file path, written in Zsh. If this arg is a dir path instead of a
file path, then the file is assumed to be `config.zsh` within the dir. The
config file must define the following params:
* test_cases
Associative array. Keys are test case names and values are commands.
The output of the commands gets compared against the golden files.
* resources
Dir path where the golden files `<test_case_name>_golden.txt` are.
Optionally, the config file may also define these params:
* pre_diff_hooks
Associative array. Keys are test case names and values are commands.
The output of the test case command is piped to the pre diff hook
command. This happens before the diff against the golden file. This
is useful to remove parts of the output of the test case command
which are expected to change among runs, such as timestamps.
Also affects patching.
<sub-command>
See the section SUB-COMMANDS. Default: `run`.
o zgold -b <directory...>
Batch run. Execute the sub-command `run` for every directory.
<directory>
A dir which has a `config.zsh` file as per expected by the form
`zgold <config-file> [<sub-command>]`. Useful for CI.
SUB-COMMANDS
* ls
List all test cases.
* run [<test-case-name>]
Run all test cases or a single test case by name.
* patch [<test-case-name>]
Patch all test cases or a single test case by name.
* exec <test-case-name>
Execute the command corresponding to a test case by name.
EXIT CODES
* >= 0 Total amount of failed test cases.
* -1 Invalid config file.
EXAMPLES
* List all test cases: `zgold test/all_pass/config.zsh ls`
* Run all test cases: `zgold test/all_pass/config.zsh run`
* Run all test cases: `zgold test/all_pass`'
# Command line argument parsing code from:
# <https://github.com/hernancerm/args.zsh>
function parse_args {
local -A args=()
local -a args_pos=()
eval "local -a bool_opts=(${1})"
eval "local -A defaults=(${2})"
local -a user_args=(${@[@]:3})
local skip_parse_of_current_arg='false'
for (( i=1 ; i<=${#user_args} ; i++ )); do
if [[ "${skip_parse_of_current_arg}" = 'true' ]]; then
skip_parse_of_current_arg='false'
continue
fi
# If arg begins with a dash (-) then it's an option.
if [[ "${${user_args[${i}]}[1]}" = '-' ]]; then
# Check if the option is bool(ean).
if [[ ${bool_opts[(i)${user_args[i]}]} -le ${#bool_opts} ]]; then
args+=(["${user_args[${i}]}"]='true')
else
args+=(["${user_args[${i}]}"]="${user_args[$((i+1))]}")
skip_parse_of_current_arg='true'
fi
continue
fi
# Otherwise, it's a positional arg.
args_pos+=("${user_args[$((i))]}")
done
# Serialize positional args as an iarray.
args[positional]="$(typeset -p args_pos)"
# Assign default values to unset options.
for flag_name flag_value in ${(kv)defaults}; do
if [[ -z "${args[${flag_name}]}" ]]; then
args+=([${flag_name}]=${flag_value})
fi
done
# Print aarray.
typeset -p args
}
# Parse command line arguments.
eval "$(parse_args \
'--help -h --version -v -b' '' ${@})"
eval "${args[positional]}"
## Pretty print using ANSI escape sequences. Without new line ending.
## @param $1 ANSI escape sequence.
## @param $2 Message.
function _pechon {
if [[ -t 1 ]]; then
echo -n "${1}${2}\e[0m"
else
echo -n "${2}"
fi
}
## Pretty print using ANSI escape sequences. With new line ending.
## @param $1 ANSI escape sequence.
## @param $2 Message.
function _pecho {
_pechon "$1" "$2"
echo
}
## @param $1 Resources dir path.
## @param $2 Test case names.
function _validate_config_file_params_are_loaded {
if [[ -z "${1}" ]]; then
_pecho '\e[31m' 'The param "resources" is not defined in the config file.'
exit -1
fi
if [[ -z "${2}" ]]; then
_pecho '\e[31m' 'The param "test_cases" is not defined in the config file.'
exit -1
fi
}
## @param $1 Test case name.
## @param $2 Resources directory path.
## @param $3 Command used to get the result to compare with the golden file.
## @stdout Diff on failure, nothing otherwise.
function _run_test {
# setup
local cmd_output="$(eval ${3})"
local golden_file_path="${2}/${1}_golden.txt"
if ! [[ -f "${golden_file_path}" ]]; then
touch "${golden_file_path}"
fi
# Apply hooks.
if [[ -n ${pre_diff_hooks} ]] && [[ -n "${pre_diff_hooks[${1}]}" ]]; then
cmd_output="$(eval "echo \"${cmd_output}\" | ${pre_diff_hooks[${1}]}")"
fi
# execute
local diff=$(diff -u "${golden_file_path}" <(echo "${cmd_output}"))
# verify
if [[ -n "${diff}" ]]; then
if [[ -d "${args_pos[1]}" ]]; then
_pechon '\e[31m' '✘'; echo " $(basename -a ${2}) -- ${1}"
else
_pechon '\e[31m' '✘'; echo " ${1}"
fi
_pecho '\e[31m\e[4m' "Command executed:"
_pecho '\e[31m' "${3}"
_pechon '\e[31m\e[4m' "Diff between golden file and command output:"
if [[ -t 1 ]] && [[ -n "$(command -v delta)" ]]; then
echo "${diff}" \
| sed '1,2d' \
| delta --hunk-header-style='line-number' --paging='never'
echo
else
echo "${diff}"
fi
return 1
fi
if [[ -d "${args_pos[1]}" ]]; then
_pechon '\e[32m' '✔'; echo " $(basename -a ${2}) -- ${1}"
else
_pechon '\e[32m' '✔'; echo " ${1}"
fi
}
## @param $1 Test case name.
## @param $2 Resources directory path.
## @param $3 Command used to get the result to compare with the golden file.
## @stdout Patch info.
function _patch_test {
local cmd_output="$(eval ${3})"
local golden_file_path="${2}/${1}_golden.txt"
if ! [[ -f "${golden_file_path}" ]]; then
touch "${golden_file_path}"
fi
# Apply hooks.
if [[ -n ${pre_diff_hooks} ]] && [[ -n "${pre_diff_hooks[${1}]}" ]]; then
cmd_output="$(eval "echo \"${cmd_output}\" | ${pre_diff_hooks[${1}]}")"
fi
local diff=$(diff -u "${golden_file_path}" <(echo "${cmd_output}"))
echo "${cmd_output}" > "${golden_file_path}"
echo "Patch complete for: ${golden_file_path}"
}
## @stdout Static header.
function _print_header {
_pecho '\e[33m' 'Launching ZGold'
echo "ZGold: $(zgold -v)"
echo "ZSH: $(zsh --version)"
}
## @param $1 Amount of total test cases.
## @param $2 Amount of failed test cases.
## @param $3 Time in seconds execution took.
## @stdout Footer.
function _print_footer {
local message='Results'
echo "${1} tests run in ${3}s"
echo
_pecho '\e[33m\e[4m' 'Results'
_pechon '\e[32m' '✔'; echo " Passed $(( ${1} - ${2} ))"
_pechon '\e[31m' '✘'; echo " Failed ${2}"
}
# Form: zgold [--help|-h]
if [[ "${args[--help]}" = 'true' ]] \
|| [[ "${args[-h]}" = 'true' ]] \
|| [[ "${#@}" -eq 0 ]]; then
echo "${help_message}"
exit 0
fi
# Form: zgold (--version|-v)
if [[ "${args[--version]}" = 'true' ]] \
|| [[ "${args[-v]}" = 'true' ]]; then
echo '0.5.2-dev'
exit 0
fi
# Form: zgold -b <directory...>
if [[ "${args[-b]}" = 'true' ]]; then
_print_header
echo
typeset -i exit_code=0
typeset -i total_test_cases=0
local start_time=$(date +%s)
for dir in ${args_pos}; do
. "${dir}/config.zsh"
_validate_config_file_params_are_loaded \
"${resources}" "${(k)test_cases}"
for test_case_name test_case_command in ${(kv)test_cases}; do
_run_test "${test_case_name}" "${resources}" "${test_case_command}"
exit_code+=$?
total_test_cases+=1
done
done
local end_time=$(date +%s)
echo
_print_footer \
${total_test_cases} ${exit_code} $(( ${end_time} - ${start_time} ))
# Total of failed test cases.
exit ${exit_code}
fi
# SUB COMMANDS
# Form: zgold <config-file> [<sub-command>]
if [[ -f "${args_pos[1]}" ]] . "${args_pos[1]}"
if [[ -d "${args_pos[1]}" ]] . "${args_pos[1]}/config.zsh"
## List all test cases.
function ls {
for test_case_name in ${(k)test_cases}; do
echo "${test_case_name}"
done
}
## Run all test cases or a single test case by name.
## @param $1 Test case name.
function run {
_print_header
echo
typeset -i exit_code=0
_validate_config_file_params_are_loaded \
"${resources}" "${(k)test_cases}"
# Run all tests from a config file.
if [[ -z ${1} ]]; then
local start_time=$(date +%s)
for test_case_name test_case_command in ${(kv)test_cases}; do
_run_test "${test_case_name}" "${resources}" "${test_case_command}"
exit_code+=$?
done
local end_time=$(date +%s)
echo
_print_footer ${#test_cases} ${exit_code} $(( ${end_time} - ${start_time} ))
# Total of failed test cases.
exit ${exit_code}
fi
# Run a single test from a config file.
local start_time=$(date +%s)
_run_test "${1}" "${resources}" "${test_cases[${1}]}"
exit_code+=$?
local end_time=$(date +%s)
echo
_print_footer 1 ${exit_code} $(( ${end_time} - ${start_time} ))
# Total of failed test cases.
exit ${exit_code}
}
## Patch all test cases or a single test case by name.
## @param $1 Test case name.
function patch {
if [[ -z ${1} ]]; then
for test_case_name test_case_cmd in ${(kv)test_cases}; do
_patch_test "${test_case_name}" "${resources}" "${test_case_cmd}"
done
exit 0
fi
_patch_test "${1}" "${resources}" "${test_cases[${1}]}"
}
## Run the command for a single test case by name.
## @param $1 Test case name.
function exec {
local cmd="${test_cases[${1}]}"
local cmd_output="$(eval ${cmd})"
# Apply hooks.
if [[ -n ${pre_diff_hooks} ]] && [[ -n "${pre_diff_hooks[${1}]}" ]]; then
cmd_output="$(eval "echo \"${cmd_output}\" | ${pre_diff_hooks[${1}]}")"
fi
echo "${cmd_output}"
}
cmd="${args_pos[2,-1]}"
# Default sub-command: `run`.
if [[ ${#args_pos} -eq 1 ]] cmd='run'
# Execute sub-comand.
eval "${cmd}"