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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
## Unreleased

### Added
- `bashunit doc --custom` lists the assertions your own project defines, rendering the comment block above each one; plain `bashunit doc` appends them as a "Custom assertions" section. Needs `--boot` / `BASHUNIT_BOOTSTRAP`, which `bashunit doc` now accepts (#918)
- `bashunit::assert_once <label> <actual>` makes a composed custom assertion count and report once instead of once per inner step, with its own label rather than the internal step's message. Opt-in, so existing totals are unchanged (#917)
- `assert_assertion_passes`, `assert_assertion_fails` and `assert_assertion_fails_with <message>` test a custom assertion for the verdict it reports. The inner assertion runs isolated β€” its counters, output and stop-on-failure guard are restored β€” so testing a failing assertion no longer means rebuilding the expected string from `bashunit::console_results::print_failed_test` (#916)
- `bashunit::assert_that <expected> <actual> <cmd> [args...]` writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so the two counters can no longer drift apart by a forgotten `return` or a missing `bashunit::assertion_passed` (#915)
Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ _bashunit() {
fi

_arguments \
'--custom[Show only the assertions your project defines]' \
'(-a --assert)'{-a,--assert}'[Run a standalone assert function]:function:' \
'(-e --env --boot)'{-e,--env,--boot}'[Load a custom env/bootstrap file]:file:_files' \
'(-f --filter)'{-f,--filter}'[Only run tests matching the name]:name:' \
Expand Down
12 changes: 12 additions & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@

_BASHUNIT_COMPLETIONS_SUBCOMMANDS="test bench doc init learn upgrade assert watch"

# Flags accepted by the doc subcommand.
_BASHUNIT_COMPLETIONS_DOC_OPTS="--custom -e --env --boot -h --help"

_BASHUNIT_COMPLETIONS_TEST_OPTS="--assert --boot --coverage --coverage-exclude \
--coverage-min --coverage-paths --coverage-report --coverage-report-html \
--debug --detailed --env --exclude-tag --fail-on-risky --failures-only \
Expand DownExpand Up@@ -82,6 +85,15 @@ _bashunit_completions() {
return 0
fi

# `bashunit doc <filter>` completes assertion names, plus its own flags.
if [ "$COMP_CWORD" -ge 2 ] && [ "${COMP_WORDS[1]}" = "doc" ]; then
case "$cur" in
-*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_DOC_OPTS" -- "$cur")) ;;
*) COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_ASSERT_FNS" -- "$cur")) ;;
esac
return 0
fi

# First word: subcommands (plus flags, since `test` is the default command).
if [ "$COMP_CWORD" -eq 1 ] && [ "${cur#-}" = "$cur" ]; then
COMPREPLY=($(compgen -W "$_BASHUNIT_COMPLETIONS_SUBCOMMANDS" -- "$cur"))
Expand Down
16 changes: 14 additions & 2 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ bashunit test [path] [options] # Run tests (default)
bashunit bench [path] [options] # Run benchmarks
bashunit watch [path] [options] # Watch files, re-run tests on change
bashunit assert <fn> <args> # Run standalone assertion
bashunit doc [filter] # Show assertion documentation
bashunit doc [options] [filter] # Show assertion documentation
bashunit init [dir] # Initialize test directory
bashunit learn # Interactive tutorial
bashunit upgrade # Upgrade to latest version
Expand DownExpand Up@@ -890,10 +890,19 @@ also uses polling.

## doc

> `bashunit doc [filter]`
> `bashunit doc [options] [filter]`

Display documentation for assertion functions.

| Option | Description |
|--------|-------------|
| `--custom` | Show only the assertions your project defines |
| `-e, --env, --boot <file>` | Load a bootstrap file defining custom assertions |

With a bootstrap loaded, `bashunit doc` appends a **Custom assertions** section
rendering the comment block above each of your own `assert_*` functions. See
[Custom asserts](/custom-asserts).

::: code-group
```bash [Examples]
# Show all assertions
Expand All@@ -904,6 +913,9 @@ bashunit doc equals

# Show file-related assertions
bashunit doc file

# Show only your project's assertions
bashunit doc --custom --boot tests/bootstrap.sh
```
```[Output]
## assert_equals
Expand Down
36 changes: 36 additions & 0 deletions docs/custom-asserts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,42 @@ source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"
See [Configuration](/configuration) and [Command line](/command-line) for the
full bootstrap options.

## Listing your assertions

`bashunit doc` prints the built-in catalogue. Once a bootstrap defines your own
assertions, it appends them too, rendering the comment block above each one:

```bash
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom http # ...narrowed by a filter
```

```
## assert_http_success
--------------
Asserts that the status code is a 2xx.
```

With `BASHUNIT_BOOTSTRAP` set, the `--boot` flag can be omitted. A bootstrap is
required either way: it is the only point at which your assertions are
guaranteed loaded, which is another reason to prefer it over sourcing from
`set_up`.

Write the docstring as a plain comment block immediately above the function β€”
the same shape the built-ins use:

```bash
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"

assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}
```

## Best practices

1. **Prefer `bashunit::assert_that`**: one call marks the assertion passed or failed, so you cannot forget the `return` after a failure (which would bump both counters) or forget `bashunit::assertion_passed` (which would leave the test with zero assertions, reported as risky).
Expand Down
7 changes: 6 additions & 1 deletion src/console_header.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,17 +208,22 @@ EOF

function bashunit::console_header::print_doc_help() {
cat <<EOF
Usage: bashunit doc [filter]
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter Optional filter to show only matching assertions

Options:
--custom Show only the assertions your project defines
-e, --env, --boot <file> Load a bootstrap file defining custom assertions

Examples:
bashunit doc Show all assertions
bashunit doc equals Show assertions containing 'equals'
bashunit doc file Show file-related assertions
bashunit doc --custom Show only your project's own assertions
EOF
}

Expand Down
140 changes: 140 additions & 0 deletions src/doc.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,143 @@ function bashunit::doc::print_asserts() {
}
'
}

##
# Collects the assert_* functions a bootstrap defined, i.e. those declared now
# that were not declared before it was sourced, into
# _BASHUNIT_DOC_CUSTOM_FNS_OUT (newline separated, sorted).
#
# Diffing against a snapshot rather than a hardcoded list means the built-in set
# never has to be maintained in two places.
# Arguments: $1 - newline separated assert_* names known before the bootstrap
##
_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
local known="$1"
# Declare and assign separately: bash 3.0 does not expand a compound array
# assignment attached to `local`.
local -a found
found=()
local count=0
local fn

for fn in $(compgen -A function assert_ 2>/dev/null); do
case "
$known
" in
*"
$fn
"*) continue ;;
esac

# Insertion sort. A project's own assertion list is tiny, so this beats a
# `sort` fork -- and `LC_ALL=C sort` is banned in src/ because bash 5.3.9 on
# macOS segfaults on that prefix inside a command substitution (#912).
local i=$count
while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
found[i]=${found[$((i - 1))]}
i=$((i - 1))
done
found[i]=$fn
count=$((count + 1))
done

local out=""
local j=0
while [ "$j" -lt "$count" ]; do
out="$out${found[j]}"$'\n'
j=$((j + 1))
done

_BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

##
# Prints the leading comment block of a function, with the comment markers
# stripped. Silent when the function has no comment or was defined somewhere
# unreadable (e.g. sourced from a process substitution).
# Arguments: $1 - function name
##
function bashunit::doc::print_fn_comment() {
local fn="$1"

# extdebug is toggled inside the subshell only: enabling it in the caller's
# shell clobbers caller state (#808).
local info
info="$(
shopt -s extdebug
declare -F "$fn"
)"

local rest="${info#* }"
local def_line="${rest%% *}"
local file="${rest#* }"

case "$def_line" in
'' | *[!0-9]*) return 0 ;;
esac
[ -n "$file" ] && [ -f "$file" ] || return 0

# Read the file once into an array; walking backwards needs random access and
# a per-line `sed -n Np` loop is exactly the quadratic pattern #807 removed.
local -a lines
lines=()
local count=0
local line
while IFS= read -r line || [ -n "$line" ]; do
lines[count]="$line"
count=$((count + 1))
done <"$file"

# Collect the comment run immediately above the definition, then print it in
# source order.
local first=$((def_line - 1))
local i=$((first - 1))
while [ "$i" -ge 0 ]; do
case "${lines[i]:-}" in
'#'*) i=$((i - 1)) ;;
*) break ;;
esac
done

local j=$((i + 1))
while [ "$j" -lt "$first" ]; do
line="${lines[j]:-}"
# Strip the marker: "## " fences render as blank, "# text" as "text".
line="${line#\#}"
line="${line#\#}"
line="${line# }"
printf '%s\n' "$line"
j=$((j + 1))
done
}

##
# Prints the custom assertions defined by a bootstrap, in the shape
# bashunit::doc::print_asserts already uses.
# Arguments: $1 - optional filter
# Returns: 0 when at least one was printed, 1 when there were none
##
function bashunit::doc::print_custom_asserts() {
local filter="${1:-}"
local printed=1
local fn

for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
if [ -n "$filter" ]; then
case "$fn" in
*"$filter"*) ;;
*) continue ;;
esac
fi

printf '## %s\n' "$fn"
printf -- '--------------\n'
bashunit::doc::print_fn_comment "$fn"
printf '\n'
printed=0
done

return $printed
}
61 changes: 55 additions & 6 deletions src/main.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -733,14 +733,63 @@ function bashunit::main::cmd_bench() {
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
case "${1:-}" in
-h | --help)
bashunit::console_header::print_doc_help
local filter=""
local custom_only=false
local boot_file="${BASHUNIT_BOOTSTRAP:-}"

while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
bashunit::console_header::print_doc_help
exit 0
;;
--custom)
custom_only=true
shift
;;
-e | --env | --boot)
boot_file="${2:-}"
shift 2
;;
*)
filter="$1"
shift
;;
esac
done

# Snapshot before sourcing: whatever assert_* appears afterwards is the
# project's own.
local known
known="$(compgen -A function assert_ 2>/dev/null)"

if [ -n "$boot_file" ]; then
if [ ! -r "$boot_file" ]; then
printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
"$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
exit 1
fi
# shellcheck disable=SC1090,SC2086
source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
fi

bashunit::doc::custom_fns_to_slot "$known"

if [ "$custom_only" = true ]; then
if ! bashunit::doc::print_custom_asserts "$filter"; then
printf 'No custom assertions found.\n'
printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
fi
exit 0
;;
esac
fi

bashunit::doc::print_asserts "$filter"

if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
printf '\n## Custom assertions\n\n'
bashunit::doc::print_custom_asserts "$filter" || true
fi

bashunit::doc::print_asserts "${1:-}"
exit 0
}

Expand Down
Loading
Loading