Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Assert functions accept an optional trailing label to override the failure title (#77)
- `--fail-on-risky` flag and `BASHUNIT_FAIL_ON_RISKY` env var treat no-assertion tests as failures (#115)
- `--log-gha <file>` flag and `BASHUNIT_LOG_GHA` env var emit GitHub Actions workflow commands so failed, risky and incomplete tests show up as inline PR annotations (#280)
- `assert_exec` accepts `--stdin`, `--stdout-contains`, `--stdout-not-contains`, `--stderr-contains` and `--stderr-not-contains` flags to test interactive prompt commands and substring output (#301)

### Changed
- Parallel test execution is now enabled on Alpine Linux (#370)
Expand Down
25 changes: 24 additions & 1 deletion docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,18 @@ function test_failure() {
:::

## assert_exec
> `assert_exec "command" [--exit <code>] [--stdout "text"] [--stderr "text"]`
> `assert_exec "command" [--exit <code>] [--stdout "text"] [--stderr "text"] [--stdout-contains "needle"] [--stdout-not-contains "needle"] [--stderr-contains "needle"] [--stderr-not-contains "needle"] [--stdin "input"]`

Runs `command` capturing its exit status, standard output and standard error and
checks all provided expectations. When `--exit` is omitted the expected exit
status defaults to `0`.

Use `--stdin` to feed input into interactive commands (e.g. commands using
`read`). Multiple answers can be passed by separating them with newlines.

Use `--stdout-contains` / `--stdout-not-contains` (and the `stderr-*` variants)
for substring matching when you don't want to assert against the full output.

::: code-group
```bash [Example]
function sample() {
Expand All @@ -535,6 +541,23 @@ function test_failure() {
assert_exec sample --exit 0 --stdout "out" --stderr "err"
}
```

```bash [Interactive]
function question() {
local name lang
read -r name
read -r lang
echo "Your name is $name and you prefer $lang."
}

function test_interactive_prompt() {
assert_exec question \
--stdin "Chemaclass"$'\n'"Phel-Lang"$'\n' \
--stdout-contains "Your name is Chemaclass and you prefer Phel-Lang." \
--stdout-not-contains "Delphi" \
--exit 0
}
```
:::

## assert_array_contains
Expand Down
82 changes: 80 additions & 2 deletions src/assert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,18 @@ function assert_exec() {
local expected_exit=0
local expected_stdout=""
local expected_stderr=""
local stdout_needle=""
local stdout_no_needle=""
local stderr_needle=""
local stderr_no_needle=""
local stdin_input=""
local check_stdout=false
local check_stderr=false
local check_stdout_contains=false
local check_stdout_not_contains=false
local check_stderr_contains=false
local check_stderr_not_contains=false
local check_stdin=false

while [ $# -gt 0 ]; do
case "$1" in
Expand All @@ -399,6 +409,31 @@ function assert_exec() {
check_stderr=true
shift 2
;;
--stdout-contains)
stdout_needle="$2"
check_stdout_contains=true
shift 2
;;
--stdout-not-contains)
stdout_no_needle="$2"
check_stdout_not_contains=true
shift 2
;;
--stderr-contains)
stderr_needle="$2"
check_stderr_contains=true
shift 2
;;
--stderr-not-contains)
stderr_no_needle="$2"
check_stderr_not_contains=true
shift 2
;;
--stdin)
stdin_input="$2"
check_stdin=true
shift 2
;;
*)
shift
;;
Expand All @@ -409,8 +444,17 @@ function assert_exec() {
stdout_file=$("$MKTEMP")
stderr_file=$("$MKTEMP")

eval "$cmd" >"$stdout_file" 2>"$stderr_file"
local exit_code=$?
if $check_stdin; then
local stdin_file
stdin_file=$("$MKTEMP")
printf '%s' "$stdin_input" >"$stdin_file"
eval "$cmd" <"$stdin_file" >"$stdout_file" 2>"$stderr_file"
local exit_code=$?
rm -f "$stdin_file"
else
eval "$cmd" >"$stdout_file" 2>"$stderr_file"
local exit_code=$?
fi

local stdout
stdout=$(cat "$stdout_file")
Expand All @@ -435,6 +479,23 @@ function assert_exec() {
fi
fi

if $check_stdout_contains; then
expected_desc="$expected_desc"$'\n'"stdout contains: $stdout_needle"
actual_desc="$actual_desc"$'\n'"stdout: $stdout"
case "$stdout" in
*"$stdout_needle"*) ;;
*) failed=1 ;;
esac
fi

if $check_stdout_not_contains; then
expected_desc="$expected_desc"$'\n'"stdout not contains: $stdout_no_needle"
actual_desc="$actual_desc"$'\n'"stdout: $stdout"
case "$stdout" in
*"$stdout_no_needle"*) failed=1 ;;
esac
fi

if $check_stderr; then
expected_desc="$expected_desc"$'\n'"stderr: $expected_stderr"
actual_desc="$actual_desc"$'\n'"stderr: $stderr"
Expand All @@ -443,6 +504,23 @@ function assert_exec() {
fi
fi

if $check_stderr_contains; then
expected_desc="$expected_desc"$'\n'"stderr contains: $stderr_needle"
actual_desc="$actual_desc"$'\n'"stderr: $stderr"
case "$stderr" in
*"$stderr_needle"*) ;;
*) failed=1 ;;
esac
fi

if $check_stderr_not_contains; then
expected_desc="$expected_desc"$'\n'"stderr not contains: $stderr_no_needle"
actual_desc="$actual_desc"$'\n'"stderr: $stderr"
case "$stderr" in
*"$stderr_no_needle"*) failed=1 ;;
esac
fi

