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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Added
- `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
- Named snapshot assertions support multiple snapshots per test; mismatches show the resolved path and `--snapshot-update` hint (#986)

Expand Down
4 changes: 2 additions & 2 deletions completions/_bashunit
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ _bashunit() {
assert_greater_than assert_is_directory assert_is_directory_empty
assert_is_directory_not_empty assert_is_directory_not_readable
assert_is_directory_not_writable assert_is_directory_readable
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_is_directory_writable assert_is_file assert_is_file_empty assert_is_not_symlink assert_is_symlink
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
Expand All @@ -38,7 +38,7 @@ _bashunit() {
assert_not_same assert_same assert_string_ends_with
assert_string_matches_format assert_string_not_ends_with
assert_string_not_matches_format assert_string_not_starts_with
assert_string_starts_with assert_successful_code assert_true
assert_string_starts_with assert_successful_code assert_symlink_to assert_true
assert_unsuccessful_code assert_within_delta
)
compadd -a assert_fns
Expand Down
4 changes: 2 additions & 2 deletions completions/bashunit.bash
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ assert_files_not_equals assert_general_error assert_greater_or_equal_than \
assert_greater_than assert_is_directory assert_is_directory_empty \
assert_is_directory_not_empty assert_is_directory_not_readable \
assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_is_directory_writable assert_is_file assert_is_file_empty assert_is_not_symlink assert_is_symlink \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
Expand All @@ -51,7 +51,7 @@ assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
assert_string_matches_format assert_string_not_ends_with \
assert_string_not_matches_format assert_string_not_starts_with \
assert_string_starts_with assert_successful_code assert_true \
assert_string_starts_with assert_successful_code assert_symlink_to assert_true \
assert_unsuccessful_code assert_within_delta"

# compgen output is split into COMPREPLY words on purpose; mapfile/read -a would need bash 4+.
Expand Down
47 changes: 46 additions & 1 deletion docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ to narrow it (`bashunit doc json`).
| **Numbers** | [assert_less_than](#assert-less-than) Β· [assert_less_or_equal_than](#assert-less-or-equal-than) Β· [assert_greater_than](#assert-greater-than) Β· [assert_greater_or_equal_than](#assert-greater-or-equal-than) Β· [assert_within_delta](#assert-within-delta) |
| **Dates** | [assert_date_equals](#assert-date-equals) Β· [assert_date_before](#assert-date-before) Β· [assert_date_after](#assert-date-after) Β· [assert_date_within_range](#assert-date-within-range) Β· [assert_date_within_delta](#assert-date-within-delta) |
| **Exit codes and commands** | [assert_exit_code](#assert-exit-code) Β· [assert_successful_code](#assert-successful-code) Β· [assert_unsuccessful_code](#assert-unsuccessful-code) Β· [assert_general_error](#assert-general-error) Β· [assert_command_not_found](#assert-command-not-found) Β· [assert_exec](#assert-exec) |
| **Files** | [assert_file_exists](#assert-file-exists) Β· [assert_file_not_exists](#assert-file-not-exists) Β· [assert_file_contains](#assert-file-contains) Β· [assert_file_not_contains](#assert-file-not-contains) Β· [assert_is_file](#assert-is-file) Β· [assert_is_file_empty](#assert-is-file-empty) Β· [assert_file_permissions](#assert-file-permissions) Β· [assert_files_equals](#assert-files-equals) Β· [assert_files_not_equals](#assert-files-not-equals) |
| **Files** | [assert_file_exists](#assert-file-exists) Β· [assert_file_not_exists](#assert-file-not-exists) Β· [assert_file_contains](#assert-file-contains) Β· [assert_file_not_contains](#assert-file-not-contains) Β· [assert_is_file](#assert-is-file) Β· [assert_is_file_empty](#assert-is-file-empty) Β· [assert_is_symlink](#assert-is-symlink) Β· [assert_is_not_symlink](#assert-is-not-symlink) Β· [assert_symlink_to](#assert-symlink-to) Β· [assert_file_permissions](#assert-file-permissions) Β· [assert_files_equals](#assert-files-equals) Β· [assert_files_not_equals](#assert-files-not-equals) |
| **Directories** | [assert_directory_exists](#assert-directory-exists) Β· [assert_directory_not_exists](#assert-directory-not-exists) Β· [assert_is_directory](#assert-is-directory) Β· [assert_is_directory_empty](#assert-is-directory-empty) Β· [assert_is_directory_not_empty](#assert-is-directory-not-empty) Β· [assert_is_directory_readable](#assert-is-directory-readable) Β· [assert_is_directory_not_readable](#assert-is-directory-not-readable) Β· [assert_is_directory_writable](#assert-is-directory-writable) Β· [assert_is_directory_not_writable](#assert-is-directory-not-writable) |
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) Β· [assert_array_contains](#assert-array-contains) Β· [assert_array_not_contains](#assert-array-not-contains) Β· [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) Β· [assert_json_contains](#assert-json-contains) Β· [assert_json_key_exists](#assert-json-key-exists) |
Expand Down Expand Up @@ -911,6 +911,51 @@ function test_failure() {
```
:::

