Skip to content
Open
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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@ jobs:
"$HOME/.local/bin/towel" update && true; test $? -eq 64
'

tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install bats
run: sudo apt-get update && sudo apt-get install -y bats
- name: Run unit tests
run: bats --print-output-on-failure tests

release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [shellcheck, shfmt, install-smoke]
needs: [shellcheck, shfmt, install-smoke, tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .releaserc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ plugins:
--exclude='*/node_modules' \
--exclude='./.pi' \
--exclude='./.opencode' \
--exclude='./tests' \
--transform "s,^\\.,towel-${nextRelease.version}," \
-czf "towel-${nextRelease.version}.tar.gz" .
sha256sum "towel-${nextRelease.version}.tar.gz" > "towel-${nextRelease.version}.tar.gz.sha256"
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ High-signal notes for OpenCode agents working in this repo.
- `shellcheck -x install user/local/share/towel/bin/towel user/local/share/towel/bin/towel-*`
- Format only shell scripts, not the whole repo:
- `shfmt -w install user/local/share/towel/bin/towel user/local/share/towel/bin/towel-*`
- Run the unit tests (bats-core; `apt-get install -y bats`):
- `bats --print-output-on-failure tests`
- Tests source the scripts under `user/local/share/towel/bin/`; `towel-update` is
source-safe via a `(return 0 2>/dev/null) && return 0` guard before its imperative tail.

## Style conventions observed in code
- Tabs are used for indentation in shell scripts.
Expand Down
46 changes: 46 additions & 0 deletions tests/cli.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bats
#
# Behavioural tests for the CLI entrypoints, run as real subprocesses. These
# lock in the documented exit-code contract (e.g. exit 64 on misuse) that the
# install smoke test partially relies on.

setup() {
load helpers/load
export TOWEL_DATA="$BATS_TEST_TMPDIR/data"
export TOWEL_BIN_DIR
# Keep the dispatcher's auto update check from doing any network I/O.
export TOWEL_NO_UPDATE_CHECK=1
mkdir -p "$TOWEL_DATA"
}

@test "towel update with no mode flag exits 64" {
run "$TOWEL_BIN_DIR/towel-update"
[ "$status" -eq 64 ]
[[ "$output" == *"a mode flag is required"* ]]
}

@test "towel update with an unknown argument exits 64" {
run "$TOWEL_BIN_DIR/towel-update" --definitely-not-a-flag
[ "$status" -eq 64 ]
[[ "$output" == *"Unknown argument"* ]]
}

@test "towel update --help exits 0 and prints usage" {
run "$TOWEL_BIN_DIR/towel-update" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}

@test "towel --version prints a semver-formatted line" {
printf '9.9.9' >"$TOWEL_DATA/VERSION"
run "$TOWEL_BIN_DIR/towel" --version
[ "$status" -eq 0 ]
[ "$output" = "towel 9.9.9" ]
}

@test "towel with an unknown command exits 1" {
printf '9.9.9' >"$TOWEL_DATA/VERSION"
run "$TOWEL_BIN_DIR/towel" not-a-command
[ "$status" -eq 1 ]
[[ "$output" == *"Unknown command"* ]]
}
98 changes: 98 additions & 0 deletions tests/common_helpers.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bats
#
# Unit tests for the pure/near-pure helpers in towel-common.

setup() {
load helpers/load
load_towel towel-common
}

@test "inside_towel: true when CONTAINER_ID=towel" {
export CONTAINER_ID=towel
run inside_towel
[ "$status" -eq 0 ]
}

@test "inside_towel: false when CONTAINER_ID is unset" {
unset CONTAINER_ID
run inside_towel
[ "$status" -ne 0 ]
}

@test "inside_towel: false for a different container" {
export CONTAINER_ID=somethingelse
run inside_towel
[ "$status" -ne 0 ]
}

@test "towel_exports_bin_dir is exports/bin under TOWEL_DATA" {
run towel_exports_bin_dir
[ "$status" -eq 0 ]
[ "$output" = "$TOWEL_DATA/exports/bin" ]
}

@test "towel_exports_apps_dir is exports/applications under TOWEL_DATA" {
run towel_exports_apps_dir
[ "$output" = "$TOWEL_DATA/exports/applications" ]
}

@test "towel_host_bin_dir is ~/.local/bin" {
local saved="$HOME"
HOME=/tmp/fakehome
run towel_host_bin_dir
HOME="$saved"
[ "$output" = "/tmp/fakehome/.local/bin" ]
}

@test "towel_host_apps_dir is ~/.local/share/applications" {
local saved="$HOME"
HOME=/tmp/fakehome
run towel_host_apps_dir
HOME="$saved"
[ "$output" = "/tmp/fakehome/.local/share/applications" ]
}

@test "towel_repoquery_exportables keeps bins and desktops, drops everything else" {
mkdir -p "$BATS_TEST_TMPDIR/bin"
cat >"$BATS_TEST_TMPDIR/bin/dnf" <<'EOF'
#!/usr/bin/env bash
# Emulate: dnf repoquery -l <pkg>
cat <<'LIST'
/usr/bin/foo
/usr/bin/bar
/usr/share/applications/foo.desktop
/usr/lib/foo/libfoo.so
/etc/foo/foo.conf
/usr/share/doc/foo/README
LIST
EOF
chmod +x "$BATS_TEST_TMPDIR/bin/dnf"

local saved="$PATH"
PATH="$BATS_TEST_TMPDIR/bin:$PATH"
run towel_repoquery_exportables foo
PATH="$saved"

[ "$status" -eq 0 ]
[[ "$output" == *"/usr/bin/foo"* ]]
[[ "$output" == *"/usr/bin/bar"* ]]
[[ "$output" == *"/usr/share/applications/foo.desktop"* ]]
[[ "$output" != *"/usr/lib/foo/libfoo.so"* ]]
[[ "$output" != *"/etc/foo/foo.conf"* ]]
[[ "$output" != *"/usr/share/doc/foo/README"* ]]
}

@test "towel_export_bin_wrapper creates an executable wrapper and host symlink" {
HOME="$BATS_TEST_TMPDIR/home"
mkdir -p "$HOME"

run towel_export_bin_wrapper /usr/bin/somecmd
[ "$status" -eq 0 ]

local wrapper="$TOWEL_DATA/exports/bin/somecmd"
[ -f "$wrapper" ]
[ -x "$wrapper" ]
grep -q "command: somecmd" "$wrapper"
grep -q 'towel" exec "somecmd"' "$wrapper"
[ -L "$HOME/.local/bin/somecmd" ]
}
23 changes: 23 additions & 0 deletions tests/helpers/load.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Shared helpers for the towel bats test suite.

REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
TOWEL_BIN_DIR="$REPO_ROOT/user/local/share/towel/bin"

# Source a towel script into the current test shell with an isolated TOWEL_DATA.
# Usage: load_towel towel-common / load_towel towel-update
load_towel() {
export TOWEL_DATA="${TOWEL_DATA:-$BATS_TEST_TMPDIR/data}"
export TOWEL_BIN_DIR

# towel/towel-update harden the shell with `enable`/`unalias`, which can
# return non-zero; bats runs setup under errexit, so disable it around the
# source (restoring it afterwards) to avoid an unrelated abort.
local errexit_was_set=0
case $- in *e*) errexit_was_set=1 ;; esac
set +e
# shellcheck source=/dev/null
source "$TOWEL_BIN_DIR/$1"
local rc=$?
[ "$errexit_was_set" -eq 1 ] && set -e
return "$rc"
}
51 changes: 51 additions & 0 deletions tests/update_compare.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bats
#
# Unit tests for _towel_update_compare (semver comparison). This is the
# function that decides whether a newer release exists, so a regression here
# is exactly the kind of bug that ships a "broken release".

