Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
## Unreleased

### Added
- Add `--jobs N` flag to limit parallel test concurrency (e.g., `--jobs 4`)
- Add `--watch` mode to automatically re-run tests when files change
- Add `watch [path]` subcommand to re-run tests automatically on file changes
- Uses `inotifywait` on Linux (via `inotify-tools`) or `fswatch` on macOS
- Falls back with a clear install hint if neither tool is available
- Accepts optional path argument (defaults to current directory)

- Add source context display in failure summaries showing relevant assertion lines
- Add TAP version 13 output format via `--output tap` for CI/CD integration
- Add date comparison assertions: `assert_date_equals`, `assert_date_before`, `assert_date_after`, `assert_date_within_range`, `assert_date_within_delta`
- Auto-detects epoch seconds, ISO 8601, space-separated datetime, and timezone offsets
- Mixed formats supported in the same assertion call
Expand Down
21 changes: 21 additions & 0 deletions docs/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ bashunit test tests/ --parallel --simple
| `-e, --env, --boot <file>` | Load custom env/bootstrap file (supports args) |
| `-f, --filter <name>` | Only run tests matching name |
| `--log-junit <file>` | Write JUnit XML report |
| `-j, --jobs <N>` | Run tests in parallel with max N concurrent jobs |
| `-p, --parallel` | Run tests in parallel |
| `--no-parallel` | Run tests sequentially |
| `-r, --report-html <file>` | Write HTML report |
Expand Down Expand Up @@ -214,6 +215,26 @@ The file will still run in parallel with other files, but tests within it will
run sequentially.
:::

### Jobs

> `bashunit test -j|--jobs <N>`

Run tests in parallel with a maximum of N concurrent jobs. This implicitly
enables parallel mode.

Use this to limit CPU usage on CI or machines with constrained resources.

::: code-group
```bash [Example]
bashunit test tests/ --jobs 4
```
:::

::: tip
`--jobs 0` (the default) means unlimited concurrency, which is equivalent to
`--parallel`.
:::

### Output Style

> `bashunit test -s|--simple`
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ systems bashunit forces sequential execution to avoid inconsistent results.

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

## Parallel Jobs

> `BASHUNIT_PARALLEL_JOBS=<N>`

Limits the number of concurrent jobs when running in parallel mode. Set to `0` (default) for unlimited concurrency.