## assert_is_symlink
> `assert_is_symlink "path"`

Reports an error if `path` is not a symbolic link.

Every other filesystem assertion follows the link β€” `assert_is_file` and
`assert_file_exists` report on the *target*, so a link and the file it points at
look identical, and a dangling link reads as "does not exist". This is the
assertion that tells them apart, and it passes for a link whose target is gone.

::: code-group
```bash [Example]
function test_success() {
ln -s /etc/hosts ./hosts_link

assert_is_symlink "./hosts_link"
}
```
:::

## assert_is_not_symlink
> `assert_is_not_symlink "path"`

Reports an error if `path` is a symbolic link.

## assert_symlink_to
> `assert_symlink_to "expected_target" "path"`

Reports an error if `path` is not a symbolic link, or if it points somewhere
other than `expected_target`.

The target is compared **as written**, via `readlink`, not fully resolved: that
is what the test author wrote, and `readlink -f` is GNU-only. A relative link
therefore compares as the relative string it is.

::: code-group
```bash [Example]
function test_success() {
ln -s ./releases/42 ./current

assert_symlink_to "./releases/42" "./current"
}
```
:::

## assert_file_permissions
> `assert_file_permissions "mode" "file"`

Expand Down
79 changes: 79 additions & 0 deletions src/assert/files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,82 @@ function assert_file_permissions() {

bashunit::state::add_assertions_passed
}

##
# Whether $1 is a symbolic link, regardless of whether its target resolves.
#
# Every other filesystem assertion here follows the link: -f and -e report on
# the target, so a link and the file it points at are indistinguishable, and a
# dangling link reads as "does not exist" -- the same answer as a path that was
# never created. This is the one that can tell them apart.
#
# Arguments: $1 - path, $2 - unused, $3 - optional label override
##
function assert_is_symlink() {
bashunit::assert::should_skip && return 0

local expected="$1"

if [ ! -L "$expected" ]; then
bashunit::assert::fail_with "${3:-}" "${expected}" "to be a symlink" "but is not a symlink"
return
fi

bashunit::state::add_assertions_passed
}

##
# Whether $1 exists and is not a symbolic link.
# Arguments: $1 - path, $2 - unused, $3 - optional label override
##
function assert_is_not_symlink() {
bashunit::assert::should_skip && return 0

local expected="$1"

if [ -L "$expected" ]; then
bashunit::assert::fail_with "${3:-}" "${expected}" "not to be a symlink" "but is a symlink"
return
fi

bashunit::state::add_assertions_passed
}

