Summary
There is no way to assert anything about a symlink. Every existing filesystem assertion follows the link, so a link and its target are indistinguishable, and a broken link is indistinguishable from a path that was never there.
Reproduced on main:
function test_symlink_passes_every_file_assertion() {
local d; d=$(bashunit::temp_dir)
printf 'x' > "$d/target"; ln -s "$d/target" "$d/link"
assert_is_file "$d/link" # passes — -f follows the link
assert_file_exists "$d/link" # passes
ln -s "$d/missing" "$d/broken"
assert_file_not_exists "$d/broken" # passes — a broken link "does not exist"
}
All three pass. Nothing in the 76-assertion catalogue can tell you that $d/link is a link, where it points, or that $d/broken is a dangling link rather than an absent file.
Why this matters
Symlinks are the payload of a large share of what people write bash for: install scripts, dotfile managers, update-alternatives-style switching, release layouts with a current -> releases/N pointer. For those, "the link exists and points at the right place" is the assertion, and today it has to be written as:
assert_true "[ -L \"$path\" ]" # …which does not work, see #<this repo's assert_true issue>
assert_same "$target" "$(readlink "$path")" # works, but loses the failure message
The second form works and is what people will fall back to. It just reports Expected '/a/b' but got '/a/c' with no indication that a symlink was involved.
Proposal
Three assertions, matching the existing assert_is_* naming:
| Assertion |
Passes when |
assert_is_symlink "$path" |
$path is a symbolic link (-L), regardless of whether the target resolves |
assert_is_not_symlink "$path" |
$path exists and is not a symbolic link |
assert_symlink_to "$expected_target" "$path" |
$path is a link and its target matches |
Argument order for the third follows assert_same "expected" "actual" — expected first — which is the majority convention in this catalogue.
Open question worth deciding rather than guessing: should assert_symlink_to compare the literal target (readlink) or the resolved one (readlink -f)? They differ for relative links, and readlink -f is not portable to macOS's BSD readlink. My suggestion is literal readlink, documented as such, because that is what the test author wrote in the first place.
A dangling link is deliberately still a link for assert_is_symlink — separating "is a link" from "the target resolves" is the distinction that is missing today.
Constraints
- Bash 3.0+.
[ -L ] is fine. readlink is one fork; per-assertion paths are meant to stay fork-free, but there is no pure-bash way to read a link target, so one fork is inherent here — the same category as stat in assert_file_permissions. Note readlink -f is GNU-only; BSD/macOS needs readlink without -f or stat -f %Y.
- Windows/Git Bash: symlink creation often needs privileges and silently produces a copy. Guard the new tests with the same
is_windows skip the permission tests already use — see tests/unit/assert/files_test.sh for the pattern.
- Public API addition: needs
docs/assertions.md entries, and the bashunit doc snapshot regenerated.
- New assertions must appear in both completion scripts or
tests/unit/main/completions_test.sh fails.
CHANGELOG.md entry under ## Unreleased → ### Added.
Acceptance criteria
Summary
There is no way to assert anything about a symlink. Every existing filesystem assertion follows the link, so a link and its target are indistinguishable, and a broken link is indistinguishable from a path that was never there.
Reproduced on
main:All three pass. Nothing in the 76-assertion catalogue can tell you that
$d/linkis a link, where it points, or that$d/brokenis a dangling link rather than an absent file.Why this matters
Symlinks are the payload of a large share of what people write bash for: install scripts, dotfile managers,
update-alternatives-style switching, release layouts with acurrent -> releases/Npointer. For those, "the link exists and points at the right place" is the assertion, and today it has to be written as:The second form works and is what people will fall back to. It just reports
Expected '/a/b' but got '/a/c'with no indication that a symlink was involved.Proposal
Three assertions, matching the existing
assert_is_*naming:assert_is_symlink "$path"$pathis a symbolic link (-L), regardless of whether the target resolvesassert_is_not_symlink "$path"$pathexists and is not a symbolic linkassert_symlink_to "$expected_target" "$path"$pathis a link and its target matchesArgument order for the third follows
assert_same "expected" "actual"— expected first — which is the majority convention in this catalogue.Open question worth deciding rather than guessing: should
assert_symlink_tocompare the literal target (readlink) or the resolved one (readlink -f)? They differ for relative links, andreadlink -fis not portable to macOS's BSDreadlink. My suggestion is literalreadlink, documented as such, because that is what the test author wrote in the first place.A dangling link is deliberately still a link for
assert_is_symlink— separating "is a link" from "the target resolves" is the distinction that is missing today.Constraints
[ -L ]is fine.readlinkis one fork; per-assertion paths are meant to stay fork-free, but there is no pure-bash way to read a link target, so one fork is inherent here — the same category asstatinassert_file_permissions. Notereadlink -fis GNU-only; BSD/macOS needsreadlinkwithout-forstat -f %Y.is_windowsskip the permission tests already use — seetests/unit/assert/files_test.shfor the pattern.docs/assertions.mdentries, and thebashunit docsnapshot regenerated.tests/unit/main/completions_test.shfails.CHANGELOG.mdentry under## Unreleased→### Added.Acceptance criteria
assert_is_symlinkpasses on a link, fails on a regular file and on a missing pathassert_is_symlinkpasses on a dangling link (this is the case nothing covers today)assert_is_not_symlinkpasses on a regular file, fails on a linkassert_symlink_tocompares the target and fails with both paths in the messagedocs/assertions.md+ regenerated doc snapshot + both completion scriptsmake sa·make lint·./bashunit --parallel --simple --strict tests/·bash build.sh bin -v