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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ BASHUNIT_NO_DIFF= # Default: false (disable unified diff on mu
BASHUNIT_PARALLEL_RUN= # Default: false
BASHUNIT_STOP_ON_FAILURE= # Default: false (stop suite on first failure)
BASHUNIT_RERUN_FAILED= # Default: false (replay only last run's failing tests)
BASHUNIT_ORDER_BY= # Default: defined (or defects, random)
BASHUNIT_CHANGED= # Default: false (run only test files changed since a git ref)
BASHUNIT_CHANGED_REF= # Default: empty (--changed ref: origin/HEAD, then HEAD)
BASHUNIT_EXCLUDE_FILTER= # Default: empty (skip tests whose name matches)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

### Added
- `--order-by <mode>` picks the execution order: `defined` (default), `defects` (last run's failures first, whole suite still runs) or `random`. `--random-order` and `--seed` keep working unchanged (#1011)
- `--changed [<ref>]` runs only the test files git reports as touched since `<ref>` (default `origin/HEAD`, then `HEAD`), covering committed, staged, unstaged and untracked changes. Deletions are dropped, a rename selects its new path, and a missing work tree or unresolvable ref fails the run instead of selecting nothing (#1010)
- `--list` (alias `--dry-run`) prints the tests a run would execute, without running them; `--list-format json` emits file, function, name, line and tags. Honours every selection flag, including `--shard` and `--random-order --seed` ordering (#1007)
- `--exclude-filter <name>` skips tests by name, the counterpart of `--exclude-tag`. Repeatable, OR'd, and wins over `--filter` (#1009)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ _bashunit() {
'--test-timeout[Fail a test running longer than N seconds]:seconds:' \
'--retry[Rerun a failed test up to N extra times]:count:' \
'--random-order[Randomize test execution order]' \
'--order-by[Execution order]:mode:(defined defects random)' \
'--seed[Seed for random order]:seed:' \
'--shard[Run shard i of n]:shard:' \
'--rerun-failed[Replay only the tests that failed on the last run]' \
Expand Down
6 changes: 5 additions & 1 deletion completions/bashunit.bash
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ _BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --changed --coverage --coverage
--debug --detailed --dry-run --env --exclude-filter --exclude-tag --fail-on-risky --failures-only \
--filter --help --jobs --list --list-format --log-gha --log-junit --login --no-color \
--no-coverage-report --no-output --no-output-on-failure --no-parallel \
--no-progress --no-snapshot-create --output --parallel --profile \
--no-progress --no-snapshot-create --order-by --output --parallel --profile \
--random-order --report-html \
--report-json --report-junit --report-tap --rerun-failed --retry --run-all \
--seed --shard --show-incomplete --show-output --show-skipped --simple \
Expand Down Expand Up @@ -80,6 +80,10 @@ _bashunit_completions() {
COMPREPLY=($(compgen -W "text json" -- "$cur"))
return 0
;;
--order-by)
COMPREPLY=($(compgen -W "defined defects random" -- "$cur"))
return 0
;;
-f | --filter | --exclude-filter | --tag | --exclude-tag | --retry | --seed | --shard | --test-timeout)
return 0
;;
Expand Down
41 changes: 41 additions & 0 deletions docs/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ bashunit test tests/ --parallel --simple
| `--test-timeout <seconds>` | Fail a test if it runs longer than N seconds |
| `--retry <n>` | Re-run a failed test up to N extra times |
| `--random-order` | Randomize test execution order |
| `--order-by <mode>` | Execution order: `defined` (default), `defects` or `random` |
| `--seed <n>` | Seed for `--random-order` (reproducible shuffle) |
| `--shard <i>/<n>` | Run shard i of n (split suite across runners) |
| `--rerun-failed` | Replay only the tests that failed on the last run |
Expand Down Expand Up @@ -821,6 +822,46 @@ BASHUNIT_RERUN_FAILED=true bashunit test tests/
```
:::

### Order by

> `bashunit test --order-by <mode>`

Choose the execution order. Three modes:

| Mode | Order |
|------|-------|
| `defined` | Definition order. The default. |
| `defects` | Tests that failed on the last recorded run first, then everything else. |
| `random` | Shuffled, the same mode `--random-order` selects. |

`defects` reads the same `.bashunit/last-failed` cache
[`--rerun-failed`](#rerun-failed) writes, but it **reorders instead of
narrowing**: the whole suite still runs. Paired with `--stop-on-failure`, that
turns the pre-push check from minutes into seconds, because the tests most
likely to fail run first.

```bash
bashunit test tests/ --order-by defects --stop-on-failure
```

Notes:

- With no cache file the order falls back to `defined`, silently.
- `--order-by random` and `--random-order` are the same mode, and `--seed`
applies to both.
- Combining it with `--rerun-failed` is allowed: `--rerun-failed` still narrows
the selection, `--order-by` only orders what survives.
- Under `--parallel` the recorded failures are dispatched first.

::: code-group
```bash [Fail fast on known-bad tests]
bashunit test --order-by defects --stop-on-failure
```
```bash [Env variable]
BASHUNIT_ORDER_BY=defects bashunit test tests/
```
:::

### Changed

> `bashunit test --changed [<ref>]`
Expand Down
16 changes: 15 additions & 1 deletion src/config/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ _BASHUNIT_DEFAULT_SHARD_INDEX=""
_BASHUNIT_DEFAULT_SHARD_TOTAL=""
# Replay only the tests recorded as failing by the previous run
_BASHUNIT_DEFAULT_RERUN_FAILED="false"
# Execution order: defined (definition order), defects (last run's failures
# first) or random (equivalent to --random-order)
_BASHUNIT_DEFAULT_ORDER_BY="defined"
# Run only the test files git reports as changed since a ref
_BASHUNIT_DEFAULT_CHANGED="false"
# The ref --changed diffs against (empty = origin/HEAD, then HEAD)
Expand Down Expand Up @@ -305,6 +308,8 @@ _BASHUNIT_DEFAULT_SNAPSHOT_REPORT_UNUSED="false"
# up unrelated environment values.
: "${BASHUNIT_RANDOM_ORDER:=$_BASHUNIT_DEFAULT_RANDOM_ORDER}"
: "${BASHUNIT_SEED:=$_BASHUNIT_DEFAULT_SEED}"
# No bare ORDER_BY alias, same reasoning as RETRY/SEED above.
: "${BASHUNIT_ORDER_BY:=$_BASHUNIT_DEFAULT_ORDER_BY}"
: "${BASHUNIT_SHARD_INDEX:=$_BASHUNIT_DEFAULT_SHARD_INDEX}"
: "${BASHUNIT_SHARD_TOTAL:=$_BASHUNIT_DEFAULT_SHARD_TOTAL}"
# No bare RERUN_FAILED alias, same reasoning as RETRY/SEED above. The default
Expand Down Expand Up @@ -377,8 +382,17 @@ function bashunit::env::retry_count() {
printf '%s' "$_BASHUNIT_RETRY_VALIDATED"
}

##
# --random-order and `--order-by random` are the same mode under two names, and
# either may arrive through the environment rather than the parser, so the
# predicate accepts both rather than one arm normalising into the other.
##
function bashunit::env::is_random_order_enabled() {
[ "$BASHUNIT_RANDOM_ORDER" = "true" ]
[ "$BASHUNIT_RANDOM_ORDER" = "true" ] || [ "${BASHUNIT_ORDER_BY:-defined}" = "random" ]
}

function bashunit::env::is_defects_order_enabled() {
[ "${BASHUNIT_ORDER_BY:-defined}" = "defects" ]
}

##
Expand Down
78 changes: 78 additions & 0 deletions src/config/rerun.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,84 @@ $file:$fn
return 1
}

##
# Echoes the given test files with the recorded ones first, in the order the
# cache holds them, then the rest in their given order.
#
# Reordering, not filtering: --order-by defects still runs the whole suite, it
# just puts the known-bad files where --stop-on-failure trips on them first.
# Arguments: $@ test file paths.
##
function bashunit::rerun::order_files() {
local recorded_files
recorded_files="$(bashunit::rerun::files)"
if [ -z "$recorded_files" ]; then
[ "$#" -gt 0 ] && printf '%s\n' "$@"
return 0
fi

local file recorded
# A recorded file the current selection does not contain is skipped, so a
# deleted or filtered-out file cannot resurrect itself through the cache.
while IFS= read -r recorded; do
[ -z "$recorded" ] && continue
for file in "$@"; do
if [ "$file" = "$recorded" ]; then
printf '%s\n' "$file"
break
fi
done
done <<EOF
$recorded_files
EOF

for file in "$@"; do
case "
$recorded_files
" in
*"
$file
"*) ;;
*) printf '%s\n' "$file" ;;
esac
done
}

##
# Echoes a space-separated function list reordered so the ones recorded for the
# file come first, in the order the cache holds them.
# Arguments: $1 test file path, $2 space-separated function names.
##
function bashunit::rerun::order_functions() {
local file=$1
local functions=$2
local ordered=""
local entry recorded_fn fn

while IFS= read -r entry; do
case "$entry" in
"$file":*) recorded_fn="${entry#"$file":}" ;;
*) continue ;;
esac
for fn in $functions; do
if [ "$fn" = "$recorded_fn" ]; then
ordered="$ordered $fn"
break
fi
done
done <<EOF
$_BASHUNIT_RERUN_ENTRIES
EOF

for fn in $functions; do
if ! bashunit::rerun::allows "$file" "$fn"; then
ordered="$ordered $fn"
fi
done

echo "${ordered# }"
}

##
# Filters a space-separated function list down to the ones recorded for a file.
# Arguments: $1 test file path, $2 space-separated function names.
Expand Down
1 change: 1 addition & 0 deletions src/console/header.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Options:
--test-timeout <seconds> Fail a test if it runs longer than N seconds (0 = off)
--retry <n> Re-run a failed test up to N extra times (0 = off)
--random-order Randomize test execution order
--order-by <mode> Execution order: defined (default), defects (last run's failures first) or random
--seed <n> Seed for --random-order (reproducible shuffle)
--shard <i>/<n> Run shard i of n (split the suite across runners)
--rerun-failed Replay only the tests that failed on the last run (.bashunit/last-failed)
Expand Down
5 changes: 5 additions & 0 deletions src/main/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ function bashunit::main::cmd_test() {
BASHUNIT_RANDOM_ORDER=true
export -n BASHUNIT_RANDOM_ORDER
;;
--order-by)
BASHUNIT_ORDER_BY="$2"
export -n BASHUNIT_ORDER_BY
shift
;;
--seed)
BASHUNIT_SEED="$2"
export -n BASHUNIT_SEED
Expand Down
11 changes: 11 additions & 0 deletions src/main/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,17 @@ function bashunit::main::validate_config_or_exit() {
;;
esac

# Same shape as --output above: an unrecognised mode would otherwise leave the
# suite in definition order and look like it was honoured.
case "${BASHUNIT_ORDER_BY:-defined}" in
defined | defects | random) ;;
*)
printf "%sError: unsupported order '%s' for --order-by. Supported: defined, defects, random.%s\n" \
"${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_ORDER_BY}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
exit 1
;;
esac

# Same shape as --output above: an unrecognised name would otherwise fall
# through to the default renderer and look like it worked.
case "${BASHUNIT_LIST_FORMAT:-}" in
Expand Down
12 changes: 12 additions & 0 deletions src/runner/discovery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ function bashunit::runner::load_test_files() {
local -a worker_stderr_owners=()
local worker_stderr_count=0

# --order-by defects: files holding last run's failures go first. The cache is
# only read here, never used to drop a file, so the full suite still runs.
if bashunit::env::is_defects_order_enabled; then
bashunit::rerun::load
local -a _defect_files=()
local _defect_file
while IFS= read -r _defect_file; do
[ -n "$_defect_file" ] && _defect_files[${#_defect_files[@]}]=$_defect_file
done < <(bashunit::rerun::order_files "${files[@]+"${files[@]}"}")
files=("${_defect_files[@]+"${_defect_files[@]}"}")
fi

# Randomize file execution order (deterministic for the resolved seed).
if bashunit::env::is_random_order_enabled; then
local -a _shuffled_files=()
Expand Down
9 changes: 9 additions & 0 deletions src/runner/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ function bashunit::runner::order_functions_for_script() {
ordered[${#ordered[@]}]="$fn"
done

if bashunit::env::is_defects_order_enabled && [ "${#ordered[@]}" -gt 1 ]; then
local -a _defect_fns=()
local _defect_fn
for _defect_fn in $(bashunit::rerun::order_functions "$script" "${ordered[*]+${ordered[*]}}"); do
_defect_fns[${#_defect_fns[@]}]=$_defect_fn
done
ordered=("${_defect_fns[@]+"${_defect_fns[@]}"}")
fi

if bashunit::env::is_random_order_enabled && [ "${#ordered[@]}" -gt 1 ]; then
local _base _crc _fn_seed
_base=$(bashunit::env::seed)
Expand Down
Loading
Loading