Skip to content

Commit c038c02

Browse files
authored
Merge pull request #591 from TypedDevs/feat/assert-string-matches-format
Add assert_string_matches_format assertion
2 parents fa97cfd + d9f117a commit c038c02

5 files changed

Lines changed: 218 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
## Unreleased
44

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

89
### Changed
910
- Split Windows CI test jobs into parallel chunks to avoid timeouts

docs/assertions.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,36 @@ function test_failure() {
221221
```
222222
:::
223223

224+
## assert_string_matches_format
225+
> `assert_string_matches_format "format" "value"`
226+
227+
Reports an error if `value` does not match the `format` string. The format string uses PHPUnit-style placeholders:
228+
229+
| Placeholder | Matches |
230+
|-------------|---------|
231+
| `%d` | One or more digits |
232+
| `%i` | Signed integer (e.g. `+1`, `-42`) |
233+
| `%f` | Floating point number (e.g. `3.14`) |
234+
| `%s` | One or more non-whitespace characters |
235+
| `%x` | Hexadecimal (e.g. `ff00ab`) |
236+
| `%e` | Scientific notation (e.g. `1.5e10`) |
237+
| `%%` | Literal `%` character |
238+
239+
- [assert_string_not_matches_format](#assert-string-not-matches-format) is the inverse of this assertion and takes the same arguments.
240+
241+
::: code-group
242+
```bash [Example]
243+
function test_success() {
244+
assert_string_matches_format "%d items found" "42 items found"
245+
assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each"
246+
}
247+
248+
function test_failure() {
249+
assert_string_matches_format "%d items" "hello world"
250+
}
251+
```
252+
:::
253+
224254
## assert_line_count
225255
> `assert_line_count "count" "haystack"`
226256

@@ -957,6 +987,25 @@ function test_failure() {
957987
```
958988
:::
959989
990+
## assert_string_not_matches_format
991+
> `assert_string_not_matches_format "format" "value"`
992+
993+
Reports an error if `value` matches the `format` string. See [assert_string_matches_format](#assert-string-matches-format) for supported placeholders.
994+
995+
- [assert_string_matches_format](#assert-string-matches-format) is the inverse of this assertion and takes the same arguments.
996+
997+
::: code-group
998+
```bash [Example]
999+
function test_success() {
1000+
assert_string_not_matches_format "%d items" "hello world"
1001+
}
1002+
1003+
function test_failure() {
1004+
assert_string_not_matches_format "%d items" "42 items"
1005+
}
1006+
```
1007+
:::
1008+
9601009
## assert_array_not_contains
9611010
> `assert_array_not_contains "needle" "haystack"`
9621011

docs/test-doubles.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,35 @@ function test_failure() {
174174
:::
175175

176176

177+
## assert_have_been_called_nth_with
178+
> `assert_have_been_called_nth_with "nth" "spy" "expected"`
179+
180+
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.
181+
182+
::: code-group
183+
```bash [Example]
184+
function test_success() {
185+
bashunit::spy ps
186+
187+
ps first
188+
ps second
189+
ps third
190+
191+
assert_have_been_called_nth_with 1 ps "first"
192+
assert_have_been_called_nth_with 2 ps "second"
193+
assert_have_been_called_nth_with 3 ps "third"
194+
}
195+
196+
function test_failure() {
197+
bashunit::spy ps
198+
199+
ps first
200+
201+
assert_have_been_called_nth_with 1 ps "wrong"
202+
}
203+
```
204+
:::
205+
177206
## assert_have_been_called_times
178207
> assert_have_been_called_times "expected" "spy"
179208

src/assert.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,3 +729,89 @@ function assert_line_count() {
729729

730730
bashunit::state::add_assertions_passed
731731
}
732+
733+
function bashunit::format_to_regex() {
734+
local format="$1"
735+
local regex=""
736+
local i=0
737+
local len=${#format}
738+
739+
while [ $i -lt "$len" ]; do
740+
local char="${format:$i:1}"
741+
if [ "$char" = "%" ] && [ $((i + 1)) -lt "$len" ]; then
742+
local next="${format:$((i + 1)):1}"
743+
case "$next" in
744+
d) regex="${regex}[0-9]+" ;;
745+
i) regex="${regex}[+-]?[0-9]+" ;;
746+
f) regex="${regex}[+-]?[0-9]*\\.?[0-9]+" ;;
747+
s) regex="${regex}[^ ]+" ;;
748+
x) regex="${regex}[0-9a-fA-F]+" ;;
749+
e) regex="${regex}[+-]?[0-9]*\\.?[0-9]+[eE][+-]?[0-9]+" ;;
750+
%) regex="${regex}%" ;;
751+
*)
752+
regex="${regex}%${next}"
753+
;;
754+
esac
755+
i=$((i + 2))
756+
else
757+
case "$char" in
758+
. | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$')
759+
regex="${regex}\\${char}"
760+
;;
761+
\\)
762+
regex="${regex}\\\\"
763+
;;
764+
*)
765+
regex="${regex}${char}"
766+
;;
767+
esac
768+
i=$((i + 1))
769+
fi
770+
done
771+
772+
printf '%s' "^${regex}$"
773+
}
774+
775+
function assert_string_matches_format() {
776+
bashunit::assert::should_skip && return 0
777+
778+
local format="$1"
779+
local actual="$2"
780+
781+
local regex
782+
regex="$(bashunit::format_to_regex "$format")"
783+
784+
if ! [[ "$actual" =~ $regex ]]; then
785+
local test_fn
786+
test_fn="$(bashunit::helper::find_test_function_name)"
787+
local label
788+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
789+
bashunit::assert::mark_failed
790+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to match format" "${format}"
791+
return
792+
fi
793+
794+
bashunit::state::add_assertions_passed
795+
}
796+
797+
function assert_string_not_matches_format() {
798+
bashunit::assert::should_skip && return 0
799+
800+
local format="$1"
801+
local actual="$2"
802+
803+
local regex
804+
regex="$(bashunit::format_to_regex "$format")"
805+
806+
if [[ "$actual" =~ $regex ]]; then
807+
local test_fn
808+
test_fn="$(bashunit::helper::find_test_function_name)"
809+
local label
810+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
811+
bashunit::assert::mark_failed
812+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to not match format" "${format}"
813+
return
814+
fi
815+
816+
bashunit::state::add_assertions_passed
817+
}

