test suite with docker container and linter

This commit is contained in:
2026-09-05 20:57:59 +02:00
parent d32a30f39e
commit aaecbd8472
9 changed files with 777 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# Keep image builds light: the runtime mounts the live repo via -v.
# NOTE: do NOT exclude test/ - the dev stage COPYs the repo and then runs
# test/lint.sh from it.
.git
**/node_modules
**/lazy-lock.json
**/fish_variables
kreation/.vst3
term/.config/tmux/plugins
+3 -3
View File
@@ -7,15 +7,15 @@ Only entries that would be missed without explicit help are included.
## 1. Project Scope
- **Dotfiles** for a personal workstation (hyprland + Neovim + VST plugin).
- No tests, CI or build scripts the repo is purely configuration data.
- The only executable content is the `install_*` shell scripts that install system packages.
- No CI or build scripts the repo is purely configuration data; the only executable content is the `install_*` scripts and the `test/` harness.
- **Testing** lives in `test/`. Run `make test-lint` / `make test-stow` (offline, no Docker) or `make test-smoke` (Docker-based, see `test/Dockerfile` and `README.md → Testing`). The harness currently surfaces pre-existing Makefile bugs by design (missing `/` in `install_*` script paths, `stow nvim` with no `nvim/` package).
---
## 2. System Setup
| Task | Command | Why it matters |
|------|---------|----------------|
| **Install required programs** | `bash term/setup.sh` <br>`bash .install_skripte/install_basic_pcks.sh` <br>`bash .install_skripte/install_hypr_pcks.sh` <br>`bash .install_skripte/install_kreation_pcks.sh` | These scripts install Neovim, hyprland, VST runtime, and other utilities. Edit `term/packages.txt` to add/remove terminal packages. |
| **Install required programs** | `bash term/setup.sh` <br>`bash _install_skripte/install_basic_pcks.sh` <br>`bash _install_skripte/install_hypr_pcks.sh` <br>`bash _install_skripte/install_kreation_pcks.sh` | These scripts install Neovim, hyprland, VST runtime, and other utilities. Edit `term/packages.txt` to add/remove terminal packages. |
| **Link configuration directories** | `ln -s ~/.dotfiles/hypr/.config/hypr ~/.config/hypr` <br>`ln -s ~/.dotfiles/term/.config/nvim ~/.config/nvim` | The repository expects the configs to live in `~/.config`. |
| **Add VST plugin** | Copy `kreation/.vst3/MT-PowerDrumKit.vst3` into your DAWs plugins folder (usually `~/.vst`). | The repo contains a prebuilt VST for use in audio software. |
+41
View File
@@ -19,3 +19,44 @@ kreation: basic
stow kreation
bunters-monster: basic hypr kreation
_install_skripte/buntes-monster-links.sh
# ---------------------------------------------------------------------------
# Testing (see README.md "Testing" and test/ for details)
# ---------------------------------------------------------------------------
.PHONY: test test-lint test-smoke test-stow test-shellcheck test-lint-docker
# Full test suite: fast offline lint first, then the optional Docker smoke.
test: test-lint test-smoke
# Fast, offline integrity checks. No Docker required.
# shellcheck is used if installed, otherwise only syntax checks run.
test-lint:
bash test/lint.sh
# Check stow package resolution without creating symlinks.
test-stow:
bash test/probe_stow.sh
# Run shellcheck over all tracked repo scripts (needs shellcheck installed).
test-shellcheck:
shellcheck -S warning $$(git ls-files '*.sh' | grep -v '/tpm/')
# Test shellcheck: build the /dev stage with shellcheck and run lint.sh.
# docker build context is the repo root; .dockerignore excludes heavy dirs.
test-lint-docker:
docker build --target dev -t dotfiles-lint -f test/Dockerfile .
docker run --rm dotfiles-lint
# Docker smoke test: builds the Arch container and runs the dry probes.
# RUN_SMOKE=1 also installs the real pacman packages inside the container.
# RUN_SMOKE=1 RUN_FULL=1 additionally attempts AUR/npm + hypr/kreation.
# SKIP_NET=1 skips anything that contacts the network.
# Example: make test-smoke RUN_SMOKE=1
test-smoke:
docker build --target test -t dotfiles-test -f test/Dockerfile .
docker run --rm \
-e RUN_SMOKE="$${RUN_SMOKE:-0}" \
-e RUN_FULL="$${RUN_FULL:-0}" \
-e SKIP_NET=$${SKIP_NET:-0} \
-v "$$PWD":/dotfiles \
dotfiles-test
+54
View File
@@ -9,7 +9,9 @@ Personal configuration files for an Arch Linux workstation (Hyprland + Neovim +
| `term/` | Fish + kitty + starship + nvim + tmux + yazi + fastfetch + opencode terminal environment |
| `hypr/` | Hyprland compositor config, kitty theme, wpaperd wallpapers |
| `kreation/`| VST plugin (`MT-PowerDrumKit.vst3`) |
| `local_ai/`| Local AI / MCP tooling config |
| `_install_skripte/` | Miscellaneous install helper scripts |
| `test/` | Docker test harness (lint + smoke tests) |
Each top-level directory is a **stow package**. `stow` symlinks the files from the repository into your home directory (e.g. `term/.config/...``~/.config/...`).
@@ -125,4 +127,56 @@ stow --ignore='\.config/(local|misc)' term
---
## Testing
The `test/` directory contains a Docker-based test harness that verifies the
install scripts, the `Makefile`, and the stow packages.
There are **two tiers**:
| Target | What it does | Docker required? |
|--------|--------------|------------------|
| `make test-lint` | Fast, offline integrity checks: `bash -n` syntax pass over every script, `shellcheck` (if installed), `packages.txt` marker validation, and Makefile ↔ script/stow-package name consistency. Catches typos like `_install_skripte_install_basic_pcks.sh` (missing `/`). | No |
| `make test-stow` | `stow --simulate` for every stow-able package; fails if a package the Makefile stows doesn't exist (e.g. `stow nvim` — nvim lives under `term/`). | No |
| `make test-shellcheck` | Runs shellcheck over all tracked scripts (shellcheck must be installed). | No |
| `make test-lint-docker` | Builds the `/dev` stage (`archlinux:latest` + shellcheck) and runs `test/lint.sh` inside it. | Yes |
| `make test-smoke` | Builds the `/test` stage and runs the container's dry probes: `make -n` over every target, `stow --simulate` over every Makefile-stowed package, plus `test/lint.sh`. A network-enabled `RUN_SMOKE=1` variant installs the real pacman packages. | Yes |
| `make test` | `test-lint` + `test-smoke`. | Partially |
### Docker smoke details
```bash
make test-smoke # dry probes only (fast, no installs)
make test-smoke RUN_SMOKE=1 # + real pacman installs of the term packages
make test-smoke RUN_SMOKE=1 RUN_FULL=1 # + AUR/npm + hypr/kreation/buntes-monster
make test-smoke SKIP_NET=1 # skip anything that contacts the network
```
The container:
- uses `archlinux:latest` (the scripts assume Arch + pacman),
- creates a non-root user (`mathias`, uid 1000) with passwordless sudo so the
scripts don't hang on a password prompt,
- mounts the live repo at `/dotfiles` (`-v "$PWD":/dotfiles`), so the tests
always run against your current checkout,
- stubs host daemon commands (`systemctl`, `updatedb`, `kbuildsycoca6`,
`dbus-update-activation-environment`) as no-ops so nothing talks to a real
systemd/dbus inside the container.
In the `RUN_SMOKE=1` install path, the harness additionally **verifies the
installed packages** (`test/verify_packages.sh`):
- every `@pacman` entry is confirmed via `pacman -Q <pkg>` (covers fonts,
libs and python modules that ship no binary),
- a curated mapping probes each package's binary with a real
`--version`-style call (e.g. `fish --version`, `nvim --version`,
`ssh -V`), failing if the binary is missing or does not report a version.
> **Note for the current repo state:** `make test-lint` and `make test-smoke`
> currently **fail** because the harness surfaces pre-existing Makefile bugs
> (missing `/` in the `install_*` script paths and `stow nvim` for a
> non-existent `nvim/` package). That is the intended behaviour — fix the
> Makefile and the tests should go green.
---
**When in doubt:** run `stow --verbose --simulate <target>` first — it prints what would happen (linking) without changing anything. Never `rm -rf` the `~/.config` of something you still want to keep; move it to a backup directory instead.
+73
View File
@@ -0,0 +1,73 @@
# test/Dockerfile
#
# Docker test harness for the dotfiles repo.
#
# Two stages:
# --target dev : fast shellcheck/lint environment (no heavy installs)
# --target test : smoke-test environment that can run the install scripts
#
# Build with (repo root as context; .dockerignore trims heavy/binary dirs):
# docker build --target test -t dotfiles-test -f test/Dockerfile .
# Run with:
# docker run --rm -v "$PWD":/dotfiles dotfiles-test
#
# Or simply use the Makefile targets:
# make test-lint-docker # build dev stage, run shellcheck+lint
# make test-smoke # dry probes; add RUN_SMOKE=1 for real installs
#
# See README.md ("Testing") and test/entrypoint-test.sh for details.
FROM archlinux:latest AS base
# Non-root test user that mirrors the workstation account.
ARG UID_ARG=1000
ARG USERNAME=mathias
RUN pacman -Syu --noconfirm \
base-devel \
git \
stow \
curl \
which \
sudo \
&& pacman -Scc --noconfirm
RUN useradd -m -u "$UID_ARG" -s /bin/bash -G wheel "$USERNAME"
# Passwordless sudo so scripts that call `sudo pacman`, `sudo systemctl`, etc.
# do not hang on a password prompt inside the container.
RUN printf '%%wheel ALL=(ALL) NOPASSWD: ALL\n' > /etc/sudoers.d/wheel-nopasswd \
&& chmod 440 /etc/sudoers.d/wheel-nopasswd
USER "$USERNAME"
ENV HOME="/home/$USERNAME"
WORKDIR /dotfiles
# ---------------------------------------------------------------------------
# dev stage: shellcheck/lint only. Fast, no install heavy-lifting.
# ---------------------------------------------------------------------------
FROM base AS dev
USER root
RUN pacman -S --noconfirm --needed shellcheck
USER "$USERNAME"
WORKDIR /dotfiles
COPY --chown="$USERNAME" . /dotfiles
CMD ["bash", "test/lint.sh"]
# ---------------------------------------------------------------------------
# test stage: prepared to actually run the install scripts & probe stow.
# Uses the mounted repo (see -v $PWD:/dotfiles in the Makefile) so scripts
# see the live workspace checkout.
# ---------------------------------------------------------------------------
FROM base AS test
USER "$USERNAME"
WORKDIR /dotfiles
# The entrypoint drives the smoke tests. The repo is mounted at /dotfiles
# via the Makefile's `docker run -v`.
ENTRYPOINT ["bash", "/dotfiles/test/entrypoint-test.sh"]
+252
View File
@@ -0,0 +1,252 @@
#!/bin/bash
# test/entrypoint-test.sh
#
# Runs inside the docker `test` stage. The repo is mounted read-write at
# /dotfiles (see the Makefile's `docker run -v "$PWD":/dotfiles`).
#
# Two tiers:
# 1. DRY PROBES (always, fast, offline)
# - `make -n` dry-runs to catch Makefile typos / missing scripts
# - `stow --simulate` to prove every stow target resolves
# 2. REAL SMOKE (guarded by RUN_SMOKE=1)
# - exercises term/setup.sh package parsing and (optionally) installs
# - verifies stow actually creates symlinks into $HOME
# Full AUR/npm + kreation/buntes-monster only run with RUN_FULL=1.
#
# Env flags (set via `docker run -e`):
# RUN_SMOKE=1 run the real pacman-install smoke
# RUN_FULL=1 also attempt AUR/npm + hypr/kreation/buntes-monster
# SKIP_NET=1 pretend no network; skip anything that needs the mirror
set -uo pipefail
# Resolve the repo root: prefer the container's /dotfiles mount, otherwise
# fall back to the directory above this script (a local checkout).
if [[ -d /dotfiles ]]; then
cd /dotfiles || { echo "FATAL: repo not mounted at /dotfiles"; exit 1; }
else
cd "$(dirname "${BASH_SOURCE[0]}")/.." || { echo "FATAL: cannot locate repo root"; exit 1; }
fi
REPO="$(pwd)"
# Guard: both resolution paths must find a real checkout.
if [[ ! -f "$REPO/Makefile" || ! -d "$REPO/test" ]]; then
echo "FATAL: '$REPO' does not look like the dotfiles repo root." >&2
echo " In docker, run with -v \"\$PWD\":/dotfiles ." >&2
exit 1
fi
PASS=0
FAIL=0
say_ok() { printf ' ok: %s\n' "$*"; PASS=$((PASS+1)); }
say_fail(){ printf ' FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
header() { printf '\n==== %s ====\n' "$*"; }
if command -v git >/dev/null 2>&1 && [[ ! -d .git ]]; then
# when mounted from a worktree, .git may be a file; fine.
:
fi
echo "entrypoint-test.sh: running as $(id -un) in $(pwd)"
# ---------------------------------------------------------------------------
# 1. DRY PROBES - always run, cheap, offline.
# ---------------------------------------------------------------------------
header "Dry probes"
# 1a. Makefile dry runs for every target.
# Discover targets dynamically so we never drift from the Makefile.
targets=$(grep -oE '^[A-Za-z0-9_-]+:' Makefile | tr -d ':' | grep -v '^test' | sort -u)
for target in $targets; do
# `make -n` must not error out from missing scripts / dirs.
if ( cd "$REPO" && make -n "$target" >/dev/null 2>err.txt ); then
say_ok "make -n $target parses"
else
say_fail "make -n $target (typo? missing script?)"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
done
# 1b. stow --simulate for every package the Makefile stows.
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if stow --simulate --dir="$REPO" --target="$REPO" "$pkg" 2>err.txt; then
say_ok "stow --simulate $pkg resolves"
else
say_fail "stow --simulate $pkg (non-existent package or conflict)"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
done < <(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)
# 1c. run the repo's own lint + probe scripts (they're mounted here).
if [[ -x test/lint.sh ]] || [[ -f test/lint.sh ]]; then
if bash test/lint.sh >/dev/null 2>&1; then
say_ok "test/lint.sh"
else
say_fail "test/lint.sh (see logs above / rerun outside container)"
fi
else
say_fail "test/lint.sh not present in mount"
fi
header "Smoke gates"
echo " RUN_SMOKE=${RUN_SMOKE:-0} RUN_FULL=${RUN_FULL:-0} SKIP_NET=${SKIP_NET:-0}"
# ---------------------------------------------------------------------------
# 2. REAL SMOKE (RUN_SMOKE=1)
# ---------------------------------------------------------------------------
if [[ "${RUN_SMOKE:-0}" == "1" ]]; then
header "Real smoke (RUN_SMOKE=1)"
# 2a. Allow scripts to use sudo without a password (already NOPASSWD via
# the Dockerfile for the wheel group).
#
# 2b. Override host-daemon commands with no-ops so nothing tries to talk
# to systemd/dbus/dbus in the container.
mkdir -p "$HOME/bin"
cat > "$HOME/bin/systemctl" <<'NOOP'
#!/bin/bash
echo "[stub] systemctl $*"
exit 0
NOOP
cat > "$HOME/bin/dbus-update-activation-environment" <<'NOOP'
#!/bin/bash
echo "[stub] dbus-update-activation-environment $*"
exit 0
NOOP
cat > "$HOME/bin/kbuildsycoca6" <<'NOOP'
#!/bin/bash
echo "[stub] kbuildsycoca6 $*"
exit 0
NOOP
cat > "$HOME/bin/updatedb" <<'NOOP'
#!/bin/bash
echo "[stub] updatedb $*"
exit 0
NOOP
chmod +x "$HOME/bin/"*
export PATH="$HOME/bin:$PATH"
# 2c. term/setup.sh package parsing sanity (mirrors read_packages).
if ( cd "$REPO" && bash -n term/setup.sh ); then
say_ok "term/setup.sh syntax OK"
else
say_fail "term/setup.sh syntax error"
fi
pacman_count=$(awk '
/^@pacman/{s="pacman"}
/^@aur/{s="aur"}
/^@npm/{s="npm"}
/^[[:space:]]*#/{next}
/^[[:space:]]*$/{next}
s=="pacman"{pc++} s=="aur"{ac++} s=="npm"{nc++}
END{printf "%d %d %d", pc,ac,nc}
' $REPO/term/packages.txt)
read -r PC AC NC <<< "$pacman_count"
say_ok "packages.txt -> ${PC} pacman, ${AC} aur, ${NC} npm entries"
# 2d. If network allowed and not skipped, actually install the pacman
# section of term/setup.sh (mirrors `make term` without AUR/npm).
if [[ "${SKIP_NET:-0}" != "1" ]]; then
header "make term (pacman packages, AUR/npm skipped)"
# Patch packages.txt to drop @aur/@npm so we don't hit AUR/npm.
cp "$REPO/term/packages.txt" "$HOME/packages.txt.full"
awk '
/^@aur$/{s="skip"}; /^@npm$/{s="skip"}; /^@pacman$/{s="pacman"}
{ if (s!="skip" && NF>0 && $1 !~ /^#/) print }
' "$REPO/term/packages.txt" > "$HOME/packages.txt.term-only"
# set a scratch HOME for setup.sh so it doesn't touch the real one.
mkdir -p "$HOME/term"
cp "$REPO/term/setup.sh" "$HOME/term/setup.sh"
cp "$HOME/packages.txt.term-only" "$HOME/term/packages.txt"
# chsh is stubbed via $HOME/bin so even `chsh` works without a pty.
setup_ok=0
if ( cd "$HOME/term" && bash setup.sh ) ; then
say_ok "term/setup.sh (term-only) completed"
setup_ok=1
else
say_fail "term/setup.sh (term-only) failed (exit $?)"
fi
# 2d2. Verify that every package is really installed and its binary
# runs. Only meaningful if the install itself succeeded.
if [[ "$setup_ok" == "1" ]]; then
if [[ -x test/verify_packages.sh ]]; then
header "verify installed packages"
if bash test/verify_packages.sh "$HOME/packages.txt.term-only"; then
say_ok "verify_packages.sh: all packages verified"
else
say_fail "verify_packages.sh: one or more packages failed verification (see above)"
fi
else
say_fail "test/verify_packages.sh missing"
fi
else
echo " skipping package verification (install did not complete)"
fi
else
echo " SKIP_NET=1 -> skipping real package install and verification"
fi
# 2e. stow --actual into $HOME to prove real symlinks are created.
header "stow real symlinks into \$HOME"
for pkg in term hypr kreation local_ai; do
if [[ -d "$REPO/$pkg" ]]; then
if stow --dir="$REPO" --target="$HOME" "$pkg" 2>err.txt; then
say_ok "stow $pkg into \$HOME"
else
say_fail "stow $pkg into \$HOME"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
else
say_fail "package dir missing: $pkg"
fi
done
# sanity: representative symlinks must now exist in $HOME.
missing=""
for link in .config/fish .config/hypr .vst3/MT-PowerDrumKit.vst3; do
[[ -e "$HOME/$link" ]] || missing="$missing $link"
done
if [[ -z "$missing" ]]; then
say_ok "stow created representative \$HOME symlinks"
else
say_fail "missing expected stow symlinks:$missing"
fi
# 2f. kreation package: verify the VST path is stow-able and present.
header "kreation VST"
if [[ -f "$REPO/kreation/.vst3/MT-PowerDrumKit.vst3/Contents/x86_64-linux/MT-PowerDrumKit.so" ]]; then
say_ok "MT-PowerDrumKit.vst3 binary present"
else
say_fail "MT-PowerDrumKit.vst3 binary missing"
fi
else
echo " RUN_SMOKE not set -> skipping real installs & symlink creation."
fi
# ---------------------------------------------------------------------------
# 3. FULL (RUN_FULL=1) - opt-in heavy path (AUR/npm/hypr/kreation)
# ---------------------------------------------------------------------------
if [[ "${RUN_FULL:-0}" == "1" ]] && [[ "${SKIP_NET:-0}" != "1" ]]; then
header "Full smoke (RUN_FULL=1)"
if ( cd "$REPO" && make buntes-monster ); then
say_ok "make buntes-monster"
else
say_fail "make buntes-monster (exit $?)"
fi
else
echo " RUN_FULL not set (or SKIP_NET) -> skipping full/hypr/kreation install."
fi
# ---------------------------------------------------------------------------
# Summary & exit code
# ---------------------------------------------------------------------------
echo
echo "=============================================="
echo "entrypoint-test.sh: $PASS passed, $FAIL failed"
echo "=============================================="
# Exit non-zero if anything failed.
[[ $FAIL -eq 0 ]]
Executable
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
# test/lint.sh
#
# Fast, offline integrity checks for the dotfiles repo.
# Does NOT require Docker or any packages to be installed beyond:
# - shellcheck (silently skipped if unavailable)
# - bash (for `bash -n` syntax checks)
#
# Run with: make test-lint
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CD="$(pwd)"
cd "$REPO_ROOT"
PASS=0
FAIL=0
say_fail() { printf 'FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
say_pass() { printf 'ok: %s\n' "$*"; PASS=$((PASS+1)); }
# ---------------------------------------------------------------------------
# 1. Collect scripts to check.
# Only repo-owned scripts; skip vendored tpm/ plugin shell scripts.
# Preferred source is git ls-files (the repo checkout); fall back to a
# filesystem scan when .git is not present (e.g. inside the docker dev
# image, where the repo is COPYied without .git).
# ---------------------------------------------------------------------------
mapfile -t SCRIPTS < <(git ls-files '*.sh' 2>/dev/null | grep -v '/tpm/' | grep -vE '/plugins/[^/]+/scripts/')
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
mapfile -t SCRIPTS < <(find . -name '*.sh' -not -path './.git/*' -not -path '*/tpm/*' -not -path '*/.git/*' | sort)
fi
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
say_fail "no *.sh files found"
exit 1
fi
echo "== Checking $((${#SCRIPTS[@]})) tracked shell scripts =="
# ---------------------------------------------------------------------------
# 2. Syntax check (bash -n) - always runs, no external deps.
# ---------------------------------------------------------------------------
for s in "${SCRIPTS[@]}"; do
if bash -n "$s" 2>/dev/null; then
:
else
say_fail "bash -n $s"
fi
done
[[ $FAIL -eq 0 ]] && say_pass "bash -n syntax check passed"
# ---------------------------------------------------------------------------
# 3. shellcheck (optional dependency).
# ---------------------------------------------------------------------------
if command -v shellcheck >/dev/null 2>&1; then
echo "== Running shellcheck =="
if shellcheck --version >/dev/null 2>&1; then
if shellcheck -S warning "${SCRIPTS[@]}" ; then
say_pass "shellcheck (warning severity)"
else
say_fail "shellcheck reported issues (warning severity)"
fi
else
say_fail "shellcheck binary present but not functional"
fi
else
echo "== shellcheck not installed; skipping (install with: pacman -S shellcheck) =="
fi
# ---------------------------------------------------------------------------
# 4. packages.txt parsing sanity (mirrors term/setup.sh read_packages).
# ---------------------------------------------------------------------------
echo "== Checking term/packages.txt markers =="
if grep -q '^@pacman' term/packages.txt; then
say_pass "packages.txt declares @pacman section"
else
say_fail "packages.txt missing @pacman marker"
fi
# every @aur/@npm marker must be matched by a real consumer in setup.sh
for marker in '@aur' '@npm'; do
if grep -q "^$marker" term/packages.txt; then
grep -q "$marker" term/setup.sh \
&& say_pass "packages.txt $marker handled by term/setup.sh" \
|| say_fail "packages.txt uses $marker but term/setup.sh does not handle it"
else
say_pass "packages.txt has no $marker section"
fi
done
# ---------------------------------------------------------------------------
# 5. Makefile <-> install script name consistency.
# Catches typos such as `_install_skripte_install_basic_pcks.sh`
# (missing '/') in Makefile targets.
# ---------------------------------------------------------------------------
echo "== Checking Makefile references install scripts that exist =="
# Pull out every `*_install_*_pcks.sh` token mentioned in the Makefile.
while IFS= read -r tok; do
[[ -z "$tok" ]] && continue
if [[ -e "$REPO_ROOT/$tok" ]]; then
say_pass "Makefile references existing script: $tok"
else
say_fail "Makefile references missing script: $tok"
fi
done < <(grep -P '^\t' Makefile | grep -oE '[_A-Za-z0-9/-]*install_[A-Za-z0-9_-]*\.sh' | sort -u)
# ---------------------------------------------------------------------------
# 6. Makefile <-> stow package name consistency.
# Catches bugs such as `stow nvim` when the actual package dir is `term/`.
# ---------------------------------------------------------------------------
echo "== Checking Makefile stow targets reference real package dirs =="
# A 'package' is any tracked top-level directory (a .config/ subtree is stowable).
have_pkg() {
local name="$1"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git ls-files 2>/dev/null | awk -F/ 'NF>1 {print $1}' | sort -u | grep -qx "$name"
else
# git-less checkout (e.g. docker dev image): fall back to filesystem.
# Only verify the dir exists; "is it stowable" is validated by stow
# itself during the smoke test.
[[ -d "$name" ]]
fi
}
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
case "$pkg" in
\#*|"") continue ;;
esac
if have_pkg "$pkg" && [[ -d "$REPO_ROOT/$pkg" ]]; then
say_pass "Makefile stows existing package: $pkg"
else
say_fail "Makefile stows non-existent package: $pkg"
fi
done < <(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo
echo "=============================================="
echo "lint.sh: $PASS passed, $FAIL failed"
echo "=============================================="
[[ $FAIL -eq 0 ]]
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# test/probe_stow.sh
#
# Verifies that each package the Makefile stows actually resolves and that
# stow can create all symlinks into the target without conflicts.
#
# This is a DRY RUN: it uses `stow --simulate` and creates no real symlinks.
# It catches bugs like `stow nvim` for a non-existent `nvim/` package.
#
# Usage:
# bash test/probe_stow.sh # probe every stow-able package
# bash test/probe_stow.sh <pkg>... # probe specific packages
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# A stow-able package is any tracked top-level directory that contains a
# dot-prefixed config subtree (or a dot-prefixed top-level file/dir).
tracked_basenames() {
git ls-files | awk -F/ 'NF>1 {print $1}' | sort -u
}
is_stowable() {
local pkg="$1"
[[ -d "$REPO_ROOT/$pkg" ]] || return 1
# stow needs at least one dot-prefixed entry at the package root
git ls-files "$pkg/" | grep -qE "^$pkg/\." || return 1
}
# Determine which packages to probe.
PACKAGES=()
if [[ $# -eq 0 ]]; then
while IFS= read -r b; do
[[ -z "$b" ]] && continue
is_stowable "$b" && PACKAGES+=("$b")
done < <(tracked_basenames)
else
PACKAGES=("$@")
fi
if [[ ${#PACKAGES[@]} -eq 0 ]]; then
echo "probe_stow.sh: no stow-able packages found" >&2
exit 1
fi
echo "== Probing stow packages: ${PACKAGES[*]} =="
FAIL=0
for pkg in "${PACKAGES[@]}"; do
echo "--- stow --simulate: $pkg ---"
if stow --simulate --verbose=1 --dir="$REPO_ROOT" --target="$REPO_ROOT" "$pkg" >/dev/null 2>probe_err.txt; then
echo "ok: stow --simulate '$pkg' resolved without conflicts"
else
echo "FAIL: stow --simulate '$pkg'"
sed 's/^/ /' probe_err.txt
FAIL=$((FAIL+1))
fi
rm -f probe_err.txt
done
# Cross-check: does the Makefile stow anything that is NOT in our package set?
echo "== Checking Makefile stow targets against stow-able packages =="
make_pkgs="$(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)"
for mp in $make_pkgs; do
if is_stowable "$mp"; then
echo "ok: Makefile 'stow $mp' matches a stow-able package"
else
echo "FAIL: Makefile 'stow $mp' does not match a stow-able package"
FAIL=$((FAIL+1))
fi
done
echo
echo "=============================================="
echo "probe_stow.sh: $(( ${#PACKAGES[@]} + $(printf '%s\n' $make_pkgs | grep -c . || true) )) checks, $FAIL failed"
echo "=============================================="
[[ $FAIL -eq 0 ]]
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
# test/verify_packages.sh
#
# Post-install verification of the packages the smoke test installs.
# Given a package-list file (one package per line, `@section` markers
# allowed - only the section content is verified), it checks that every
# listed package is:
# 1. installed in the pacman DB -> `pacman -Q <pkg>`
# 2. its expected binary exists and reports a version (curated mapping)
#
# Layer 2 runs a real `--version`-style probe for packages that install a
# binary, so a broken/renamed executable is caught even if pacman reports
# the package as installed. Fonts/libs/modules without a binary are verified
# by Layer 1 only.
#
# Usage: bash test/verify_packages.sh <package-list-file>
set -uo pipefail
PKGS_FILE="${1:?usage: verify_packages.sh <package-list-file>}"
[[ -f "$PKGS_FILE" ]] || { echo "verify_packages.sh: not a file: $PKGS_FILE" >&2; exit 1; }
PASS=0
FAIL=0
say_ok() { printf ' ok: %s\n' "$*"; PASS=$((PASS+1)); }
say_fail() { printf ' FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
# ---------------------------------------------------------------------------
# Curated mapping: package -> "binary version_flag"
# - packages not listed here are checked with pacman -Q only
# - flag missing (empty) => existence check only
# ---------------------------------------------------------------------------
declare -A BINS=(
[fish]="fish --version"
[kitty]="kitty --version"
[neovim]="nvim --version"
[openssh]="ssh -V"
[rclone]="rclone --version"
[rsync]="rsync --version"
[unison]="unison -version"
[stow]="stow --version"
[nodejs]="node --version"
[npm]="npm --version"
[starship]="starship --version"
[fastfetch]="fastfetch --version"
[tree-sitter-cli]="tree-sitter --version"
[python]="python --version"
[python-pip]="pip3 --version"
[unzip]="unzip --version"
[selene]="selene --version"
[tmux]="tmux -V"
[yazi]="yazi --version"
[zoxide]="zoxide --version"
[fzf]="fzf --version"
[chromium]="chromium --version"
[locate]="locate --version"
)
# python-pip may expose `pip` instead of `pip3` on some setups; treat them
# as interchangeable so the probe does not spuriously fail.
probe_pip() {
for b in pip3 pip; do
if command -v "$b" >/dev/null 2>&1; then
out=$("$b" --version 2>&1) && [[ -n "$out" ]] && { say_ok "$b --version ($out)"; return 0; }
fi
done
say_fail "python-pip: neither pip3 nor pip found/probe failed"
return 1
}
echo "== Verifying installed packages from: $PKGS_FILE =="
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}" # strip trailing comments
line="${line// /}" # drop inline whitespace
[[ -z "$line" ]] && continue
[[ "$line" == @* ]] && continue # skip @pacman/@aur/@npm section markers
pkg="${line#*/}" # strip repo prefix (extra/kitty -> kitty)
if [[ -z "$pkg" ]]; then
say_fail "empty package name in '$line'"
continue
fi
# Layer 1: installed in the pacman DB?
if pacman -Q "$pkg" >/dev/null 2>&1; then
say_ok "pacman -Q $pkg (installed)"
else
say_fail "pacman -Q $pkg (not installed)"
continue
fi
# Layer 2: runtime binary probe (curated mapping)?
spec="${BINS[$pkg]:-}"
[[ -z "$spec" ]] && continue # no binary (font/lib/module) - Layer 1 only
# python-pip has special multi-candidate handling
[[ "$pkg" == "python-pip" ]] && { probe_pip; continue; }
read -r bin flag <<< "$spec"
if command -v "$bin" >/dev/null 2>&1; then
if [[ -z "$flag" ]]; then
say_ok "command -v $bin (present)"
else
out=$("$bin" $flag 2>&1) && [[ -n "$out" ]]
if [[ $? -eq 0 ]]; then
say_ok "$bin $flag ($out)"
else
say_fail "$bin $flag (binary present but probe failed)"
fi
fi
else
say_fail "command -v $bin (expected from package '$pkg')"
fi
done < "$PKGS_FILE"
echo
echo "----------------------------------------------"
echo "verify_packages.sh: $PASS passed, $FAIL failed"
echo "----------------------------------------------"
[[ $FAIL -eq 0 ]]