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 @@ -6,6 +6,7 @@
- `--list` (alias `--dry-run`) prints the tests a run would execute, without running them; `--list-format json` emits file, function, name, line and tags. Honours every selection flag, including `--shard` and `--random-order --seed` ordering (#1007)
- `# @tags a b` above any top-level line applies those tags to every test in the file, unioned with per-function `# @tag` (#1008)
- `--tag` accepts expressions: `'a&&b'` (AND) and `'!a'` (NOT), combinable as `'a&&!b'`. Repeated `--tag` flags keep OR semantics, and `--exclude-tag` still wins (#1008)
- `assert_command_available <command>` asserts that an external command, shell builtin or function resolves through `command -v` (#1027)
- The coverage engine in use is reported by `--verbose`, and an explicit `BASHUNIT_COVERAGE_ENGINE=xtrace` that the running Bash cannot honour now warns instead of being silently ignored (#1005)

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.
It ships 75 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
2 changes: 1 addition & 1 deletion completions/_bashunit
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ _bashunit() {
assert_fns=(
assert_array_contains assert_array_length assert_array_not_contains
assert_arrays_equal assert_assertion_fails assert_assertion_fails_with
assert_assertion_passes assert_command_not_found assert_contains
assert_assertion_passes assert_command_available assert_command_not_found assert_contains
assert_contains_ignore_case assert_date_after assert_date_before
assert_date_equals assert_date_within_delta assert_date_within_range
assert_directory_exists assert_directory_not_exists assert_duration
Expand Down
2 changes: 1 addition & 1 deletion completions/bashunit.bash
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ _BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \

_BASHUNIT_COMPLETIONS_ASSERT_FNS="assert_array_contains assert_array_length \
assert_array_not_contains assert_arrays_equal assert_assertion_fails \
assert_assertion_fails_with assert_assertion_passes assert_command_not_found \
assert_assertion_fails_with assert_assertion_passes assert_command_available assert_command_not_found \
assert_contains assert_contains_ignore_case assert_date_after \
assert_date_before assert_date_equals assert_date_within_delta \
assert_date_within_range assert_directory_exists assert_directory_not_exists \
Expand Down
29 changes: 28 additions & 1 deletion docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ to narrow it (`bashunit doc json`).
| **Strings** | [assert_contains](#assert-contains) · [assert_not_contains](#assert-not-contains) · [assert_contains_ignore_case](#assert-contains-ignore-case) · [assert_matches](#assert-matches) · [assert_not_matches](#assert-not-matches) · [assert_string_starts_with](#assert-string-starts-with) · [assert_string_not_starts_with](#assert-string-not-starts-with) · [assert_string_ends_with](#assert-string-ends-with) · [assert_string_not_ends_with](#assert-string-not-ends-with) · [assert_string_matches_format](#assert-string-matches-format) · [assert_string_not_matches_format](#assert-string-not-matches-format) · [assert_empty](#assert-empty) · [assert_not_empty](#assert-not-empty) · [assert_line_count](#assert-line-count) |
| **Numbers** | [assert_less_than](#assert-less-than) · [assert_less_or_equal_than](#assert-less-or-equal-than) · [assert_greater_than](#assert-greater-than) · [assert_greater_or_equal_than](#assert-greater-or-equal-than) · [assert_within_delta](#assert-within-delta) |
| **Dates** | [assert_date_equals](#assert-date-equals) · [assert_date_before](#assert-date-before) · [assert_date_after](#assert-date-after) · [assert_date_within_range](#assert-date-within-range) · [assert_date_within_delta](#assert-date-within-delta) |
| **Exit codes and commands** | [assert_exit_code](#assert-exit-code) · [assert_successful_code](#assert-successful-code) · [assert_unsuccessful_code](#assert-unsuccessful-code) · [assert_general_error](#assert-general-error) · [assert_command_not_found](#assert-command-not-found) · [assert_exec](#assert-exec) |
| **Exit codes and commands** | [assert_exit_code](#assert-exit-code) · [assert_successful_code](#assert-successful-code) · [assert_unsuccessful_code](#assert-unsuccessful-code) · [assert_general_error](#assert-general-error) · [assert_command_available](#assert-command-available) · [assert_command_not_found](#assert-command-not-found) · [assert_exec](#assert-exec) |
| **Files** | [assert_file_exists](#assert-file-exists) · [assert_file_not_exists](#assert-file-not-exists) · [assert_file_contains](#assert-file-contains) · [assert_file_not_contains](#assert-file-not-contains) · [assert_is_file](#assert-is-file) · [assert_is_file_empty](#assert-is-file-empty) · [assert_is_symlink](#assert-is-symlink) · [assert_is_not_symlink](#assert-is-not-symlink) · [assert_symlink_to](#assert-symlink-to) · [assert_file_permissions](#assert-file-permissions) · [assert_files_equals](#assert-files-equals) · [assert_files_not_equals](#assert-files-not-equals) |
| **Directories** | [assert_directory_exists](#assert-directory-exists) · [assert_directory_not_exists](#assert-directory-not-exists) · [assert_is_directory](#assert-is-directory) · [assert_is_directory_empty](#assert-is-directory-empty) · [assert_is_directory_not_empty](#assert-is-directory-not-empty) · [assert_is_directory_readable](#assert-is-directory-readable) · [assert_is_directory_not_readable](#assert-is-directory-not-readable) · [assert_is_directory_writable](#assert-is-directory-writable) · [assert_is_directory_not_writable](#assert-is-directory-not-writable) |
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
Expand Down Expand Up @@ -832,6 +832,33 @@ function test_failure() {
```
:::

## assert_command_available
> `assert_command_available "command"`

Reports an error if `command` is not available.

Availability uses the same `command -v` check as the
[bashunit::is_command_available](/globals#bashunit-is-command-available) helper, so
external commands, shell builtins and shell functions are supported. The command
is only resolved; it is not executed.

::: code-group
```bash [Example]
function test_dependencies_are_installed() {
assert_command_available bash
assert_command_available jq
}

function test_shell_function_is_available() {
function project_build() {
make build
}

assert_command_available project_build
}
```
:::

## assert_command_not_found
> `assert_command_not_found`

Expand Down
4 changes: 2 additions & 2 deletions docs/public/bashunit-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (75 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All @@ -127,7 +127,7 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_string_starts_with`, `assert_string_ends_with`, `assert_empty`, `assert_not_empty`
- Numbers: `assert_greater_than`, `assert_less_than`, `assert_within_delta`
- Exit codes: `assert_successful_code`, `assert_general_error`, `assert_exit_code`,
`assert_command_not_found`
`assert_command_not_found`, `assert_command_available`
- Files: `assert_file_exists`, `assert_file_contains`, `assert_is_file_empty`,
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
Expand Down
26 changes: 26 additions & 0 deletions src/assert/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,32 @@ function assert_general_error() {
bashunit::state::add_assertions_passed
}

##
# Reports an error unless the command resolves through
# bashunit::is_command_available, so builtins and shell functions count as
# available exactly as they do for that helper. The command is resolved, never
# executed.
# Arguments: $1 - command, $2 - label override (optional)
##
function assert_command_available() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 1 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "command" "$#"
return 2
fi

local command="$1"
local label_override="${2:-}"

if ! bashunit::is_command_available "$command"; then
bashunit::assert::fail_with "${label_override:-}" \
"${command}" "to be available but was" "not found"
return
fi

bashunit::state::add_assertions_passed
}

function assert_command_not_found() {
local actual_exit_code=${3-"$?"} # Capture $? before guard check
local label_override=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,18 @@ Use assert_exec if you want to pass a command as a string and check its exit cod
- assert_exit_code is the full version of this assertion where you can specify the expected exit code.


## assert_command_available
--------------
> `assert_command_available "command"`

Reports an error if `command` is not available.

Availability uses the same `command -v` check as the
bashunit::is_command_available(/globals#bashunit-is-command-available) helper, so
external commands, shell builtins and shell functions are supported. The command
is only resolved; it is not executed.


## assert_command_not_found
--------------
> `assert_command_not_found`
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/assert/advanced_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,57 @@ function test_unsuccessful_assert_command_not_found() {
"$(assert_command_not_found "$(fake_function)")"
}

function test_successful_assert_command_available_for_external_command() {
assert_empty "$(assert_command_available bash)"
}

function test_successful_assert_command_available_for_builtin() {
assert_empty "$(assert_command_available printf)"
}

function test_successful_assert_command_available_for_shell_function() {
function available_shell_function() {
# shellcheck disable=SC2317 # Never invoked: the assertion only resolves it.
:
}

assert_empty "$(assert_command_available available_shell_function)"
}

function test_unsuccessful_assert_command_available_names_command() {
local command_name="bashunit_command_that_does_not_exist"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert command available names command" \
"$command_name" "to be available but was" "not found")" \
"$(assert_command_available "$command_name")"
}

function test_unsuccessful_assert_command_available_with_custom_label() {
assert_same \
"$(bashunit::console_results::print_failed_test "my custom label" \
"bashunit_command_that_does_not_exist" "to be available but was" "not found")" \
"$(assert_command_available "bashunit_command_that_does_not_exist" "my custom label")"
}

function test_assert_command_available_moves_the_assertion_counters() {
assert_assertion_passes assert_command_available printf
assert_assertion_fails assert_command_available "bashunit_command_that_does_not_exist"
}

# The two never agree about the same command. Both run inside $() so their
# failures stay in the subshell instead of moving this test's counters.
function test_assert_command_available_is_the_inverse_of_assert_command_not_found() {
# Missing: not_found passes on the 127, available fails.
assert_empty "$(assert_command_not_found "$(bashunit_command_that_does_not_exist 2>/dev/null)")"
assert_not_empty "$(assert_command_available "bashunit_command_that_does_not_exist")"

# Present: available passes, not_found fails on the 0.
assert_empty "$(assert_command_available printf)"
assert_not_empty "$(assert_command_not_found "$(printf '')")"
}

function test_successful_assert_exec() {
# shellcheck disable=SC2317
function fake_command() {
Expand Down
23 changes: 15 additions & 8 deletions tests/unit/assert/arity_test.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#!/usr/bin/env bash

# @data_provider provide_core_comparison_assertions
function test_core_comparison_assertions_reject_missing_arguments() {
# @data_provider provide_core_assertions_requiring_arguments
function test_core_assertions_reject_missing_arguments() {
local assertion=$1
local required=$2
local signature=$3
local output exit_code=0

if [ "$required" -eq 3 ]; then
output=$("$assertion" "first" "second" 2>&1) || exit_code=$?
else
output=$("$assertion" "first" 2>&1) || exit_code=$?
fi
# One argument short of the signature, whatever the arity. The `+` guard keeps
# an empty list expandable under `set -u` on Bash 3.x (a one-argument
# assertion is called with nothing at all).
local supplied=1
local -a args=()
while [ "$supplied" -lt "$required" ]; do
args[${#args[@]}]="arg${supplied}"
supplied=$((supplied + 1))
done

output=$("$assertion" "${args[@]+"${args[@]}"}" 2>&1) || exit_code=$?

assert_same 2 "$exit_code"
assert_same \
"bashunit: assertion usage error: $assertion expects $required arguments ($signature), got $((required - 1))" \
"$output"
}

function provide_core_comparison_assertions() {
function provide_core_assertions_requiring_arguments() {
bashunit::data_set assert_same 2 "expected, actual"
bashunit::data_set assert_equals 2 "expected, actual"
bashunit::data_set assert_not_same 2 "expected, actual"
Expand All @@ -41,6 +47,7 @@ function provide_core_comparison_assertions() {
bashunit::data_set assert_line_count 2 "expected, actual"
bashunit::data_set assert_string_matches_format 2 "format, actual"
bashunit::data_set assert_string_not_matches_format 2 "format, actual"
bashunit::data_set assert_command_available 1 "command"
}

function test_empty_values_still_count_as_supplied_arguments() {
Expand Down
Loading