Repository navigation
Expand file tree
/
Copy pathsteps.bash
More file actions
329 lines (278 loc) · 10.7 KB
/
Copy pathsteps.bash
File metadata and controls
329 lines (278 loc) · 10.7 KB
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
#!/usr/bin/env bash
##
# @file
# Step runner for sequences of mocked command and output assertions.
#
##
# Sets up and processes a sequence of string and mocked command assertions.
#
# Steps that mock commands need two calls: the 'setup' phase creates the mocks,
# and the 'assert' phase checks how they were called. Steps that only assert on
# output need the 'assert' phase alone.
#
# declare -a STEPS=( ... )
# mocks="$(steps_run "setup")"
# # ... code to be tested ...
# steps_run "assert" "${mocks}"
#
# Each step takes one of four forms:
#
# @<command> [<args>] # <status> [ # <output> [ # <side_effect> ]]
# Mocks <command>. The status may be omitted and the output given in its
# place, unless that output is all digits, which parses as a status. The
# side effect is Bash code run when the mock is called. A command may be
# mocked by several steps; every call goes through the same mock. The step
# is one line: literal '#' characters in <args> are escaped as '\#', and
# <output> expands backslash escapes, so '\n' gives a multi-line response.
#
# <substring>
# Asserts that the output contains <substring>.
#
# - <substring>
# Asserts that the output does not contain <substring>.
#
# = <call>
# Adds <call> to the expected call sequence, in the serialisation
# '_mock_log_quote' documents. All such steps together are the complete
# ordered sequence of mocked calls.
#
# See docs/steps.md for worked examples.
#
# Arguments:
# 1. phase: Either 'setup' or 'assert'. Optional, defaults to 'assert'.
# 2. mocked_commands: Mocks returned by the 'setup' phase. Required for the
# 'assert' phase.
#
# Globals:
# STEPS: Array of steps to process.
# BATS_HELPERS_STEPS_DEBUG: Set to '1' to enable debug output.
# RUN_STEPS_DEBUG: Deprecated name of the above.
# BATS_HELPERS_DEPRECATION_QUIET: Non-empty suppresses this library's
# deprecation notices.
#
# Outputs:
# STDOUT: The created mocks, in the 'setup' phase only.
##
steps_run() {
local PHASE_SETUP="setup"
local PHASE_ASSERT="assert"
local phase="${1:-${PHASE_ASSERT}}"
local mocked_commands_var="${2-}"
if [ -z "${BATS_HELPERS_STEPS_DEBUG-}" ] && [ -n "${RUN_STEPS_DEBUG-}" ]; then
[ -n "${BATS_HELPERS_DEPRECATION_QUIET-}" ] || echo "Deprecated: 'RUN_STEPS_DEBUG' will be removed in v2.1. Use 'BATS_HELPERS_STEPS_DEBUG' instead." >&3
fi
if [ -z "${STEPS+x}" ]; then
flunk "STEPS array is empty."
return 1
fi
declare -A command_indices
declare -A mocked_commands
_steps_debug "Phase : ${phase}"
_steps_debug "Total steps : ${#STEPS[@]}"
_steps_debug
if [ -n "${mocked_commands_var}" ]; then
local line
while IFS= read -r line; do
local key="${line%%=*}"
local value="${line#*=}"
mocked_commands["${key}"]="${value}"
done <<<"${mocked_commands_var}"
fi
local -a expected_calls=()
local mock
local command_index
local i
for ((i = 0; i < ${#STEPS[@]}; i++)); do
local item="${STEPS[i]}"
_steps_debug "STEP START: '${item}'"
##
## Command.
##
if [[ ${item} == "@"* ]]; then
_steps_debug "Type: command"
_steps_debug
##
## Parsing the command, status, and optional output.
##
_steps_debug_sub "PARSE: STARTED"
# A step is split with 'read', which stops at the first newline, so a step
# holding one would lose its status, output and side effect in silence.
if [[ ${item} == *$'\n'* ]]; then
flunk "A command step must be a single line. Continue a long step with '\\' and write a newline in the output as '\\n'."
return 1
fi
local ESCAPED_HASH_PLACEHOLDER="__ESCAPED_HASH__"
local item_with_placeholders="${item//\\#/${ESCAPED_HASH_PLACEHOLDER}}"
if [[ ${item_with_placeholders} =~ (##) || $(echo "${item_with_placeholders}" | grep -o "#" | wc -l) -gt 3 ]]; then
flunk "The string should not contain consecutive '##' and should have a maximum of three '#' characters in total."
return 1
fi
local command_parts
IFS='#' read -ra command_parts <<<"${item_with_placeholders}"
command_parts=("${command_parts[@]/# /}")
command_parts=("${command_parts[@]/% /}")
local full_command
IFS=' ' read -ra full_command <<<"${command_parts[0]:1}" # Removing '@'.
local command_binary="${full_command[0]}"
local command_args="${full_command[*]:1}" # Extract all elements except the first one.
local mock_status="${command_parts[1]-}"
local mock_output="${command_parts[2]-}"
local mock_side_effect="${command_parts[3]-}"
command_args="${command_args//${ESCAPED_HASH_PLACEHOLDER}/#}"
mock_status="${mock_status//${ESCAPED_HASH_PLACEHOLDER}/#}"
mock_output="${mock_output//${ESCAPED_HASH_PLACEHOLDER}/#}"
mock_side_effect="${mock_side_effect//${ESCAPED_HASH_PLACEHOLDER}/#}"
if ! [[ ${mock_status} =~ ^[0-9]+$ ]]; then
_steps_debug_sub "PARSE: Converting output to '${mock_status}' output."
_steps_debug_sub "PARSE: Setting status to '0'."
mock_output="${mock_status}"
mock_status=0
fi
# A step is one line, so an escape is the only way to write a newline in
# the output. The arguments and the side effect stay as written.
printf -v mock_output '%b' "${mock_output}"
_steps_debug_sub "PARSE: FINISHED"
_steps_debug_sub " command : '${command_binary}'"
_steps_debug_sub " args : '${command_args}'"
_steps_debug_sub " status : '${mock_status}'"
_steps_debug_sub " output : '${mock_output}'"
_steps_debug_sub " side_effect : '${mock_side_effect}'"
##
## Processing the command.
##
command_index=${command_indices["${command_binary}"]:-1}
_steps_debug_sub "Command index for '${command_binary}' is '${command_index}'."
if [ "${phase}" = "${PHASE_SETUP}" ]; then
if [ -z "${mocked_commands["${command_binary}"]-}" ]; then
mock="$(mock_command "${command_binary}")"
mocked_commands["${command_binary}"]=${mock}
_steps_debug_sub "SETUP: Created new mock for '${command_binary}' with value '${mocked_commands["${command_binary}"]}'."
else
mock="${mocked_commands["${command_binary}"]}"
_steps_debug_sub "SETUP: Using existing mock for '${command_binary}' with value '${mocked_commands["${command_binary}"]}'."
fi
_steps_debug_sub "SETUP: Setting mock status to '${mock_status}'."
mock_set_status "${mock}" "${mock_status}" "${command_index}"
if [ -n "${mock_output}" ]; then
_steps_debug_sub "SETUP: Setting mock output to '${mock_output}'."
mock_set_output "${mock}" "${mock_output}" "${command_index}"
fi
if [ -n "${mock_side_effect}" ]; then
_steps_debug_sub "SETUP: Setting mock side effect to '${mock_side_effect}'."
mock_set_side_effect "${mock}" "${mock_side_effect}" "${command_index}"
fi
_steps_debug_sub "SETUP: Setup mock for binary '${command_binary}' complete."
else
if [ -z "${mocked_commands["${command_binary}"]-}" ]; then
flunk "Mock for the binary '${command_binary}' does not exist."
return 1
fi
_steps_debug_sub "ASSERT: Found mock for '${command_binary}' with value '${mocked_commands["${command_binary}"]}'"
local mock_args_actual
mock="${mocked_commands["${command_binary}"]}"
_steps_debug_sub " command : ${command_binary}"
_steps_debug_sub " args : ${command_args}"
_steps_debug_sub " mock : ${mock}"
_steps_debug_sub " index : ${command_index}"
if ! mock_args_actual="$(mock_get_call_args "${mock}" "${command_index}" 2>/dev/null)"; then
flunk "Mocked command '${command_binary}' was expected to be called at least '${command_index}' time(s), but was called fewer times."
return 1
fi
_steps_debug_sub " actual args : ${mock_args_actual}"
if ! mock_assert_call_args "${mock}" "${command_args}" "${command_index}"; then
flunk "Mocked command '${command_binary}' was called with arguments '${mock_args_actual}', but '${command_args}' was expected."
return 1
fi
fi
command_indices["${command_binary}"]=$((command_index + 1))
_steps_debug "Updated command index for '${command_binary}' to '${command_indices["${command_binary}"]}'"
##
## String absent.
##
elif [[ ${item} == "-"* ]]; then
_steps_debug "Type: string absent"
if [ "${phase}" = "${PHASE_ASSERT}" ]; then
assert_output_not_contains "${item:2}" || return 1 # Skip '-' and a space.
fi
##
## Expected call.
##
elif [[ ${item} == "= "* ]]; then
_steps_debug "Type: expected call"
if [ "${phase}" = "${PHASE_ASSERT}" ]; then
expected_calls+=("${item:2}") # Skip '=' and a space.
fi
##
## String present.
##
else
_steps_debug "Type: string present"
if [ "${phase}" = "${PHASE_ASSERT}" ]; then
assert_output_contains "${item}" || return 1
fi
fi
_steps_debug "STEP FINISH: '${item}'"
_steps_debug
done
local key
if [ "${phase}" = "${PHASE_SETUP}" ]; then
local mc_string=""
for key in "${!mocked_commands[@]}"; do
mc_string+="${key}=${mocked_commands["${key}"]}"$'\n'
done
printf '%s\n' "${mc_string}"
return 0
fi
if [ "${#expected_calls[@]}" -gt 0 ]; then
mock_assert_calls "${expected_calls[@]}" || return 1
fi
local -a owned=()
for key in "${!mocked_commands[@]}"; do
owned+=("${mocked_commands["${key}"]}")
done
if [ "${#owned[@]}" -gt 0 ]; then
mock_verify "${owned[@]}" || return 1
fi
}
##
# Prints a debug message for a step.
#
# Arguments:
# 1. message: Message to print. Optional.
##
_steps_debug() {
_steps_debug_write " > " "${1-}"
}
##
# Prints a debug message for a sub-step.
#
# Arguments:
# 1. message: Message to print. Optional.
##
_steps_debug_sub() {
_steps_debug_write " > " "${1-}"
}
##
# Prints a debug message to file descriptor 3, shown by 'bats --tap'.
#
# Arguments:
# 1. prefix: Prefix to print before the message.
# 2. message: Message to print.
#
# Globals:
# BATS_HELPERS_STEPS_DEBUG: Set to '1' to enable debug output.
# RUN_STEPS_DEBUG: Deprecated name of the above.
##
_steps_debug_write() {
local debug="${BATS_HELPERS_STEPS_DEBUG:-${RUN_STEPS_DEBUG-}}"
if [ "${debug}" = "1" ]; then
echo "${1}${2}" >&3
fi
}
##
## Deprecated aliases, removed in v2.1.
##
run_steps() {
[ -n "${BATS_HELPERS_DEPRECATION_QUIET-}" ] || echo "Deprecated: 'run_steps' will be removed in v2.1. Use 'steps_run' instead." >&3
steps_run "$@"
}