Skip to content

Commit d1ab988

Browse files
authored
feat: P1 developer experience improvements (#599)
1 parent 87aadbe commit d1ab988

22 files changed

Lines changed: 333 additions & 13 deletions

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
## Unreleased
44

55
### Added
6+
- Add `--jobs N` flag to limit parallel test concurrency (e.g., `--jobs 4`)
7+
- Add `--watch` mode to automatically re-run tests when files change
68
- Add `watch [path]` subcommand to re-run tests automatically on file changes
79
- Uses `inotifywait` on Linux (via `inotify-tools`) or `fswatch` on macOS
810
- Falls back with a clear install hint if neither tool is available
911
- Accepts optional path argument (defaults to current directory)
10-
12+
- Add source context display in failure summaries showing relevant assertion lines
13+
- Add TAP version 13 output format via `--output tap` for CI/CD integration
1114
- Add date comparison assertions: `assert_date_equals`, `assert_date_before`, `assert_date_after`, `assert_date_within_range`, `assert_date_within_delta`
1215
- Auto-detects epoch seconds, ISO 8601, space-separated datetime, and timezone offsets
1316
- Mixed formats supported in the same assertion call

docs/command-line.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ bashunit test tests/ --parallel --simple
5656
| `-e, --env, --boot <file>` | Load custom env/bootstrap file (supports args) |
5757
| `-f, --filter <name>` | Only run tests matching name |
5858
| `--log-junit <file>` | Write JUnit XML report |
59+
| `-j, --jobs <N>` | Run tests in parallel with max N concurrent jobs |
5960
| `-p, --parallel` | Run tests in parallel |
6061
| `--no-parallel` | Run tests sequentially |
6162
| `-r, --report-html <file>` | Write HTML report |
@@ -214,6 +215,26 @@ The file will still run in parallel with other files, but tests within it will
214215
run sequentially.
215216
:::
216217

218+
### Jobs
219+
220+
> `bashunit test -j|--jobs <N>`
221+
222+
Run tests in parallel with a maximum of N concurrent jobs. This implicitly
223+
enables parallel mode.
224+
225+
Use this to limit CPU usage on CI or machines with constrained resources.
226+
227+
::: code-group
228+
```bash [Example]
229+
bashunit test tests/ --jobs 4
230+
```
231+
:::
232+
233+
::: tip
234+
`--jobs 0` (the default) means unlimited concurrency, which is equivalent to
235+
`--parallel`.
236+
:::
237+
217238
### Output Style
218239

219240
> `bashunit test -s|--simple`

docs/configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ systems bashunit forces sequential execution to avoid inconsistent results.
7676

7777
Similar as using `-p|--parallel` option on the [command line](/command-line#parallel).
7878

79+
## Parallel Jobs
80+
81+
> `BASHUNIT_PARALLEL_JOBS=<N>`
82+
83+
Limits the number of concurrent jobs when running in parallel mode. Set to `0` (default) for unlimited concurrency.
84+
85+
Similar as using `-j|--jobs` option on the [command line](/command-line#jobs).
7986

8087
## Stop on failure
8188

src/console_header.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,13 @@ Options:
109109
--tag <name> Only run tests with matching @tag (repeatable, OR logic)
110110
--exclude-tag <name> Skip tests with matching @tag (repeatable, exclude wins)
111111
--log-junit <file> Write JUnit XML report
112-
-p, --parallel Run tests in parallel
112+
-j, --jobs <N> Run tests in parallel with max N concurrent jobs
113+
-p, --parallel Run tests in parallel (unlimited concurrency)
113114
--no-parallel Run tests sequentially
114115
-r, --report-html <file> Write HTML report
115116
-s, --simple Simple output (dots)
116117
--detailed Detailed output (default)
118+
--output <format> Output format: tap (TAP version 13)
117119
-R, --run-all Run all assertions (don't stop on first failure)
118120
-S, --stop-on-failure Stop on first failure
119121
-vvv, --verbose Show execution details
@@ -126,6 +128,7 @@ Options:
126128
--strict Enable strict shell mode (set -euo pipefail)
127129
--skip-env-file Skip .env loading, use shell environment only
128130
-l, --login Run tests in login shell context
131+
-w, --watch Watch for changes and re-run tests
129132
--no-color Disable colored output (honors NO_COLOR env var)
130133
-h, --help Show this help message
131134

src/console_results.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ function bashunit::console_results::render_result() {
1212
return 1
1313
fi
1414

15+
if bashunit::env::is_tap_output_enabled; then
16+
printf "1..%d\n" "$_BASHUNIT_TOTAL_TESTS_COUNT"
17+
if [[ $_BASHUNIT_TESTS_FAILED -gt 0 ]]; then
18+
return 1
19+
fi
20+
return 0
21+
fi
22+
1523
if bashunit::env::is_simple_output_enabled; then
1624
printf "\n\n"
1725
fi
@@ -176,6 +184,10 @@ function bashunit::console_results::print_hook_completed() {
176184
return
177185
fi
178186

187+
if bashunit::env::is_tap_output_enabled; then
188+
return
189+
fi
190+
179191
if bashunit::parallel::is_enabled; then
180192
return
181193
fi

src/env.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ _BASHUNIT_DEFAULT_FAILURES_ONLY="false"
6464
_BASHUNIT_DEFAULT_NO_COLOR="false"
6565
_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE="true"
6666
_BASHUNIT_DEFAULT_NO_PROGRESS="false"
67+
_BASHUNIT_DEFAULT_OUTPUT_FORMAT=""
6768

6869
: "${BASHUNIT_PARALLEL_RUN:=${PARALLEL_RUN:=$_BASHUNIT_DEFAULT_PARALLEL_RUN}}"
70+
: "${BASHUNIT_PARALLEL_JOBS:=0}"
6971
: "${BASHUNIT_SHOW_HEADER:=${SHOW_HEADER:=$_BASHUNIT_DEFAULT_SHOW_HEADER}}"
7072
: "${BASHUNIT_HEADER_ASCII_ART:=${HEADER_ASCII_ART:=$_BASHUNIT_DEFAULT_HEADER_ASCII_ART}}"
7173
: "${BASHUNIT_SIMPLE_OUTPUT:=${SIMPLE_OUTPUT:=$_BASHUNIT_DEFAULT_SIMPLE_OUTPUT}}"
@@ -84,6 +86,7 @@ _BASHUNIT_DEFAULT_NO_PROGRESS="false"
8486
: "${BASHUNIT_FAILURES_ONLY:=${FAILURES_ONLY:=$_BASHUNIT_DEFAULT_FAILURES_ONLY}}"
8587
: "${BASHUNIT_SHOW_OUTPUT_ON_FAILURE:=${SHOW_OUTPUT_ON_FAILURE:=$_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE}}"
8688
: "${BASHUNIT_NO_PROGRESS:=${NO_PROGRESS:=$_BASHUNIT_DEFAULT_NO_PROGRESS}}"
89+
: "${BASHUNIT_OUTPUT_FORMAT:=${OUTPUT_FORMAT:=$_BASHUNIT_DEFAULT_OUTPUT_FORMAT}}"
8790
# Support NO_COLOR standard (https://no-color.org)
8891
if [[ -n "${NO_COLOR:-}" ]]; then
8992
BASHUNIT_NO_COLOR="true"
@@ -179,6 +182,10 @@ function bashunit::env::is_coverage_enabled() {
179182
[[ "$BASHUNIT_COVERAGE" == "true" ]]
180183
}
181184

185+
function bashunit::env::is_tap_output_enabled() {
186+
[[ "$BASHUNIT_OUTPUT_FORMAT" == "tap" ]]
187+
}
188+
182189
function bashunit::env::active_internet_connection() {
183190
if [[ "${BASHUNIT_NO_NETWORK:-}" == "true" ]]; then
184191
return 1

src/main.sh

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ function bashunit::main::cmd_test() {
4848
--detailed)
4949
export BASHUNIT_SIMPLE_OUTPUT=false
5050
;;
51+
--output)
52+
export BASHUNIT_OUTPUT_FORMAT="$2"
53+
shift
54+
;;
5155
--debug)
5256
local output_file="${2:-}"
5357
if [[ -n "$output_file" && "${output_file:0:1}" != "-" ]]; then
@@ -62,9 +66,17 @@ function bashunit::main::cmd_test() {
6266
-p | --parallel)
6367
export BASHUNIT_PARALLEL_RUN=true
6468
;;
69+
-j | --jobs)
70+
export BASHUNIT_PARALLEL_RUN=true
71+
export BASHUNIT_PARALLEL_JOBS="$2"
72+
shift
73+
;;
6574
--no-parallel)
6675
export BASHUNIT_PARALLEL_RUN=false
6776
;;
77+
-w | --watch)
78+
export BASHUNIT_WATCH_MODE=true
79+
;;
6880
-e | --env | --boot)
6981
# Support: --env "bootstrap.sh arg1 arg2"
7082
local boot_file="${2%% *}"
@@ -265,12 +277,19 @@ function bashunit::main::cmd_test() {
265277
export BASHUNIT_COVERAGE=false
266278
bashunit::main::exec_assert "$assert_fn" ${args+"${args[@]}"}
267279
else
268-
# Bash 3.0 compatible: only pass args if we have files
269-
# (local args without =() creates a scalar, not an empty array)
270-
if [[ "$args_count" -gt 0 ]]; then
271-
bashunit::main::exec_tests "$filter" "$tag_filter" "$exclude_tag_filter" "${args[@]}"
280+
if [[ "${BASHUNIT_WATCH_MODE:-false}" == true ]]; then
281+
bashunit::main::watch_loop \
282+
"$filter" "$tag_filter" "$exclude_tag_filter" \
283+
${args+"${args[@]}"}
272284
else
273-
bashunit::main::exec_tests "$filter" "$tag_filter" "$exclude_tag_filter"
285+
if [[ "$args_count" -gt 0 ]]; then
286+
bashunit::main::exec_tests \
287+
"$filter" "$tag_filter" "$exclude_tag_filter" \
288+
"${args[@]}"
289+
else
290+
bashunit::main::exec_tests \
291+
"$filter" "$tag_filter" "$exclude_tag_filter"
292+
fi
274293
fi
275294
fi
276295
}
@@ -491,6 +510,79 @@ function bashunit::main::cmd_assert() {
491510
exit $?
492511
}
493512

513+
#############################
514+
# Watch mode
515+
#############################
516+
function bashunit::main::watch_get_checksum() {
517+
local IFS=$' \t\n'
518+
local -a paths=("$@")
519+
520+
local file checksum=""
521+
for file in "${paths[@]+"${paths[@]}"}"; do
522+
if [[ -d "$file" ]]; then
523+
local found
524+
found=$(find "$file" -name '*.sh' -type f \
525+
-exec stat -f '%m %N' {} + 2>/dev/null ||
526+
find "$file" -name '*.sh' -type f \
527+
-exec stat -c '%Y %n' {} + 2>/dev/null) || true
528+
checksum="${checksum}${found}"
529+
elif [[ -f "$file" ]]; then
530+
local mtime
531+
mtime=$(stat -f '%m' "$file" 2>/dev/null ||
532+
stat -c '%Y' "$file" 2>/dev/null) || true
533+
checksum="${checksum}${mtime} ${file}"
534+
fi
535+
done
536+
echo "$checksum"
537+
}
538+
539+
function bashunit::main::watch_loop() {
540+
local filter="$1"
541+
local tag_filter="${2:-}"
542+
local exclude_tag_filter="${3:-}"
543+
shift 3
544+
545+
local IFS=$' \t\n'
546+
local -a watch_paths=("$@")
547+
[[ -d "src" ]] && watch_paths[${#watch_paths[@]}]="src"
548+
549+
trap 'printf "\n%sWatch mode stopped.%s\n" \
550+
"${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"; \
551+
exit 0' INT
552+
553+
local last_checksum=""
554+
while true; do
555+
local current_checksum
556+
current_checksum=$(bashunit::main::watch_get_checksum \
557+
"${watch_paths[@]}")
558+
559+
if [[ "$current_checksum" != "$last_checksum" ]]; then
560+
last_checksum="$current_checksum"
561+
printf '\033[2J\033[H'
562+
printf "%s[watch] Running tests...%s\n\n" \
563+
"${_BASHUNIT_COLOR_SKIPPED}" \
564+
"${_BASHUNIT_COLOR_DEFAULT}"
565+
566+
(
567+
if [[ $# -gt 0 ]]; then
568+
bashunit::main::exec_tests \
569+
"$filter" "$tag_filter" \
570+
"$exclude_tag_filter" "$@"
571+
else
572+
bashunit::main::exec_tests \
573+
"$filter" "$tag_filter" \
574+
"$exclude_tag_filter"
575+
fi
576+
) || true
577+
578+
printf "\n%s[watch] Waiting for changes...%s\n" \
579+
"${_BASHUNIT_COLOR_SKIPPED}" \
580+
"${_BASHUNIT_COLOR_DEFAULT}"
581+
fi
582+
sleep 1
583+
done
584+
}
585+
494586
#############################
495587
# Test execution
496588
#############################
@@ -533,7 +625,11 @@ function bashunit::main::exec_tests() {
533625
bashunit::parallel::init
534626
fi
535627

536-
bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
628+
if bashunit::env::is_tap_output_enabled; then
629+
printf "TAP version 13\n"
630+
else
631+
bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
632+
fi
537633

538634
if bashunit::env::is_verbose_enabled; then
539635
if bashunit::env::is_simple_output_enabled; then
@@ -559,9 +655,11 @@ function bashunit::main::exec_tests() {
559655
printf "\r%sStop on failure enabled...%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
560656
fi
561657

562-
bashunit::console_results::print_failing_tests_and_reset
563-
bashunit::console_results::print_incomplete_tests_and_reset
564-
bashunit::console_results::print_skipped_tests_and_reset
658+
if ! bashunit::env::is_tap_output_enabled; then
659+
bashunit::console_results::print_failing_tests_and_reset
660+
bashunit::console_results::print_incomplete_tests_and_reset
661+
bashunit::console_results::print_skipped_tests_and_reset
662+
fi
565663
bashunit::console_results::render_result
566664
exit_code=$?
567665

0 commit comments

Comments
 (0)