tests/unit/assert_string_test.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,55 @@ function test_assert_string_start_end_with_special_chars_fail() {
158158
"Assert string start end with special chars fail" "fooX" "to end with" ".bar")" \
159159
"$(assert_string_ends_with ".bar" "fooX")"
160160
}
161+
162+
function test_successful_assert_string_matches_format_with_digit() {
163+
assert_empty "$(assert_string_matches_format "%d items found" "42 items found")"
164+
}
165+
166+
function test_successful_assert_string_matches_format_with_string() {
167+
assert_empty "$(assert_string_matches_format "Hello %s" "Hello world")"
168+
}
169+
170+
function test_successful_assert_string_matches_format_with_hex() {
171+
assert_empty "$(assert_string_matches_format "Color: %x" "Color: ff00ab")"
172+
}
173+
174+
function test_successful_assert_string_matches_format_with_float() {
175+
assert_empty "$(assert_string_matches_format "Value: %f" "Value: 3.14")"
176+
}
177+
178+
function test_successful_assert_string_matches_format_with_signed_integer() {
179+
assert_empty "$(assert_string_matches_format "Offset: %i" "Offset: -42")"
180+
}
181+
182+
function test_successful_assert_string_matches_format_with_scientific() {
183+
assert_empty "$(assert_string_matches_format "Result: %e" "Result: 1.5e10")"
184+
}
185+
186+
function test_successful_assert_string_matches_format_with_literal_percent() {
187+
assert_empty "$(assert_string_matches_format "100%% done" "100% done")"
188+
}
189+
190+
function test_successful_assert_string_matches_format_with_multiple_placeholders() {
191+
assert_empty "$(assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each")"
192+
}
193+
194+
function test_unsuccessful_assert_string_matches_format() {
195+
assert_same \
196+
"$(bashunit::console_results::print_failed_test \
197+
"Unsuccessful assert string matches format" \
198+
"hello world" "to match format" "%d items")" \
199+
"$(assert_string_matches_format "%d items" "hello world")"
200+
}
201+
202+
function test_successful_assert_string_not_matches_format() {
203+
assert_empty "$(assert_string_not_matches_format "%d items" "hello world")"
204+
}
205+
206+
function test_unsuccessful_assert_string_not_matches_format() {
207+
assert_same \
208+
"$(bashunit::console_results::print_failed_test \
209+
"Unsuccessful assert string not matches format" \
210+
"42 items" "to not match format" "%d items")" \
211+
"$(assert_string_not_matches_format "%d items" "42 items")"
212+
}

0 commit comments

Comments
 (0)