Similar as using `-j|--jobs` option on the [command line](/command-line#jobs).

## Stop on failure

Expand Down
5 changes: 4 additions & 1 deletion src/console_header.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,13 @@ Options:
--tag <name> Only run tests with matching @tag (repeatable, OR logic)
--exclude-tag <name> Skip tests with matching @tag (repeatable, exclude wins)
--log-junit <file> Write JUnit XML report
-p, --parallel Run tests in parallel
-j, --jobs <N> Run tests in parallel with max N concurrent jobs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the documentation updated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! Added --jobs documentation to both docs/command-line.md (options table + dedicated section) and docs/configuration.md (BASHUNIT_PARALLEL_JOBS env var).

-p, --parallel Run tests in parallel (unlimited concurrency)
--no-parallel Run tests sequentially
-r, --report-html <file> Write HTML report
-s, --simple Simple output (dots)
--detailed Detailed output (default)
--output <format> Output format: tap (TAP version 13)
-R, --run-all Run all assertions (don't stop on first failure)
-S, --stop-on-failure Stop on first failure
-vvv, --verbose Show execution details
Expand All @@ -126,6 +128,7 @@ Options:
--strict Enable strict shell mode (set -euo pipefail)
--skip-env-file Skip .env loading, use shell environment only
-l, --login Run tests in login shell context
-w, --watch Watch for changes and re-run tests
--no-color Disable colored output (honors NO_COLOR env var)
-h, --help Show this help message

Expand Down
12 changes: 12 additions & 0 deletions src/console_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ function bashunit::console_results::render_result() {
return 1
fi

if bashunit::env::is_tap_output_enabled; then
printf "1..%d\n" "$_BASHUNIT_TOTAL_TESTS_COUNT"
if [[ $_BASHUNIT_TESTS_FAILED -gt 0 ]]; then
return 1
fi
return 0
fi

if bashunit::env::is_simple_output_enabled; then
printf "\n\n"
fi
Expand Down Expand Up @@ -176,6 +184,10 @@ function bashunit::console_results::print_hook_completed() {
return
fi

if bashunit::env::is_tap_output_enabled; then
return
fi

if bashunit::parallel::is_enabled; then
return
fi
Expand Down
7 changes: 7 additions & 0 deletions src/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ _BASHUNIT_DEFAULT_FAILURES_ONLY="false"
_BASHUNIT_DEFAULT_NO_COLOR="false"
_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE="true"
_BASHUNIT_DEFAULT_NO_PROGRESS="false"
_BASHUNIT_DEFAULT_OUTPUT_FORMAT=""

: "${BASHUNIT_PARALLEL_RUN:=${PARALLEL_RUN:=$_BASHUNIT_DEFAULT_PARALLEL_RUN}}"
: "${BASHUNIT_PARALLEL_JOBS:=0}"
: "${BASHUNIT_SHOW_HEADER:=${SHOW_HEADER:=$_BASHUNIT_DEFAULT_SHOW_HEADER}}"
: "${BASHUNIT_HEADER_ASCII_ART:=${HEADER_ASCII_ART:=$_BASHUNIT_DEFAULT_HEADER_ASCII_ART}}"
: "${BASHUNIT_SIMPLE_OUTPUT:=${SIMPLE_OUTPUT:=$_BASHUNIT_DEFAULT_SIMPLE_OUTPUT}}"
Expand All @@ -84,6 +86,7 @@ _BASHUNIT_DEFAULT_NO_PROGRESS="false"
: "${BASHUNIT_FAILURES_ONLY:=${FAILURES_ONLY:=$_BASHUNIT_DEFAULT_FAILURES_ONLY}}"
: "${BASHUNIT_SHOW_OUTPUT_ON_FAILURE:=${SHOW_OUTPUT_ON_FAILURE:=$_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE}}"
: "${BASHUNIT_NO_PROGRESS:=${NO_PROGRESS:=$_BASHUNIT_DEFAULT_NO_PROGRESS}}"
: "${BASHUNIT_OUTPUT_FORMAT:=${OUTPUT_FORMAT:=$_BASHUNIT_DEFAULT_OUTPUT_FORMAT}}"
# Support NO_COLOR standard (https://no-color.org)
if [[ -n "${NO_COLOR:-}" ]]; then
BASHUNIT_NO_COLOR="true"
Expand Down Expand Up @@ -179,6 +182,10 @@ function bashunit::env::is_coverage_enabled() {
[[ "$BASHUNIT_COVERAGE" == "true" ]]
}

function bashunit::env::is_tap_output_enabled() {
[[ "$BASHUNIT_OUTPUT_FORMAT" == "tap" ]]
}

function bashunit::env::active_internet_connection() {
if [[ "${BASHUNIT_NO_NETWORK:-}" == "true" ]]; then
return 1
Expand Down
116 changes: 107 additions & 9 deletions src/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ function bashunit::main::cmd_test() {
--detailed)
export BASHUNIT_SIMPLE_OUTPUT=false
;;
--output)
export BASHUNIT_OUTPUT_FORMAT="$2"
shift
;;
--debug)
local output_file="${2:-}"
if [[ -n "$output_file" && "${output_file:0:1}" != "-" ]]; then
Expand All @@ -62,9 +66,17 @@ function bashunit::main::cmd_test() {
-p | --parallel)
export BASHUNIT_PARALLEL_RUN=true
;;
-j | --jobs)
export BASHUNIT_PARALLEL_RUN=true
export BASHUNIT_PARALLEL_JOBS="$2"
shift
;;
--no-parallel)
export BASHUNIT_PARALLEL_RUN=false
;;
-w | --watch)
export BASHUNIT_WATCH_MODE=true
;;
-e | --env | --boot)
# Support: --env "bootstrap.sh arg1 arg2"
local boot_file="${2%% *}"
Expand Down Expand Up @@ -265,12 +277,19 @@ function bashunit::main::cmd_test() {
export BASHUNIT_COVERAGE=false
bashunit::main::exec_assert "$assert_fn" ${args+"${args[@]}"}
else
# Bash 3.0 compatible: only pass args if we have files
# (local args without =() creates a scalar, not an empty array)
if [[ "$args_count" -gt 0 ]]; then
bashunit::main::exec_tests "$filter" "$tag_filter" "$exclude_tag_filter" "${args[@]}"
if [[ "${BASHUNIT_WATCH_MODE:-false}" == true ]]; then
bashunit::main::watch_loop \
"$filter" "$tag_filter" "$exclude_tag_filter" \
${args+"${args[@]}"}
else
bashunit::main::exec_tests "$filter" "$tag_filter" "$exclude_tag_filter"
if [[ "$args_count" -gt 0 ]]; then
bashunit::main::exec_tests \
"$filter" "$tag_filter" "$exclude_tag_filter" \
"${args[@]}"
else
bashunit::main::exec_tests \
"$filter" "$tag_filter" "$exclude_tag_filter"
fi
fi
fi
}
Expand Down Expand Up @@ -491,6 +510,79 @@ function bashunit::main::cmd_assert() {
exit $?
}

