Skip to content

Commit 6a427f0

Browse files
Chemaclassclaude
andcommitted
feat(assert): add assert_string_matches_format assertion
Adds PHPUnit-style format assertions with placeholders: %d (digits), %i (signed int), %f (float), %s (non-whitespace), %x (hex), %e (scientific), %% (literal percent). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fa97cfd commit 6a427f0

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

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

src/assert.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,3 +729,86 @@ 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}${char}"
763+
;;
764+
esac
765+
i=$((i + 1))
766+
fi
767+
done
768+
769+
printf '%s' "^${regex}$"
770+
}
771+
772+
function assert_string_matches_format() {
773+
bashunit::assert::should_skip && return 0
774+
775+
local format="$1"
776+
local actual="$2"
777+
778+
local regex
779+
regex="$(bashunit::format_to_regex "$format")"
780+
781+
if ! [[ "$actual" =~ $regex ]]; then
782+
local test_fn
783+
test_fn="$(bashunit::helper::find_test_function_name)"
784+
local label
785+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
786+
bashunit::assert::mark_failed
787+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to match format" "${format}"
788+
return
789+
fi
790+
791+
bashunit::state::add_assertions_passed
792+
}
793+
794+
function assert_string_not_matches_format() {
795+
bashunit::assert::should_skip && return 0
796+
797+
local format="$1"
798+
local actual="$2"
799+
800+
local regex
801+
regex="$(bashunit::format_to_regex "$format")"
802+
803+
if [[ "$actual" =~ $regex ]]; then
804+
local test_fn
805+
test_fn="$(bashunit::helper::find_test_function_name)"
806+
local label
807+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
808+
bashunit::assert::mark_failed
809+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to not match format" "${format}"
810+
return
811+
fi
812+
813+
bashunit::state::add_assertions_passed
814+
}

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)