if [ "$failed" -eq 1 ]; then
local label
label="$(bashunit::assert::label "${label_override:-}")"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,18 @@ are more semantic versions of this assertion, for which you don't need to specif

## assert_exec
--------------
> `assert_exec "command" --exit <code> --stdout "text" --stderr "text"`
> `assert_exec "command" --exit <code> --stdout "text" --stderr "text" --stdout-contains "needle" --stdout-not-contains "needle" --stderr-contains "needle" --stderr-not-contains "needle" --stdin "input"`

Runs `command` capturing its exit status, standard output and standard error and
checks all provided expectations. When `--exit` is omitted the expected exit
status defaults to `0`.

Use `--stdin` to feed input into interactive commands (e.g. commands using
`read`). Multiple answers can be passed by separating them with newlines.

Use `--stdout-contains` / `--stdout-not-contains` (and the `stderr-*` variants)
for substring matching when you don't want to assert against the full output.


## assert_array_contains
--------------
Expand Down
127 changes: 127 additions & 0 deletions tests/unit/assert_advanced_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,130 @@ function test_assert_line_count_does_not_modify_existing_variable() {
assert_empty "$(assert_line_count 1 "one")"
assert_same "original" "$additional_new_lines"
}

function test_successful_assert_exec_with_stdin() {
# shellcheck disable=SC2317
function prompt_command() {
local name lang
read -r name
read -r lang
echo "Your name is $name and you prefer $lang."
}

assert_empty "$(assert_exec prompt_command \
--stdin "Chemaclass"$'\n'"Phel-Lang"$'\n' \
--stdout "Your name is Chemaclass and you prefer Phel-Lang." \
--exit 0)"
}

function test_successful_assert_exec_stdout_contains() {
# shellcheck disable=SC2317
function greet_command() {
echo "Hello, World! Welcome to bashunit."
}

assert_empty "$(assert_exec greet_command --stdout-contains "bashunit")"
}

function test_unsuccessful_assert_exec_stdout_contains() {
# shellcheck disable=SC2317
function greet_command() {
echo "Hello, World!"
}

local expected="exit: 0"$'\n'"stdout contains: bashunit"
local actual="exit: 0"$'\n'"stdout: Hello, World!"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert exec stdout contains" "$expected" "but got " "$actual")" \
"$(assert_exec greet_command --stdout-contains "bashunit")"
}

function test_successful_assert_exec_stdout_not_contains() {
# shellcheck disable=SC2317
function greet_command() {
echo "Hello, World!"
}

assert_empty "$(assert_exec greet_command --stdout-not-contains "Delphi")"
}

function test_unsuccessful_assert_exec_stdout_not_contains() {
# shellcheck disable=SC2317
function greet_command() {
echo "Hello, Delphi lovers!"
}

local expected="exit: 0"$'\n'"stdout not contains: Delphi"
local actual="exit: 0"$'\n'"stdout: Hello, Delphi lovers!"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert exec stdout not contains" "$expected" "but got " "$actual")" \
"$(assert_exec greet_command --stdout-not-contains "Delphi")"
}

function test_successful_assert_exec_stderr_contains() {
# shellcheck disable=SC2317
function warn_command() {
echo "warning: low disk" >&2
}

assert_empty "$(assert_exec warn_command --stderr-contains "low disk")"
}

function test_unsuccessful_assert_exec_stderr_contains() {
# shellcheck disable=SC2317
function warn_command() {
echo "ok" >&2
}

local expected="exit: 0"$'\n'"stderr contains: failure"
local actual="exit: 0"$'\n'"stderr: ok"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert exec stderr contains" "$expected" "but got " "$actual")" \
"$(assert_exec warn_command --stderr-contains "failure")"
}

function test_successful_assert_exec_stderr_not_contains() {
# shellcheck disable=SC2317
function warn_command() {
echo "ok" >&2
}

assert_empty "$(assert_exec warn_command --stderr-not-contains "error")"
}

function test_unsuccessful_assert_exec_stderr_not_contains() {
# shellcheck disable=SC2317
function warn_command() {
echo "fatal error" >&2
}

local expected="exit: 0"$'\n'"stderr not contains: error"
local actual="exit: 0"$'\n'"stderr: fatal error"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert exec stderr not contains" "$expected" "but got " "$actual")" \
"$(assert_exec warn_command --stderr-not-contains "error")"
}

function test_successful_assert_exec_interactive_prompt_flow() {
# shellcheck disable=SC2317
function question_command() {
local name lang
read -r name
read -r lang
echo "Your name is $name and you prefer $lang."
}

assert_empty "$(assert_exec question_command \
--stdin "Chemaclass"$'\n'"Phel-Lang"$'\n' \
--stdout-contains "Your name is Chemaclass and you prefer Phel-Lang." \
--stdout-not-contains "Delphi" \
--exit 0)"
}
Loading