Skip to content

Commit 5bcb2b3

Browse files
committed
feat: add assert_stderr_line
Add a new assert_stderr_line function for checking expected lines in the stderr output. This improves the clarity and functionality of the assertion process.
1 parent 1f7ff83 commit 5bcb2b3

File tree

3 files changed

+544
-23
lines changed

3 files changed

+544
-23
lines changed

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ This project provides the following functions:
4040
- [assert_line](#assert_line) / [refute_line](#refute_line) Assert a specific line of output does (or does not) contain given content.
4141
- [assert_regex](#assert_regex) / [refute_regex](#refute_regex) Assert a parameter does (or does not) match given pattern.
4242
- [assert_stderr](#assert_stderr) / [refute_stderr](#refute_stderr) Assert stderr does (or does not) contain given content.
43+
- [assert_stderr_line](#assert_stderr_line) Assert a specific line of stderr does contain given content.
4344

4445
These commands are described in more detail below.
4546

@@ -897,6 +898,115 @@ Similar to `refute_output`, this function verifies that a command or function do
897898
The stderr matching can be literal (the default), partial or by regular expression.
898899
The unexpected stderr can be specified either by positional argument or read from STDIN by passing the `-`/`--stdin` flag.
899900
901+
### `assert_stderr_line`
902+
903+
> _**Note**:
904+
> `run` has to be called with `--separate-stderr` to separate stdout and stderr into `$output` and `$stderr`.
905+
> If not, `$stderr` will be empty, causing `assert_stderr_line` to always fail.
906+
907+
Similarly to `assert_stderr`, this function verifies that a command or function produces the expected stderr.
908+
It checks that the expected line appears in the stderr (default) or at a specific line number.
909+
Matching can be literal (default), partial or regular expression.
910+
This function is the logical complement of `refute_stderr_line`.
911+
912+
#### Looking for a line in the stderr
913+
914+
By default, the entire stderr is searched for the expected line.
915+
The assertion fails if the expected line is not found in `${stderr_lines[@]}`.
916+
917+
```bash
918+
echo_err() {
919+
echo "$@" >&2
920+
}
921+
922+
@test 'assert_stderr_line() looking for line' {
923+
run --separate-stderr echo_err $'have-0\nhave-1\nhave-2'
924+
assert_stderr_line 'want'
925+
}
926+
```
927+
928+
On failure, the expected line and the stderr are displayed.
929+
930+
```
931+
-- stderr does not contain line --
932+
line : want
933+
stderr (3 lines):
934+
have-0
935+
have-1
936+
have-2
937+
--
938+
```
939+
940+
#### Matching a specific line
941+
942+
When the `--index <idx>` option is used (`-n <idx>` for short), the expected line is matched only against the line identified by the given index.
943+
The assertion fails if the expected line does not equal `${stderr_lines[<idx>]}`.
944+
945+
```bash
946+
@test 'assert_stderr_line() specific line' {
947+
run --separate-stderr echo_err $'have-0\nhave-1\nhave-2'
948+
assert_stderr_line --index 1 'want-1'
949+
}
950+
```
951+
952+
On failure, the index and the compared stderr_lines are displayed.
953+
954+
```
955+
-- line differs --
956+
index : 1
957+
expected : want-1
958+
actual : have-1
959+
--
960+
```
961+
962+
#### Partial matching
963+
964+
Partial matching can be enabled with the `--partial` option (`-p` for short).
965+
When used, a match fails if the expected *substring* is not found in the matched line.
966+
967+
```bash
968+
@test 'assert_stderr_line() partial matching' {
969+
run --separate-stderr echo_err $'have 1\nhave 2\nhave 3'
970+
assert_stderr_line --partial 'want'
971+
}
972+
```
973+
974+
On failure, the same details are displayed as for literal matching, except that the substring replaces the expected line.
975+
976+
```
977+
-- no stderr line contains substring --
978+
substring : want
979+
stderr (3 lines):
980+
have 1
981+
have 2
982+
have 3
983+
--
984+
```
985+
986+
#### Regular expression matching
987+
988+
Regular expression matching can be enabled with the `--regexp` option (`-e` for short).
989+
When used, a match fails if the *extended regular expression* does not match the line being tested.
990+
991+
*Note: As expected, the anchors `^` and `$` bind to the beginning and the end (respectively) of the matched line.*
992+
993+
```bash
994+
@test 'assert_stderr_line() regular expression matching' {
995+
run --separate-stderr echo_err $'have-0\nhave-1\nhave-2'
996+
assert_stderr_line --index 1 --regexp '^want-[0-9]$'
997+
}
998+
```
999+
1000+
On failure, the same details are displayed as for literal matching, except that the regular expression replaces the expected line.
1001+
1002+
```
1003+
-- regular expression does not match line --
1004+
index : 1
1005+
regexp : ^want-[0-9]$
1006+
line : have-1
1007+
--
1008+
```
1009+
9001010
<!-- REFERENCES -->
9011011
9021012
[bats]: https://github.com/bats-core/bats-core

src/assert_line.bash

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,65 @@
128128
# ```
129129
# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
130130
assert_line() {
131+
__assert_line "$@"
132+
}
133+
134+
# assert_stderr_line
135+
# ===========
136+
#
137+
# Summary: Fail if the expected line is not found in the stderr (default) or at a specific line number.
138+
#
139+
# Usage: assert_stderr_line [-n index] [-p | -e] [--] <expected>
140+
#
141+
# Options:
142+
# -n, --index <idx> Match the <idx>th line
143+
# -p, --partial Match if `expected` is a substring of `$stderr` or line <idx>
144+
# -e, --regexp Treat `expected` as an extended regular expression
145+
# <expected> The expected line string, substring, or regular expression
146+
#
147+
# IO:
148+
# STDERR - details, on failure
149+
# error message, on error
150+
# Globals:
151+
# stderr
152+
# stderr_lines
153+
# Returns:
154+
# 0 - if matching line found
155+
# 1 - otherwise
156+
#
157+
# Similarly to `assert_stderr`, this function verifies that a command or function produces the expected stderr.
158+
# (It is the logical complement of `refute_stderr_line`.)
159+
# It checks that the expected line appears in the stderr (default) or at a specific line number.
160+
# Matching can be literal (default), partial or regular expression.
161+
#
162+
assert_stderr_line() {
163+
__assert_line "$@"
164+
}
165+
166+
__assert_line() {
167+
local -r caller=${FUNCNAME[1]}
131168
local -i is_match_line=0
132169
local -i is_mode_partial=0
133170
local -i is_mode_regexp=0
134-
: "${lines?}"
171+
172+
if [[ "${caller}" == "assert_line" ]]; then
173+
local -ar stream_lines=("${lines[@]}")
174+
local -r stream_type=output
175+
elif [[ "${caller}" == "assert_stderr_line" ]]; then
176+
local -ar stream_lines=("${stderr_lines[@]}")
177+
local -r stream_type=stderr
178+
else
179+
# Coding error: unknown caller
180+
:
181+
fi
135182

136183
# Handle options.
137184
while (( $# > 0 )); do
138185
case "$1" in
139186
-n|--index)
140187
if (( $# < 2 )) || ! [[ $2 =~ ^-?([0-9]|[1-9][0-9]+)$ ]]; then
141188
echo "\`--index' requires an integer argument: \`$2'" \
142-
| batslib_decorate 'ERROR: assert_line' \
189+
| batslib_decorate "ERROR: ${caller}" \
143190
| fail
144191
return $?
145192
fi
@@ -156,7 +203,7 @@ assert_line() {
156203

157204
if (( is_mode_partial )) && (( is_mode_regexp )); then
158205
echo "\`--partial' and \`--regexp' are mutually exclusive" \
159-
| batslib_decorate 'ERROR: assert_line' \
206+
| batslib_decorate "ERROR: ${caller}" \
160207
| fail
161208
return $?
162209
fi
@@ -166,7 +213,7 @@ assert_line() {
166213

167214
if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
168215
echo "Invalid extended regular expression: \`$expected'" \
169-
| batslib_decorate 'ERROR: assert_line' \
216+
| batslib_decorate "ERROR: ${caller}" \
170217
| fail
171218
return $?
172219
fi
@@ -175,73 +222,73 @@ assert_line() {
175222
if (( is_match_line )); then
176223
# Specific line.
177224
if (( is_mode_regexp )); then
178-
if ! [[ ${lines[$idx]} =~ $expected ]]; then
225+
if ! [[ ${stream_lines[$idx]} =~ $expected ]]; then
179226
batslib_print_kv_single 6 \
180227
'index' "$idx" \
181228
'regexp' "$expected" \
182-
'line' "${lines[$idx]}" \
229+
'line' "${stream_lines[$idx]}" \
183230
| batslib_decorate 'regular expression does not match line' \
184231
| fail
185232
fi
186233
elif (( is_mode_partial )); then
187-
if [[ ${lines[$idx]} != *"$expected"* ]]; then
234+
if [[ ${stream_lines[$idx]} != *"$expected"* ]]; then
188235
batslib_print_kv_single 9 \
189236
'index' "$idx" \
190237
'substring' "$expected" \
191-
'line' "${lines[$idx]}" \
238+
'line' "${stream_lines[$idx]}" \
192239
| batslib_decorate 'line does not contain substring' \
193240
| fail
194241
fi
195242
else
196-
if [[ ${lines[$idx]} != "$expected" ]]; then
243+
if [[ ${stream_lines[$idx]} != "$expected" ]]; then
197244
batslib_print_kv_single 8 \
198245
'index' "$idx" \
199246
'expected' "$expected" \
200-
'actual' "${lines[$idx]}" \
247+
'actual' "${stream_lines[$idx]}" \
201248
| batslib_decorate 'line differs' \
202249
| fail
203250
fi
204251
fi
205252
else
206-
# Contained in output.
253+
# Contained in output/error stream.
207254
if (( is_mode_regexp )); then
208255
local -i idx
209-
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
210-
[[ ${lines[$idx]} =~ $expected ]] && return 0
256+
for (( idx = 0; idx < ${#stream_lines[@]}; ++idx )); do
257+
[[ ${stream_lines[$idx]} =~ $expected ]] && return 0
211258
done
212259
{ local -ar single=( 'regexp' "$expected" )
213-
local -ar may_be_multi=( 'output' "$output" )
260+
local -ar may_be_multi=( "${stream_type}" "${!stream_type}" )
214261
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
215262
batslib_print_kv_single "$width" "${single[@]}"
216263
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
217264
} \
218-
| batslib_decorate 'no output line matches regular expression' \
265+
| batslib_decorate "no ${stream_type} line matches regular expression" \
219266
| fail
220267
elif (( is_mode_partial )); then
221268
local -i idx
222-
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
223-
[[ ${lines[$idx]} == *"$expected"* ]] && return 0
269+
for (( idx = 0; idx < ${#stream_lines[@]}; ++idx )); do
270+
[[ ${stream_lines[$idx]} == *"$expected"* ]] && return 0
224271
done
225272
{ local -ar single=( 'substring' "$expected" )
226-
local -ar may_be_multi=( 'output' "$output" )
273+
local -ar may_be_multi=( "${stream_type}" "${!stream_type}" )
227274
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
228275
batslib_print_kv_single "$width" "${single[@]}"
229276
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
230277
} \
231-
| batslib_decorate 'no output line contains substring' \
278+
| batslib_decorate "no ${stream_type} line contains substring" \
232279
| fail
233280
else
234281
local -i idx
235-
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
236-
[[ ${lines[$idx]} == "$expected" ]] && return 0
282+
for (( idx = 0; idx < ${#stream_lines[@]}; ++idx )); do
283+
[[ ${stream_lines[$idx]} == "$expected" ]] && return 0
237284
done
238285
{ local -ar single=( 'line' "$expected" )
239-
local -ar may_be_multi=( 'output' "$output" )
286+
local -ar may_be_multi=( "${stream_type}" "${!stream_type}" )
240287
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
241288
batslib_print_kv_single "$width" "${single[@]}"
242289
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
243290
} \
244-
| batslib_decorate 'output does not contain line' \
291+
| batslib_decorate "${stream_type} does not contain line" \
245292
| fail
246293
fi
247294
fi

0 commit comments

Comments
 (0)