##
# Whether $2 is a symlink pointing at $1.
#
# Compares the target as written, via `readlink`, not the fully resolved path.
# Two reasons: it is what the test author wrote in the first place, so a failure
# names something they recognise; and `readlink -f` is GNU-only, so resolving
# would need a second implementation for BSD/macOS. A relative link therefore
# compares as the relative string it is.
#
# Arguments: $1 - expected target, $2 - path, $3 - optional label override
##
function assert_symlink_to() {
bashunit::assert::should_skip && return 0

if [ $# -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected_target, path" "$#"
return 2
fi

local expected="$1"
local path="$2"

if [ ! -L "$path" ]; then
bashunit::assert::fail_with "${3:-}" "${path}" "to be a symlink" "but is not a symlink"
return
fi

local actual
actual=$(readlink "$path")

if [ "$actual" != "$expected" ]; then
bashunit::assert::fail_with "${3:-}" "${expected}" \
"to be the target of ${path}, but got " "${actual}"
return
fi

bashunit::state::add_assertions_passed
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,31 @@ Reports an error if `file` does not contains the search string.
- assert_file_not_contains is the inverse of this assertion and takes the same arguments.


## assert_is_symlink
--------------
> `assert_is_symlink "path"`

Reports an error if `path` is not a symbolic link.

Every other filesystem assertion follows the link β€” `assert_is_file` and
`assert_file_exists` report on the *target*, so a link and the file it points at
look identical, and a dangling link reads as "does not exist". This is the
assertion that tells them apart, and it passes for a link whose target is gone.


## assert_is_not_symlink
## assert_symlink_to
--------------
> `assert_symlink_to "expected_target" "path"`

Reports an error if `path` is not a symbolic link, or if it points somewhere
other than `expected_target`.

The target is compared **as written**, via `readlink`, not fully resolved: that
is what the test author wrote, and `readlink -f` is GNU-only. A relative link
therefore compares as the relative string it is.


## assert_file_permissions
--------------
> `assert_file_permissions "mode" "file"`
Expand Down
116 changes: 116 additions & 0 deletions tests/unit/assert/files_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,119 @@ function test_unsuccessful_assert_file_permissions_when_file_does_not_exist() {
"$file" "to have permissions 644" "but the file does not exist")" \
"$(assert_file_permissions "644" "$file")"
}

# Symlinks are invisible to every other filesystem assertion: -f and -e follow
# the link, so a link and its target look identical, and a dangling link reads
# as "does not exist" -- the same as a path that was never created.
function symlink_fixture_dir() {
bashunit::temp_dir
}

function test_successful_assert_is_symlink() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/target"
ln -s "$dir/target" "$dir/link"

assert_empty "$(assert_is_symlink "$dir/link")"
}

# The case nothing covers today: a link whose target is gone is still a link.
function test_successful_assert_is_symlink_when_the_target_is_missing() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
ln -s "$dir/never_created" "$dir/dangling"

assert_empty "$(assert_is_symlink "$dir/dangling")"
}

function test_unsuccessful_assert_is_symlink_on_a_regular_file() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/plain"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert is symlink on a regular file" \
"$dir/plain" "to be a symlink" "but is not a symlink")" \
"$(assert_is_symlink "$dir/plain")"
}

function test_successful_assert_is_not_symlink() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/plain"

assert_empty "$(assert_is_not_symlink "$dir/plain")"
}

function test_unsuccessful_assert_is_not_symlink() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/target"
ln -s "$dir/target" "$dir/link"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert is not symlink" \
"$dir/link" "not to be a symlink" "but is a symlink")" \
"$(assert_is_not_symlink "$dir/link")"
}

function test_successful_assert_symlink_to() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/target"
ln -s "$dir/target" "$dir/link"

assert_empty "$(assert_symlink_to "$dir/target" "$dir/link")"
}

function test_unsuccessful_assert_symlink_to_reports_both_targets() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/target"
ln -s "$dir/target" "$dir/link"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert symlink to reports both targets" \
"$dir/other" "to be the target of ${dir}/link, but got " "$dir/target")" \
"$(assert_symlink_to "$dir/other" "$dir/link")"
}

function test_unsuccessful_assert_symlink_to_on_a_regular_file() {
if bashunit::check_os::is_windows; then
bashunit::skip && return
fi
local dir
dir=$(symlink_fixture_dir)
: >"$dir/plain"

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert symlink to on a regular file" \
"$dir/plain" "to be a symlink" "but is not a symlink")" \
"$(assert_symlink_to "$dir/whatever" "$dir/plain")"
}
Loading