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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
## Unreleased

### Added
- Add `assert_have_been_called_nth_with` assertion for verifying arguments on the Nth invocation of a spy (Issue #172)
- Add `assert_have_been_called_nth_with` for verifying arguments on the Nth invocation of a spy
- Add `assert_string_matches_format` and `assert_string_not_matches_format` with format placeholders (`%d`, `%s`, `%f`, `%i`, `%x`, `%e`, `%%`)

### Changed
- Split Windows CI test jobs into parallel chunks to avoid timeouts
Expand Down
49 changes: 49 additions & 0 deletions docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,36 @@ function test_failure() {
```
:::

## assert_string_matches_format
> `assert_string_matches_format "format" "value"`

Reports an error if `value` does not match the `format` string. The format string uses PHPUnit-style placeholders:

| Placeholder | Matches |
|-------------|---------|
| `%d` | One or more digits |
| `%i` | Signed integer (e.g. `+1`, `-42`) |
| `%f` | Floating point number (e.g. `3.14`) |
| `%s` | One or more non-whitespace characters |
| `%x` | Hexadecimal (e.g. `ff00ab`) |
| `%e` | Scientific notation (e.g. `1.5e10`) |
| `%%` | Literal `%` character |

- [assert_string_not_matches_format](#assert-string-not-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
assert_string_matches_format "%d items found" "42 items found"
assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each"
}

function test_failure() {
assert_string_matches_format "%d items" "hello world"
}
```
:::

## assert_line_count
> `assert_line_count "count" "haystack"`

Expand Down Expand Up @@ -957,6 +987,25 @@ function test_failure() {
```
:::

## assert_string_not_matches_format
> `assert_string_not_matches_format "format" "value"`

Reports an error if `value` matches the `format` string. See [assert_string_matches_format](#assert-string-matches-format) for supported placeholders.

- [assert_string_matches_format](#assert-string-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
assert_string_not_matches_format "%d items" "hello world"
}

function test_failure() {
assert_string_not_matches_format "%d items" "42 items"
}
```
:::

## assert_array_not_contains
> `assert_array_not_contains "needle" "haystack"`

Expand Down
29 changes: 29 additions & 0 deletions docs/test-doubles.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,35 @@ function test_failure() {
:::


## assert_have_been_called_nth_with
> `assert_have_been_called_nth_with "nth" "spy" "expected"`

Reports an error if the `nth` invocation of `spy` was not called with `expected`. The index starts at 1. Reports an error if `spy` was called fewer than `nth` times.

::: code-group
```bash [Example]
function test_success() {
bashunit::spy ps

ps first
ps second
ps third

assert_have_been_called_nth_with 1 ps "first"
assert_have_been_called_nth_with 2 ps "second"
assert_have_been_called_nth_with 3 ps "third"
}

function test_failure() {
bashunit::spy ps

ps first

assert_have_been_called_nth_with 1 ps "wrong"
}
```
:::

## assert_have_been_called_times
> assert_have_been_called_times "expected" "spy"

Expand Down
86 changes: 86 additions & 0 deletions src/assert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,89 @@ function assert_line_count() {

bashunit::state::add_assertions_passed
}

function bashunit::format_to_regex() {
local format="$1"
local regex=""
local i=0
local len=${#format}

while [ $i -lt "$len" ]; do
local char="${format:$i:1}"
if [ "$char" = "%" ] && [ $((i + 1)) -lt "$len" ]; then
local next="${format:$((i + 1)):1}"
case "$next" in
d) regex="${regex}[0-9]+" ;;
i) regex="${regex}[+-]?[0-9]+" ;;
f) regex="${regex}[+-]?[0-9]*\\.?[0-9]+" ;;
s) regex="${regex}[^ ]+" ;;
x) regex="${regex}[0-9a-fA-F]+" ;;
e) regex="${regex}[+-]?[0-9]*\\.?[0-9]+[eE][+-]?[0-9]+" ;;
%) regex="${regex}%" ;;
*)
regex="${regex}%${next}"
;;
esac
i=$((i + 2))
else
case "$char" in
. | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$')
regex="${regex}\\${char}"
;;
\\)
regex="${regex}\\\\"
;;
*)
regex="${regex}${char}"
;;
esac
i=$((i + 1))
fi
done

printf '%s' "^${regex}$"
}

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0

local format="$1"
local actual="$2"

local regex
regex="$(bashunit::format_to_regex "$format")"

if ! [[ "$actual" =~ $regex ]]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${actual}" "to match format" "${format}"
return
fi

bashunit::state::add_assertions_passed
}

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0

local format="$1"
local actual="$2"

local regex
regex="$(bashunit::format_to_regex "$format")"

if [[ "$actual" =~ $regex ]]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${actual}" "to not match format" "${format}"
return
fi

bashunit::state::add_assertions_passed
}
52 changes: 52 additions & 0 deletions tests/unit/assert_string_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,55 @@ function test_assert_string_start_end_with_special_chars_fail() {
"Assert string start end with special chars fail" "fooX" "to end with" ".bar")" \
"$(assert_string_ends_with ".bar" "fooX")"
}

function test_successful_assert_string_matches_format_with_digit() {
assert_empty "$(assert_string_matches_format "%d items found" "42 items found")"
}

function test_successful_assert_string_matches_format_with_string() {
assert_empty "$(assert_string_matches_format "Hello %s" "Hello world")"
}

function test_successful_assert_string_matches_format_with_hex() {
assert_empty "$(assert_string_matches_format "Color: %x" "Color: ff00ab")"
}

function test_successful_assert_string_matches_format_with_float() {
assert_empty "$(assert_string_matches_format "Value: %f" "Value: 3.14")"
}

function test_successful_assert_string_matches_format_with_signed_integer() {
assert_empty "$(assert_string_matches_format "Offset: %i" "Offset: -42")"
}

function test_successful_assert_string_matches_format_with_scientific() {
assert_empty "$(assert_string_matches_format "Result: %e" "Result: 1.5e10")"
}

function test_successful_assert_string_matches_format_with_literal_percent() {
assert_empty "$(assert_string_matches_format "100%% done" "100% done")"
}

function test_successful_assert_string_matches_format_with_multiple_placeholders() {
assert_empty "$(assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each")"
}

function test_unsuccessful_assert_string_matches_format() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert string matches format" \
"hello world" "to match format" "%d items")" \
"$(assert_string_matches_format "%d items" "hello world")"
}

function test_successful_assert_string_not_matches_format() {
assert_empty "$(assert_string_not_matches_format "%d items" "hello world")"
}

function test_unsuccessful_assert_string_not_matches_format() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert string not matches format" \
"42 items" "to not match format" "%d items")" \
"$(assert_string_not_matches_format "%d items" "42 items")"
}
Loading