Skip to content

Commit 9df47c1

Browse files
authored
Merge pull request #402 from TypedDevs/feat/252-allow-interpolating-arguments-data-providers
Allow interpolating arguments data providers
2 parents 014f2e9 + f2133e4 commit 9df47c1

5 files changed

Lines changed: 115 additions & 39 deletions

File tree

CHANGELOG.md

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

55
- Fix asserts on test doubles in subshell
6+
- Allow interpolating arguments in data providers output
67

78
## [0.19.1](https://github.com/TypedDevs/bashunit/compare/0.19.0...0.19.1) - 2025-05-23
89

docs/data-providers.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ function test_my_test_case() {
2424

2525
The provider function can echo a single value like `"one"` or multiple values `"one" "two" "three"`.
2626

27+
## Interpolating arguments in test names
28+
29+
You can reference the values provided by a data provider directly in the test
30+
function name using placeholders like `::1::`, `::2::`, ... matching the
31+
argument position.
32+
33+
::: code-group
34+
```bash [example_test.sh]
35+
# data_provider fizz_numbers
36+
function test_returns_fizz_when_multiple_of_::1::_like_::2::_given() {
37+
# ...
38+
}
39+
40+
function fizz_numbers() {
41+
echo 3 4
42+
echo 3 6
43+
}
44+
```
45+
```[Output]
46+
Running example_test.sh
47+
✓ Passed: Returns fizz when multiple of '3' like '4' given
48+
✓ Passed: Returns fizz when multiple of '3' like '6' given
49+
```
50+
:::
51+
2752
## Multiple args in one call
2853

2954
::: code-group

src/helpers.sh

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ declare -r BASHUNIT_GIT_REPO="https://github.com/TypedDevs/bashunit"
88
# @return string Eg: "Some logic camelCase"
99
#
1010
function helper::normalize_test_function_name() {
11-
local original_function_name="${1-}"
11+
local original_fn_name="${1-}"
12+
local interpolated_fn_name="${2-}"
13+
14+
if [[ -n "${interpolated_fn_name-}" ]]; then
15+
original_fn_name="$interpolated_fn_name"
16+
fi
17+
1218
local result
1319

1420
# Remove the first "test_" prefix, if present
15-
result="${original_function_name#test_}"
21+
result="${original_fn_name#test_}"
1622
# If no "test_" was removed (e.g., "testFoo"), remove the "test" prefix
17-
if [[ "$result" == "$original_function_name" ]]; then
18-
result="${original_function_name#test}"
23+
if [[ "$result" == "$original_fn_name" ]]; then
24+
result="${original_fn_name#test}"
1925
fi
2026
# Replace underscores with spaces
2127
result="${result//_/ }"
@@ -25,6 +31,29 @@ function helper::normalize_test_function_name() {
2531
echo "$result"
2632
}
2733

34+
function helper::escape_single_quotes() {
35+
local value="$1"
36+
# shellcheck disable=SC1003
37+
echo "${value//\'/'\'\\''\'}"
38+
}
39+
40+
function helper::interpolate_function_name() {
41+
local function_name="$1"
42+
shift
43+
local args=("$@")
44+
local result="$function_name"
45+
46+
for ((i=0; i<${#args[@]}; i++)); do
47+
local placeholder="::$((i+1))::"
48+
# shellcheck disable=SC2155
49+
local value="$(helper::escape_single_quotes "${args[$i]}")"
50+
value="'$value'"
51+
result="${result//${placeholder}/${value}}"
52+
done
53+
54+
echo "$result"
55+
}
56+
2857
function helper::check_duplicate_functions() {
2958
local script="$1"
3059

src/runner.sh

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ function runner::spinner() {
5050

5151
function runner::functions_for_script() {
5252
local script="$1"
53-
local all_function_names="$2"
53+
local all_fn_names="$2"
5454

5555
# Filter the names down to the ones defined in the script, sort them by line number
5656
shopt -s extdebug
57-
for f in $all_function_names; do
57+
for f in $all_fn_names; do
5858
declare -F "$f" | grep "$script"
5959
done | sort -k2 -n | awk '{print $1}'
6060
shopt -u extdebug
@@ -65,8 +65,8 @@ function runner::call_test_functions() {
6565
local filter="$2"
6666
local prefix="test"
6767
# Use declare -F to list all function names
68-
local all_function_names=$(declare -F | awk '{print $3}')
69-
local filtered_functions=$(helper::get_functions_to_run "$prefix" "$filter" "$all_function_names")
68+
local all_fn_names=$(declare -F | awk '{print $3}')
69+
local filtered_functions=$(helper::get_functions_to_run "$prefix" "$filter" "$all_fn_names")
7070
# shellcheck disable=SC2207
7171
local functions_to_run=($(runner::functions_for_script "$script" "$filtered_functions"))
7272

@@ -77,33 +77,33 @@ function runner::call_test_functions() {
7777
runner::render_running_file_header
7878
helper::check_duplicate_functions "$script" || true
7979

80-
for function_name in "${functions_to_run[@]}"; do
80+
for fn_name in "${functions_to_run[@]}"; do
8181
if parallel::is_enabled && parallel::must_stop_on_failure; then
8282
break
8383
fi
8484

8585
local provider_data=()
8686
while IFS=" " read -r line; do
8787
provider_data+=("$line")
88-
done <<< "$(helper::get_provider_data "$function_name" "$script")"
88+
done <<< "$(helper::get_provider_data "$fn_name" "$script")"
8989

9090
# No data provider found
9191
if [[ "${#provider_data[@]}" -eq 0 ]]; then
92-
runner::run_test "$script" "$function_name"
93-
unset function_name
92+
runner::run_test "$script" "$fn_name"
93+
unset fn_name
9494
continue
9595
fi
9696

9797
# Execute the test function for each line of data
9898
for data in "${provider_data[@]}"; do
9999
IFS=" " read -r -a args <<< "$data"
100100
if [ "${#args[@]}" -gt 1 ]; then
101-
runner::run_test "$script" "$function_name" "${args[@]}"
101+
runner::run_test "$script" "$fn_name" "${args[@]}"
102102
else
103-
runner::run_test "$script" "$function_name" "$data"
103+
runner::run_test "$script" "$fn_name" "$data"
104104
fi
105105
done
106-
unset function_name
106+
unset fn_name
107107
done
108108

109109
if ! env::is_simple_output_enabled; then
@@ -133,8 +133,10 @@ function runner::run_test() {
133133

134134
local test_file="$1"
135135
shift
136-
local function_name="$1"
136+
local fn_name="$1"
137137
shift
138+
139+
local interpolated_fn_name="$(helper::interpolate_function_name "$fn_name" "$@")"
138140
local current_assertions_failed="$(state::get_assertions_failed)"
139141
local current_assertions_snapshot="$(state::get_assertions_snapshot)"
140142
local current_assertions_incomplete="$(state::get_assertions_incomplete)"
@@ -157,7 +159,7 @@ function runner::run_test() {
157159
158160
# 2>&1: Redirects the std-error (FD 2) to the std-output (FD 1).
159161
# points to the original std-output.
160-
"$function_name" "$@" 2>&1
162+
"$fn_name" "$@" 2>&1
161163
162164
)
163165

@@ -175,7 +177,7 @@ function runner::run_test() {
175177

176178
printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '='
177179
printf "%s\n" "File: $test_file"
178-
printf "%s\n" "Function: $function_name"
180+
printf "%s\n" "Function: $fn_name"
179181
printf "%s\n" "Duration: $duration ms"
180182
local raw_text=${test_execution_result%%##ASSERTIONS_*}
181183
[[ -n $raw_text ]] && printf "%s" "Raw text: ${test_execution_result%%##ASSERTIONS_*}"
@@ -216,22 +218,22 @@ function runner::run_test() {
216218
fi
217219
done
218220

219-
runner::parse_result "$function_name" "$test_execution_result" "$@"
221+
runner::parse_result "$fn_name" "$test_execution_result" "$@"
220222

221223
local total_assertions="$(state::calculate_total_assertions "$test_execution_result")"
222224
local test_exit_code="$(state::get_test_exit_code)"
223225

224226
if [[ -n $runtime_error || $test_exit_code -ne 0 ]]; then
225227
state::add_tests_failed
226-
console_results::print_error_test "$function_name" "$runtime_error"
227-
reports::add_test_failed "$test_file" "$function_name" "$duration" "$total_assertions"
228+
console_results::print_error_test "$fn_name" "$runtime_error"
229+
reports::add_test_failed "$test_file" "$fn_name" "$duration" "$total_assertions"
228230
runner::write_failure_result_output "$test_file" "$runtime_error"
229231
return
230232
fi
231233

232234
if [[ "$current_assertions_failed" != "$(state::get_assertions_failed)" ]]; then
233235
state::add_tests_failed
234-
reports::add_test_failed "$test_file" "$function_name" "$duration" "$total_assertions"
236+
reports::add_test_failed "$test_file" "$fn_name" "$duration" "$total_assertions"
235237
runner::write_failure_result_output "$test_file" "$subshell_output"
236238

237239
if env::is_stop_on_failure_enabled; then
@@ -246,28 +248,32 @@ function runner::run_test() {
246248

247249
if [[ "$current_assertions_snapshot" != "$(state::get_assertions_snapshot)" ]]; then
248250
state::add_tests_snapshot
249-
console_results::print_snapshot_test "$function_name"
250-
reports::add_test_snapshot "$test_file" "$function_name" "$duration" "$total_assertions"
251+
console_results::print_snapshot_test "$fn_name"
252+
reports::add_test_snapshot "$test_file" "$fn_name" "$duration" "$total_assertions"
251253
return
252254
fi
253255

254256
if [[ "$current_assertions_incomplete" != "$(state::get_assertions_incomplete)" ]]; then
255257
state::add_tests_incomplete
256-
reports::add_test_incomplete "$test_file" "$function_name" "$duration" "$total_assertions"
258+
reports::add_test_incomplete "$test_file" "$fn_name" "$duration" "$total_assertions"
257259
return
258260
fi
259261

260262
if [[ "$current_assertions_skipped" != "$(state::get_assertions_skipped)" ]]; then
261263
state::add_tests_skipped
262-
reports::add_test_skipped "$test_file" "$function_name" "$duration" "$total_assertions"
264+
reports::add_test_skipped "$test_file" "$fn_name" "$duration" "$total_assertions"
263265
return
264266
fi
265267

266-
local label="$(helper::normalize_test_function_name "$function_name")"
268+
local label="$(helper::normalize_test_function_name "$fn_name" "$interpolated_fn_name")"
267269

268-
console_results::print_successful_test "${label}" "$duration" "$@"
270+
if [[ "$fn_name" == "$interpolated_fn_name" ]]; then
271+
console_results::print_successful_test "${label}" "$duration" "$@"
272+
else
273+
console_results::print_successful_test "${label}" "$duration"
274+
fi
269275
state::add_tests_passed
270-
reports::add_test_passed "$test_file" "$function_name" "$duration" "$total_assertions"
276+
reports::add_test_passed "$test_file" "$fn_name" "$duration" "$total_assertions"
271277
}
272278

273279
function runner::decode_subshell_output() {
@@ -285,21 +291,21 @@ function runner::decode_subshell_output() {
285291
}
286292

287293
function runner::parse_result() {
288-
local function_name=$1
294+
local fn_name=$1
289295
shift
290296
local execution_result=$1
291297
shift
292298
local args=("$@")
293299

294300
if parallel::is_enabled; then
295-
runner::parse_result_parallel "$function_name" "$execution_result" "${args[@]}"
301+
runner::parse_result_parallel "$fn_name" "$execution_result" "${args[@]}"
296302
else
297-
runner::parse_result_sync "$function_name" "$execution_result"
303+
runner::parse_result_sync "$fn_name" "$execution_result"
298304
fi
299305
}
300306

301307
function runner::parse_result_parallel() {
302-
local function_name=$1
308+
local fn_name=$1
303309
shift
304310
local execution_result=$1
305311
shift
@@ -310,9 +316,9 @@ function runner::parse_result_parallel() {
310316

311317
local test_result_file=$(echo "${args[@]}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-|-$//')
312318
if [[ -z "$test_result_file" ]]; then
313-
test_result_file="${function_name}.$$.result"
319+
test_result_file="${fn_name}.$$.result"
314320
else
315-
test_result_file="${function_name}-${test_result_file}.$$.result"
321+
test_result_file="${fn_name}-${test_result_file}.$$.result"
316322
fi
317323

318324
local unique_test_result_file="${test_suite_dir}/${test_result_file}"
@@ -323,15 +329,15 @@ function runner::parse_result_parallel() {
323329
count=$((count + 1))
324330
done
325331

326-
log "debug" "[PARA]" "function_name:$function_name" "execution_result:$execution_result"
332+
log "debug" "[PARA]" "fn_name:$fn_name" "execution_result:$execution_result"
327333

328-
runner::parse_result_sync "$function_name" "$execution_result"
334+
runner::parse_result_sync "$fn_name" "$execution_result"
329335

330336
echo "$execution_result" > "$unique_test_result_file"
331337
}
332338

333339
function runner::parse_result_sync() {
334-
local function_name=$1
340+
local fn_name=$1
335341
local execution_result=$2
336342

337343
local assertions_failed=$(\
@@ -370,7 +376,7 @@ function runner::parse_result_sync() {
370376
sed -E -e 's/.*##TEST_EXIT_CODE=([0-9]*)##.*/\1/g'\
371377
)
372378

373-
log "debug" "[SYNC]" "function_name:$function_name" "execution_result:$execution_result"
379+
log "debug" "[SYNC]" "fn_name:$fn_name" "execution_result:$execution_result"
374380

375381
((_ASSERTIONS_PASSED += assertions_passed)) || true
376382
((_ASSERTIONS_FAILED += assertions_failed)) || true

tests/unit/helpers_test.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,18 @@ function test_to_run_with_filter_matching_string_in_function_name() {
224224
"test_my_awesome_function test_your_awesome_function"\
225225
"$(helper::get_functions_to_run "test" "awesome" "${functions[*]}")"
226226
}
227+
228+
function test_interpolate_fn_name() {
229+
local result
230+
result="$(helper::interpolate_function_name "test_name_::1::_foo" "bar")"
231+
232+
assert_same "test_name_'bar'_foo" "$result"
233+
}
234+
235+
function test_normalize_test_function_name_with_interpolation() {
236+
local fn="test_returns_value_::1::_and_::2::_given"
237+
# shellcheck disable=SC2155
238+
local interpolated_fn="$(helper::interpolate_function_name "$fn" "3" "4")"
239+
240+
assert_same "Returns value '3' and '4' given" "$(helper::normalize_test_function_name "$fn" "$interpolated_fn")"
241+
}

0 commit comments

Comments
 (0)