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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
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
- `assert_true` and `assert_false` accept a command with its arguments β€” `assert_true test -d /tmp`. Arguments are passed through rather than re-parsed, so a value containing a space survives. A single argument keeps its previous meaning exactly, so existing calls are unaffected (#994)
- `assert_is_symlink`, `assert_is_not_symlink` and `assert_symlink_to` assert on a symbolic link itself. Every other filesystem assertion follows the link, so a link and its target were indistinguishable and a dangling link read as "does not exist" (#981)

### Added
Expand Down
25 changes: 14 additions & 11 deletions docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,21 +34,24 @@ to narrow it (`bashunit doc json`).
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command`
> `assert_true bool|function|command [args...]`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:

```bash
assert_true "my_function" # bare name: works
assert_true "eval test -d /tmp" # arguments: needs eval
assert_true "eval grep -q foo file" # arguments: needs eval
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix β€”
`assert_true "eval test -d /tmp"` β€” or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still β€” `assert_directory_exists`
rather than a hand-rolled `test -d`.

Expand DownExpand Up@@ -83,7 +86,7 @@ function mock_false() {
:::

## assert_false
> `assert_false bool|function|command`
> `assert_false bool|function|command [args...]`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
105 changes: 69 additions & 36 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,33 @@ function bashunit::fail() {
bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

##
# Runs the subject of assert_true / assert_false and leaves its exit code in
# _BASHUNIT_ASSERT_BOOL_EXIT_OUT.
#
# With more than one argument the arguments are passed through as arguments --
# no re-parsing, so a path containing a space survives. With exactly one they go
# through run_command_or_eval, which is the historical behaviour: a bare command
# word, or an `eval `-prefixed string. Keeping that split is what makes the
# variadic form purely additive.
#
# The `|| exit_code=$?` capture is deliberate: a bare failing command as a
# statement would abort the whole test under --strict (`set -e`).
##
function bashunit::assert::_run_bool_subject() {
local exit_code=0

if [ $# -gt 1 ]; then
"$@" >/dev/null 2>&1 || exit_code=$?
else
bashunit::run_command_or_eval "$1" || exit_code=$?
fi

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

##
Expand DownExpand Up@@ -158,25 +185,28 @@ function assert_true() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
# The literal values only mean themselves when they are the whole subject;
# with arguments following, "true" is the command named true.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
"true" | "0")
bashunit::state::add_assertions_passed
return
;;
"false" | "1")
bashunit::handle_bool_assertion_failure "true or 0" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

if [ "$exit_code" -ne 0 ]; then
bashunit::assert::_describe_exit_code "$exit_code" "$actual"
Expand All@@ -192,25 +222,28 @@ function assert_false() {

local actual="$1"

# Check for expected literal values first
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
# As in assert_true: the literal values only mean themselves when they are the
# whole subject.
if [ $# -eq 1 ]; then
case "$actual" in
"")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
"false" | "1")
bashunit::state::add_assertions_passed
return
;;
"true" | "0")
bashunit::handle_bool_assertion_failure "false or 1" "$actual"
return
;;
esac
fi

# Run command or eval and check the exit code
bashunit::run_command_or_eval "$actual"
local exit_code=$?
bashunit::assert::_run_bool_subject "$@"
local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
actual="$*"

# 127/126 mean the command never ran. Treating "did not run" as "returned
# false" let a typo in the command name satisfy this assertion while testing
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
## assert_true
--------------
> `assert_true bool|function|command`
> `assert_true bool|function|command args...`

The argument is run as a **single command word**, so it must be a bare command or
function name with no arguments. Anything with arguments β€” including a bracketed
condition of the form used inside `if` β€” is treated as one long command name, and
the assertion fails with `unknown command`.

Prefix with `eval` to run anything more than a bare name:
Pass a command with its arguments as separate arguments:


## assert_false
--------------
> `assert_false bool|function|command`
> `assert_false bool|function|command args...`

Reports an error if the argument result in a falsy value: `false` or `1`.

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/assert/basic_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,3 +217,42 @@ function test_assert_false_fails_when_the_command_does_not_exist() {
"unknown command: definitely_not_a_command")" \
"$(assert_false "definitely_not_a_command")"
}

# Arguments as real arguments. The single-argument forms are untouched: one
# argument still means "run this as a command word", so every existing call
# behaves exactly as before.
function test_assert_true_accepts_a_command_with_arguments() {
assert_empty "$(assert_true test -d /tmp)"
}

function test_assert_true_variadic_fails_when_the_command_fails() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic fails when the command fails" \
"command or function with zero exit code" "but got " \
"exit code: 1")" \
"$(assert_true test -d /definitely/not/a/directory)"
}

function test_assert_false_accepts_a_command_with_arguments() {
assert_empty "$(assert_false test -d /definitely/not/a/directory)"
}

function test_assert_true_variadic_does_not_re_parse_its_arguments() {
local dir
dir=$(bashunit::temp_dir)
# A path containing a space survives, because it is passed as one argument
# rather than re-split out of a single string.
mkdir -p "$dir/two words"

assert_empty "$(assert_true test -d "$dir/two words")"
}

function test_assert_true_variadic_reports_a_missing_command() {
assert_same \
"$(bashunit::console_results::print_failed_test \
"Assert true variadic reports a missing command" \
"command or function with zero exit code" "but got " \
"unknown command: definitely_not_a_command --flag")" \
"$(assert_true definitely_not_a_command --flag)"
}
Loading