setup() {
load helpers/load
load_towel towel-update
}

@test "compare: equal versions return eq" {
run _towel_update_compare 1.2.3 1.2.3
[ "$status" -eq 0 ]
[ "$output" = "eq" ]
}

@test "compare: lower patch returns lt" {
run _towel_update_compare 1.2.3 1.2.4
[ "$output" = "lt" ]
}

@test "compare: higher patch returns gt" {
run _towel_update_compare 1.2.4 1.2.3
[ "$output" = "gt" ]
}

@test "compare: lower minor returns lt" {
run _towel_update_compare 1.2.0 1.3.0
[ "$output" = "lt" ]
}

@test "compare: higher major returns gt" {
run _towel_update_compare 2.0.0 1.9.9
[ "$output" = "gt" ]
}

@test "compare: missing patch field defaults to zero (eq)" {
run _towel_update_compare 1.2 1.2.0
[ "$output" = "eq" ]
}

@test "compare: two-digit field compares numerically not lexically (1.10 > 1.9)" {
run _towel_update_compare 1.10.0 1.9.0
[ "$output" = "gt" ]
}

@test "compare: two-digit field, reversed (1.9 < 1.10)" {
run _towel_update_compare 1.9.0 1.10.0
[ "$output" = "lt" ]
}
4 changes: 4 additions & 0 deletions user/local/share/towel/bin/towel-update
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ function _towel_fetch_latest_version() {
sed -E 's/.*"v?([^"]+)"/\1/'
}

# When sourced (e.g. by the test suite) stop here and expose the functions
# above only; the imperative tail below runs solely on direct execution.
(return 0 2>/dev/null) && return 0

mode=""
assume_yes="false"
quiet="false"
Expand Down