System tests: assert(): friendlier failure messages

...safer, too: the big change is using 'mapfile' to split
multiline strings; this preserves empty lines, making it
easy to see spurious (or missing) blank lines in output.

Another change is to indent the expected-output string
consistently, for readability.

Then, to handle \r (CR) and other control characters, use
bash %q to format special chars. But %q makes\ it\ hard\ to
read\ lines\ with\ spaces, so strip off those backslashes.
This makes assert() much larger and uglier, but this is
code that shouldn't be touched often.

Finally, because these are big changes to critical code,
write a complicated regression test suite for assert().

Signed-off-by: Ed Santiago <santiago@redhat.com>
This commit is contained in:
Ed Santiago 2023-02-16 08:36:54 -07:00
parent 71f3e9834b
commit c81fbd5d0a
2 changed files with 180 additions and 5 deletions

View File

@ -629,15 +629,33 @@ function assert() {
# This is a multi-line message, which may in turn contain multi-line
# output, so let's format it ourself to make it more readable.
local expect_split
mapfile -t expect_split <<<"$expect_string"
local actual_split
IFS=$'\n' read -rd '' -a actual_split <<<"$actual_string" || true
mapfile -t actual_split <<<"$actual_string"
# bash %q is really nice, except for the way it backslashes spaces
local -a expect_split_q
for line in "${expect_split[@]}"; do
local q=$(printf "%q" "$line" | sed -e 's/\\ / /g')
expect_split_q+=("$q")
done
local -a actual_split_q
for line in "${actual_split[@]}"; do
local q=$(printf "%q" "$line" | sed -e 's/\\ / /g')
actual_split_q+=("$q")
done
printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2
printf "#| FAIL: %s\n" "$testname" >&2
printf "#| expected: %s'%s'\n" "$op" "$expect_string" >&2
printf "#| actual: %s'%s'\n" "$ws" "${actual_split[0]}" >&2
printf "#| expected: %s%s\n" "$op" "${expect_split_q[0]}" >&2
local line
for line in "${actual_split[@]:1}"; do
printf "#| > %s'%s'\n" "$ws" "$line" >&2
for line in "${expect_split_q[@]:1}"; do
printf "#| > %s%s\n" "$ws" "$line" >&2
done
printf "#| actual: %s%s\n" "$ws" "${actual_split_q[0]}" >&2
for line in "${actual_split_q[@]:1}"; do
printf "#| > %s%s\n" "$ws" "$line" >&2
done
printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2
false

View File

@ -242,5 +242,162 @@ done < <(parse_table "$table")
# END ipv6_to_procfs
###############################################################################
# BEGIN check_assert
#
# This is way, way more complicated than it should be. The purpose is
# to generate readable error messages should any of the tests ever fail.
#
# Args: the last one is "" (expect to pass) or non-"" (expect that as msg).
# All other args are what we feed to assert()
function check_assert() {
local argv=("$@")
testnum=$(expr $testnum + 1)
# Final arg: "" to expect pass, anything else is expected error message
local expect="${argv[-1]}"
unset 'argv[-1]'
# Descriptive test name. If multiline, use sed to make the rest '[...]'
local testname="assert ${argv[*]}"
testname="$(sed -z -e 's/[\r\n].\+/ [...]/' <<<"$testname")"
# HERE WE GO. This is the actual test.
actual=$(assert "${argv[@]}" 2>&1)
status=$?
# Now compare actual to expect.
if [[ -z "$expect" ]]; then
# expect: pass
if [[ $status -eq 0 ]]; then
# got: pass
echo "ok $testnum $testname"
else
# got: fail
echo "not ok $testnum $testname"
echo "# expected success; got:"
local -a actual_split
IFS=$'\n' read -rd '' -a actual_split <<<"$actual" || true
if [[ "${actual_split[0]}" =~ 'vvvvv' ]]; then
unset 'actual_split[0]'
unset 'actual_split[1]'
unset 'actual_split[-1]'
actual_split=("${actual_split[@]}")
fi
for line in "${actual_split[@]}"; do
echo "# $line"
done
rc=1
fi
else
# expect: fail
if [[ $status -eq 0 ]]; then
# got: pass
echo "not ok $testnum $testname"
echo "# expected it to fail, but it passed"
rc=1
else
# Expected failure, got failure. But is it the desired failure?
# Split what we got into lines, and remove the top/bottom borders
local -a actual_split
IFS=$'\n' read -rd '' -a actual_split <<<"$actual" || true
if [[ "${actual_split[0]}" =~ 'vvvvv' ]]; then
unset 'actual_split[0]'
unset 'actual_split[1]'
unset 'actual_split[-1]'
actual_split=("${actual_split[@]}")
fi
# Split the expect string into lines, and remove first if empty
local -a expect_split
IFS=$'\n' read -rd '' -a expect_split <<<"$expect" || true
if [[ -z "${expect_split[0]}" ]]; then
unset 'expect_split[0]'
expect_split=("${expect_split[@]}")
fi
if [[ "${actual_split[*]}" = "${expect_split[*]}" ]]; then
# Yay.
echo "ok $testnum $testname"
else
# Nope. Mismatch between actual and expected output
echo "not ok $testnum $testname"
rc=1
# Ugh, this is complicated. Try to produce a useful err msg.
local n_e=${#expect_split[*]}
local n_a=${#actual_split[*]}
local n_max=${n_e}
if [[ $n_max -lt $n_a ]]; then
n_max=${n_a}
fi
printf "# %-35s | actual\n" "expect"
printf "# ----------------------------------- | ------\n"
for i in $(seq 0 $((${n_max}-1))); do
local e="${expect_split[$i]}"
local a="${actual_split[$i]}"
local same=' '
local eq='='
if [[ "$e" != "$a" ]]; then
same='!'
eq='|'
fi
printf "# %s %-35s %s %s\n" "$same" "$e" "$eq" "$a"
done
fi
fi
fi
}
# Positive tests
check_assert "a" = "a" ""
check_assert "abc" =~ "a" ""
check_assert "abc" =~ "b" ""
check_assert "abc" =~ "c" ""
check_assert "abc" =~ "a.*c" ""
check_assert "a" != "b" ""
# Simple Failure tests
check_assert "a" = "b" "
#| expected: = b
#| actual: a"
# This is the one that triggered #17509
expect="abcd efg
hijk lmnop"
actual="abcd efg
hijk lmnop"
check_assert "$actual" = "$expect" "
#| expected: = abcd efg
#| > hijk lmnop
#| actual: abcd efg
#| > ''
#| > hijk lmnop"
# Undesired carriage returns
cr=$'\r'
expect="this is line 1
this is line 2"
actual="this is line 1$cr
this is line 2$cr"
check_assert "$actual" = "$expect" "
#| expected: = this is line 1
#| > this is line 2
#| actual: \$'this is line 1\r'
#| > \$'this is line 2\r'"
# Anchored expressions; the 2nd and 3rd are 15 and 17 characters, not 16
check_assert "0123456789abcdef" =~ "^[0-9a-f]{16}\$" ""
check_assert "0123456789abcde" =~ "^[0-9a-f]{16}\$" "
#| expected: =~ \^\[0-9a-f\]\{16\}\\$
#| actual: 0123456789abcde"
check_assert "0123456789abcdeff" =~ "^[0-9a-f]{16}\$" "
#| expected: =~ \^\[0-9a-f\]\{16\}\\$
#| actual: 0123456789abcdeff"
# END check_assert
###############################################################################
exit $rc