#############################
# Watch mode
#############################
function bashunit::main::watch_get_checksum() {
local IFS=$' \t\n'
local -a paths=("$@")

local file checksum=""
for file in "${paths[@]+"${paths[@]}"}"; do
if [[ -d "$file" ]]; then
local found
found=$(find "$file" -name '*.sh' -type f \
-exec stat -f '%m %N' {} + 2>/dev/null ||
find "$file" -name '*.sh' -type f \
-exec stat -c '%Y %n' {} + 2>/dev/null) || true
checksum="${checksum}${found}"
elif [[ -f "$file" ]]; then
local mtime
mtime=$(stat -f '%m' "$file" 2>/dev/null ||
stat -c '%Y' "$file" 2>/dev/null) || true
checksum="${checksum}${mtime} ${file}"
fi
done
echo "$checksum"
}

function bashunit::main::watch_loop() {
local filter="$1"
local tag_filter="${2:-}"
local exclude_tag_filter="${3:-}"
shift 3

local IFS=$' \t\n'
local -a watch_paths=("$@")
[[ -d "src" ]] && watch_paths[${#watch_paths[@]}]="src"

trap 'printf "\n%sWatch mode stopped.%s\n" \
"${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"; \
exit 0' INT

local last_checksum=""
while true; do
local current_checksum
current_checksum=$(bashunit::main::watch_get_checksum \
"${watch_paths[@]}")

if [[ "$current_checksum" != "$last_checksum" ]]; then
last_checksum="$current_checksum"
printf '\033[2J\033[H'
printf "%s[watch] Running tests...%s\n\n" \
"${_BASHUNIT_COLOR_SKIPPED}" \
"${_BASHUNIT_COLOR_DEFAULT}"

(
if [[ $# -gt 0 ]]; then
bashunit::main::exec_tests \
"$filter" "$tag_filter" \
"$exclude_tag_filter" "$@"
else
bashunit::main::exec_tests \
"$filter" "$tag_filter" \
"$exclude_tag_filter"
fi
) || true

printf "\n%s[watch] Waiting for changes...%s\n" \
"${_BASHUNIT_COLOR_SKIPPED}" \
"${_BASHUNIT_COLOR_DEFAULT}"
fi
sleep 1
done
}

#############################
# Test execution
#############################
Expand Down Expand Up @@ -533,7 +625,11 @@ function bashunit::main::exec_tests() {
bashunit::parallel::init
fi

bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
if bashunit::env::is_tap_output_enabled; then
printf "TAP version 13\n"
else
bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
fi

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

bashunit::console_results::print_failing_tests_and_reset
bashunit::console_results::print_incomplete_tests_and_reset
bashunit::console_results::print_skipped_tests_and_reset
if ! bashunit::env::is_tap_output_enabled; then
bashunit::console_results::print_failing_tests_and_reset
bashunit::console_results::print_incomplete_tests_and_reset
bashunit::console_results::print_skipped_tests_and_reset
fi
bashunit::console_results::render_result
exit_code=$?

Expand Down
Loading
Loading