diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8a47c0e --- /dev/null +++ b/.dockerignore @@ -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 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 8837464..fed0014 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`
`bash .install_skripte/install_basic_pcks.sh`
`bash .install_skripte/install_hypr_pcks.sh`
`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`
`bash _install_skripte/install_basic_pcks.sh`
`bash _install_skripte/install_hypr_pcks.sh`
`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`
`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 DAW’s plugins folder (usually `~/.vst`). | The repo contains a pre‑built VST for use in audio software. | diff --git a/Makefile b/Makefile index 4ef81fd..8fa7674 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 67d461d..cb2fc9b 100644 --- a/README.md +++ b/README.md @@ -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 ` (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 ` 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. diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..6fd8172 --- /dev/null +++ b/test/Dockerfile @@ -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"] diff --git a/test/entrypoint-test.sh b/test/entrypoint-test.sh new file mode 100755 index 0000000..86d89ac --- /dev/null +++ b/test/entrypoint-test.sh @@ -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 ]] diff --git a/test/lint.sh b/test/lint.sh new file mode 100755 index 0000000..f713d00 --- /dev/null +++ b/test/lint.sh @@ -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 ]] diff --git a/test/probe_stow.sh b/test/probe_stow.sh new file mode 100755 index 0000000..3a8a00f --- /dev/null +++ b/test/probe_stow.sh @@ -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 ... # 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 ]] diff --git a/test/verify_packages.sh b/test/verify_packages.sh new file mode 100755 index 0000000..307c3e7 --- /dev/null +++ b/test/verify_packages.sh @@ -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 ` +# 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 + +set -uo pipefail + +PKGS_FILE="${1:?usage: verify_packages.sh }" +[[ -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 ]] \ No newline at end of file