diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 6b00def..6f3cf54 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -13,10 +13,33 @@ on: - "functions/**" - "conf.d/**" - "config.fish" + - "completions/**" + - "integrations/**" + - "tests/**" workflow_dispatch: jobs: + test: + runs-on: racknerd-mini + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITEA_TOKEN }} + + - name: Install fish + run: | + sudo apt-get update -qq + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:fish-shell/release-4 + sudo apt-get update -qq + sudo apt-get install -y fish + + - name: Run fish config tests + run: fish tests/run-tests.fish + build-docs: + needs: test runs-on: racknerd-mini steps: - name: Checkout diff --git a/README.md b/README.md index 11e074c..ea478f5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ abbreviation system for keyboard-driven workflows. - [Installation](#installation) - [Personalization](#personalization) - [Minimal Mode](#minimal-mode) +- [Testing](#testing) - [Attribution](#attribution) - [License](#license) @@ -330,6 +331,16 @@ for the full sub-category list per category. --- +## Testing + +```fish +fish tests/run-tests.fish +``` + +Runs before every push (and gates the [documentation build](.github/workflows/build-docs.yml) in CI, so a broken config can't get published): syntax-lints every `.fish` file, then loads the config in an isolated `HOME`/XDG sandbox — never this checkout itself, since it doubles as a real `~/.config/fish` — and runs functional checks against foundational behavior (XDG/PATH/CDPATH setup, key bindings, abbreviations, core functions, the opinionated-component registry, and more). + +--- + ## Attribution The core of the [Zoxide integration](https://fish.rootiest.fyi/02-path-setup/) in this repository was originally adapted from the [icezyclon/zoxide.fish](https://github.com/icezyclon/zoxide.fish) plugin (MIT Licensed) and has since been heavily customized for performance and Fish 4.x compatibility. diff --git a/tests/functional.fish b/tests/functional.fish new file mode 100644 index 0000000..a6f326f --- /dev/null +++ b/tests/functional.fish @@ -0,0 +1,75 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Functional checks for foundational config behavior. Sourced inside a +# fully-loaded, sandboxed interactive fish session by tests/run-tests.fish +# -- see that file for the sandbox setup. Each test_* function returns 0 +# on pass, non-zero on fail; functional_test_main collects and runs them. + +function test_xdg_defaults + test -n "$XDG_CONFIG_HOME" -a -n "$XDG_CACHE_HOME" \ + -a -n "$XDG_DATA_HOME" -a -n "$XDG_STATE_HOME" +end + +function test_path_additions + contains -- "$HOME/.local/bin" $PATH +end + +function test_cdpath + contains -- "$HOME/projects" $CDPATH +end + +function test_vi_key_bindings + test "$fish_key_bindings" = fish_vi_key_bindings +end + +function test_abbreviations_loaded + abbr -q n +end + +function test_core_functions_defined + for f in cat logs config-help fish-deps check_fish_deps config-settings + if not functions -q $f + echo " missing function: $f" + return 1 + end + end +end + +function test_exit_rewired + functions -q exit + and functions exit | string match -q '*smart_exit*' +end + +function test_op_registry_lookup + functions -q __fish_config_op_registry_lookup + or return 1 + set -l tags (__fish_config_op_registry_lookup config cdpath) + test $status -eq 0 -a (count $tags) -gt 0 +end + +function test_op_enabled_fail_open + # An identity/site pair with no registry entry must resolve to + # enabled -- the documented fail-open default. + __fish_config_op_enabled __fish_config_test_never_registered somesite +end + +function test_greeting_function_defined + functions -q fish_greeting +end + +function functional_test_main + set -l names (functions -a | string match 'test_*' | sort) + set -l failed 0 + for name in $names + if $name + echo " PASS $name" + else + echo " FAIL $name" + set failed (math $failed + 1) + end + end + echo "" + echo (math (count $names) - $failed)"/"(count $names)" passed" + return $failed +end diff --git a/tests/run-tests.fish b/tests/run-tests.fish new file mode 100755 index 0000000..a0af7cc --- /dev/null +++ b/tests/run-tests.fish @@ -0,0 +1,89 @@ +#!/usr/bin/env fish +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# CI test runner for this fish configuration. +# 1. Syntax-lints every tracked .fish file (fish -n). +# 2. Copies the config-relevant files into a throwaway sandbox (never +# the live checkout -- this repo doubles as a real ~/.config/fish, +# so a symlinked sandbox would let universal-variable writes like +# first-run's escape into the real, gitignored fish_variables file) +# and loads it as an isolated interactive session. +# 3. Runs the functional checks in tests/functional.fish inside that +# loaded session. +# +# Usage: fish tests/run-tests.fish + +set -l script_dir (realpath (dirname (status filename))) +set -l repo_root (realpath $script_dir/..) +set -l overall_failed 0 + +# ---- Phase 1: syntax lint ------------------------------------------------ +echo "== Syntax lint ==" +set -l lint_files $repo_root/config.fish +for dir in functions conf.d completions integrations + set -a lint_files (find $repo_root/$dir -name '*.fish' | sort) +end + +set -l lint_failed 0 +for f in $lint_files + set -l out (fish -n $f 2>&1) + if test $status -ne 0 + echo " FAIL "(string replace $repo_root/ '' $f) + printf '%s\n' $out + set lint_failed (math $lint_failed + 1) + end +end +set -l lint_total (count $lint_files) +echo (math $lint_total - $lint_failed)"/$lint_total files passed lint" +if test $lint_failed -ne 0 + set overall_failed 1 +end + +# ---- Phase 2: isolated load + functional checks -------------------------- +echo "" +echo "== Sandboxed load + functional checks ==" + +set -l sandbox (mktemp -d) +set -l sandbox_cfg $sandbox/xdgcfg/fish +mkdir -p $sandbox_cfg +# path-setup only adds directories that already exist (fish_add_path is a +# no-op on missing paths), so give it $HOME/.local/bin to find. +mkdir -p $sandbox/home/.local/bin + +cp $repo_root/config.fish $sandbox_cfg/ +test -f $repo_root/fish_plugins +and cp $repo_root/fish_plugins $sandbox_cfg/ +for d in functions conf.d completions integrations themes data + test -d $repo_root/$d + and cp -r $repo_root/$d $sandbox_cfg/ +end + +set -l err_file (mktemp) +env -i \ + HOME=$sandbox/home \ + XDG_CONFIG_HOME=$sandbox/xdgcfg \ + PATH="$PATH" \ + TERM=xterm \ + __fish_config_op_autoexec=off \ + fish -i -c "source $repo_root/tests/functional.fish; functional_test_main" \ + 2>$err_file +set -l functional_status $status + +set -l stderr_out (cat $err_file) +rm -rf $sandbox $err_file + +if test -n "$stderr_out" + # Diagnostic only, not a gate: on machines with vendor fish configs + # (e.g. CachyOS's cachyos-fish-config, which this repo's config.fish + # sources when present) unrelated vendor warnings can land here. Real + # breakage in this repo's own code is caught by the assertions below. + echo " Session stderr output (informational):" + printf '%s\n' $stderr_out +end + +if test $functional_status -ne 0 + set overall_failed 1 +end + +exit $overall_failed