From 6efb9936463cce0f77980e7e146e2e3001991e48 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 2 Oct 2026 14:23:42 +0200 Subject: [PATCH] perf(runner): transfer provider args without codecs Closes #1370 --- CHANGELOG.md | 3 ++ src/runner/exec.sh | 19 ++++++++--- src/runner/provider.sh | 19 ++++++++--- tests/acceptance/bashunit_run_forks_test.sh | 37 +++++++++++++++++++++ tests/functional/provider_test.sh | 29 +++++++++++++--- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8aa2bc9..58c91e3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Changed +- Data-provider rows transfer arguments without a base64 encode and decode process for each value (#1370) + ## [0.51.0](https://github.com/TypedDevs/bashunit/compare/0.50.1...0.51.0) - 2026-09-18 ### Changed diff --git a/src/runner/exec.sh b/src/runner/exec.sh index 72c313a16..98268d17f 100644 --- a/src/runner/exec.sh +++ b/src/runner/exec.sh @@ -122,6 +122,7 @@ function bashunit::runner::call_test_functions() { local provider_data_count=0 local -a parsed_data=() local parsed_data_count=0 + local provider_arg_file="" # Monotonic within this file; names each parallel worker's .result file. local _test_ordinal=0 @@ -202,16 +203,26 @@ function bashunit::runner::call_test_functions() { fi # Execute the test function for each line of data + if [ -z "$provider_arg_file" ]; then + bashunit::env::ensure_run_output_dir || return 1 + local provider_arg_dir="$_BASHUNIT_RUN_OUTPUT_DIR" + case "$provider_arg_dir" in + /*) ;; + *) provider_arg_dir="$BASHUNIT_WORKING_DIR/$provider_arg_dir" ;; + esac + provider_arg_file="$("$MKTEMP" "$provider_arg_dir/provider-args.XXXXXXX")" || return 1 + fi local data for data in "${provider_data[@]+"${provider_data[@]}"}"; do parsed_data=() parsed_data_count=0 local line - while IFS= read -r line; do - [ -z "$line" ] && continue - parsed_data[parsed_data_count]="$(bashunit::helper::decode_base64 "${line}")" + # The parser's eval must stay in a subshell so expansions cannot change runner state. + (bashunit::runner::parse_data_provider_args "$data" nul) >"$provider_arg_file" + while IFS= read -r -d '' line; do + parsed_data[parsed_data_count]="$line" parsed_data_count=$((parsed_data_count + 1)) - done <<<"$(bashunit::runner::parse_data_provider_args "$data")" + done <"$provider_arg_file" if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then bashunit::runner::wait_for_job_slot _test_ordinal=$((_test_ordinal + 1)) diff --git a/src/runner/provider.sh b/src/runner/provider.sh index a6fb440fe..4f91b1557 100644 --- a/src/runner/provider.sh +++ b/src/runner/provider.sh @@ -11,6 +11,7 @@ function bashunit::runner::parse_data_provider_args() { local i=0 local arg="" local encoded_arg + local output_format="${2:-base64}" local -a args=() local args_count=0 @@ -46,8 +47,12 @@ function bashunit::runner::parse_data_provider_args() { fi # Print args and return early for arg in "${args[@]+"${args[@]}"}"; do - encoded_arg="$(bashunit::helper::encode_base64 "${arg}")" - printf '%s\n' "$encoded_arg" + if [ "$output_format" = nul ]; then + printf '%s\0' "$arg" + else + encoded_arg="$(bashunit::helper::encode_base64 "${arg}")" + printf '%s\n' "$encoded_arg" + fi done return fi @@ -117,10 +122,14 @@ function bashunit::runner::parse_data_provider_args() { break fi done - # Print one arg per line to stdout, base64-encoded to preserve newlines in the data + # Keep the original base64 stdout format for callers without a requested transport. local arg for arg in ${args+"${args[@]}"}; do - encoded_arg="$(bashunit::helper::encode_base64 "${arg}")" - printf '%s\n' "$encoded_arg" + if [ "$output_format" = nul ]; then + printf '%s\0' "$arg" + else + encoded_arg="$(bashunit::helper::encode_base64 "${arg}")" + printf '%s\n' "$encoded_arg" + fi done } diff --git a/tests/acceptance/bashunit_run_forks_test.sh b/tests/acceptance/bashunit_run_forks_test.sh index 8d9c2db20..6720e4a6e 100644 --- a/tests/acceptance/bashunit_run_forks_test.sh +++ b/tests/acceptance/bashunit_run_forks_test.sh @@ -283,6 +283,43 @@ function test_reports_do_not_fork_base64_per_field() { assert_less_or_equal_than 16 "$calls" } +function test_provider_arguments_do_not_fork_base64_per_value() { + if bashunit::check_os::is_windows; then + bashunit::skip "PATH shims are unreliable under Git Bash" && return + fi + + local dir + dir="$(bashunit::temp_dir)" + local count_file="$dir/base64_calls" + local real_base64 + real_base64="$(command -v base64)" + { + echo '#!/usr/bin/env bash' + echo "case \"\$*\" in --help) ;; *) echo x >> \"$count_file\" ;; esac" + echo "exec \"$real_base64\" \"\$@\"" + } >"$dir/base64" + chmod +x "$dir/base64" + + local fixture="$dir/provider_forks_test.sh" + { + echo 'function provide_rows() {' + echo ' bashunit::data_set a b' + echo ' bashunit::data_set c d' + echo ' bashunit::data_set e f' + echo '}' + echo '# @data_provider provide_rows' + echo 'function test_row() { assert_not_empty "$1"; assert_not_empty "$2"; }' + } >"$fixture" + + PATH="$dir:$PATH" ./bashunit --no-parallel "$fixture" >/dev/null 2>&1 + + local calls=0 + if [ -f "$count_file" ]; then + calls="$(grep -c . "$count_file" || true)" + fi + assert_equals 0 "$calls" +} + # Regression guard for the per-test hook path. A test in a file that defines # `set_up` or `tear_down` used to cost five process forks: each hook minted its # output file with `mktemp` and removed it with `rm -f`, and the temp-owner diff --git a/tests/functional/provider_test.sh b/tests/functional/provider_test.sh index 8e95d0264..424f98f79 100644 --- a/tests/functional/provider_test.sh +++ b/tests/functional/provider_test.sh @@ -153,10 +153,6 @@ function provide_two_args_with_spaces() { bashunit::data_set "first test" "second test" } -# A value ending in an odd run of backslashes escaped the `)` of the parser's -# `eval "args=($input)"` fast path, making it a syntax error -- which kills the -# command substitution the runner calls the parser inside, so the argument -# reached the test unset rather than as `C:` (#1134). function provide_trailing_backslash() { # shellcheck disable=SC1003 # a lone trailing backslash is the case under test echo 'C:\' @@ -167,3 +163,28 @@ function provide_trailing_backslash() { function test_a_value_ending_in_a_backslash_still_arrives() { assert_not_empty "${1-}" } + +function provide_lossless_values() { + bashunit::data_set "" $'unit\037separator' $'line\none\n' $'trailing \t ' "C:\\path\\" '*;|&' +} + +# @data_provider provide_lossless_values +function test_provider_values_arrive_without_record_delimiter_collisions() { + assert_same 6 "$#" + assert_same "" "$1" + assert_same $'unit\037separator' "$2" + assert_same $'line\none\n' "$3" + assert_same $'trailing \t ' "$4" + assert_same "C:\\path\\" "$5" + assert_same '*;|&' "$6" +} + +function provide_eval_assignment() { + printf '%s\n' '${PROVIDER_EVAL_SIDE_EFFECT:=changed}' +} + +# @data_provider provide_eval_assignment +function test_provider_eval_does_not_assign_in_runner() { + assert_same changed "$1" + assert_same unset "${PROVIDER_EVAL_SIDE_EFFECT-unset}" +}