From 67ffdfe99f8d057170f993928678a3fd178044f1 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Tue, 5 May 2026 00:31:12 +0800 Subject: [PATCH] Add reusable project test runner --- .github/workflows/test.yml | 152 +------------------------ CHANGELOG.md | 3 + CONTRIBUTING.md | 15 ++- scripts/test.sh | 221 +++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 149 deletions(-) create mode 100755 scripts/test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ed562a..4d6c3bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,157 +51,15 @@ jobs: } done - - name: Test startup - run: | - vim -u .vimrc -i NONE -es -N -c 'qa!' 2>&1 - echo "Vim exited cleanly" - - - name: Verify modules load - run: | - vim -u .vimrc -i NONE -es -N \ - -c 'redir! > /tmp/test.txt' \ - -c 'silent echo len(g:plugs)' \ - -c 'redir END' \ - -c 'qa!' 2>/dev/null - PLUGS=$(cat /tmp/test.txt | tr -d '[:space:]') - echo "Plugins registered: $PLUGS" - if [ "$PLUGS" -lt 20 ]; then - echo "FAIL: expected 20+ plugins, got $PLUGS" - exit 1 - fi - - - name: Verify path-safe module loading - run: | - mkdir -p "/tmp/chopsticks path/modules" - cp .vimrc "/tmp/chopsticks path/.vimrc" - cp modules/*.vim "/tmp/chopsticks path/modules/" - vim -u "/tmp/chopsticks path/.vimrc" -i NONE -es -N -c 'qa!' 2>&1 - - - name: Verify minimal profile - run: | - vim -u NONE -i NONE -es -N \ - -c 'let g:chopsticks_profile = "minimal"' \ - -c 'source .vimrc' \ - -c 'if has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "vim-lsp-settings") || has_key(g:plugs, "asyncomplete.vim") | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify local config hook - run: | - mkdir -p /tmp/chopsticks-ci - printf "%s\n" "let g:chopsticks_profile = 'minimal'" > /tmp/chopsticks-ci/local.vim - vim -u NONE -i NONE -es -N \ - -c 'let g:chopsticks_local_config = "/tmp/chopsticks-ci/local.vim"' \ - -c 'source .vimrc' \ - -c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify XDG local config hook - run: | - mkdir -p /tmp/chopsticks-xdg - printf "%s\n" "let g:chopsticks_profile = 'minimal'" > /tmp/chopsticks-xdg/chopsticks.vim - XDG_CONFIG_HOME=/tmp/chopsticks-xdg vim -u NONE -i NONE -es -N \ - -c 'source .vimrc' \ - -c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify profile-aware cheat sheet - run: | - vim -u NONE -i NONE -es -N \ - -c 'let g:chopsticks_profile = "minimal"' \ - -c 'source .vimrc' \ - -c 'normal ,?' \ - -c 'redir! > /tmp/chopsticks-cheat.txt' \ - -c 'silent %print' \ - -c 'redir END' \ - -c 'qa!' 2>&1 - if grep -Eq 'definition|LspInstallServer|ALE errors|undo tree|markdown preview' /tmp/chopsticks-cheat.txt; then - cat /tmp/chopsticks-cheat.txt - exit 1 - fi - grep -q ',cr run file' /tmp/chopsticks-cheat.txt - - - name: Verify Markdown quiet defaults - run: | - vim -u .vimrc -i NONE -es -N README.md \ - -c 'set filetype=markdown' \ - -c 'if &l:spell || &l:conceallevel != 0 || &l:signcolumn !=# "no" || exists("g:lsp_settings_filetype_markdown") | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify ergonomic defaults - run: | - vim -u .vimrc -i NONE -es -N \ - -c 'if maparg("s", "n") !=# "" | cquit | endif' \ - -c 'if maparg(",w", "n") =~# "!" | cquit | endif' \ - -c 'if !&swapfile || !&writebackup || &directory !~# "\.vim/.swap" | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify local ALE override - run: | - vim -u NONE -i NONE -es -N \ - -c 'let g:ale_fix_on_save = 0' \ - -c 'source .vimrc' \ - -c 'if g:ale_fix_on_save != 0 | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Verify large file protection - run: | - truncate -s 11000000 /tmp/chopsticks-large.py - vim -u .vimrc -i NONE -es -N /tmp/chopsticks-large.py \ - -c 'set filetype=python' \ - -c 'if &l:syntax !=# "" || &l:undolevels != -1 || &l:swapfile || get(b:, "ale_enabled", 1) != 0 | cquit | endif' \ - -c 'qa!' 2>&1 - - - name: Measure startup time - run: | - vim -u .vimrc -i NONE --startuptime /tmp/startup.log -es -N -c 'qa!' 2>/dev/null - tail -1 /tmp/startup.log - STARTUP_MS=$(tail -1 /tmp/startup.log | awk '{print $1}') - awk -v ms="$STARTUP_MS" 'BEGIN { if (ms > 150) exit 1 }' + - name: Run Vim smoke tests + run: scripts/test.sh vim shellcheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Shellcheck install.sh - run: shellcheck install.sh get.sh - - name: Verify installer profile-only modes - run: | - XDG_CONFIG_HOME=/tmp/chopsticks-dry ./install.sh --dry-run --profile=full \ - | tee /tmp/chopsticks-dry-run.txt - grep -q 'Profile: full' /tmp/chopsticks-dry-run.txt - test ! -e /tmp/chopsticks-dry/chopsticks.vim - - XDG_CONFIG_HOME=/tmp/chopsticks-config ./install.sh --configure-only --profile=minimal - grep -q "let g:chopsticks_profile = 'minimal'" /tmp/chopsticks-config/chopsticks.vim - - XDG_CONFIG_HOME=/tmp/chopsticks-config ./install.sh --configure-only --profile=full - grep -q "let g:chopsticks_profile = 'full'" /tmp/chopsticks-config/chopsticks.vim - - XDG_CONFIG_HOME=/tmp/chopsticks-default ./install.sh --configure-only --yes - grep -q "let g:chopsticks_profile = 'engineer'" /tmp/chopsticks-default/chopsticks.vim - - - name: Verify bootstrap dry-run safety - run: | - CHOPSTICKS_DEST=/tmp/chopsticks-bootstrap ./get.sh --dry-run --profile=minimal \ - | tee /tmp/chopsticks-get-dry-run.txt - grep -q 'Would clone' /tmp/chopsticks-get-dry-run.txt - test ! -e /tmp/chopsticks-bootstrap - - mkdir -p /tmp/not-chopsticks - git init /tmp/not-chopsticks - git -C /tmp/not-chopsticks remote add origin https://github.com/example/not-chopsticks.git - if CHOPSTICKS_DEST=/tmp/not-chopsticks ./get.sh --dry-run; then - echo "Expected get.sh to reject non-chopsticks repo" - exit 1 - fi - - mkdir -p /tmp/chopsticks-existing - git init /tmp/chopsticks-existing - git -C /tmp/chopsticks-existing remote add origin https://github.com/m1ngsama/chopsticks.git - touch /tmp/chopsticks-existing/install.sh /tmp/chopsticks-existing/.vimrc - CHOPSTICKS_DEST=/tmp/chopsticks-existing ./get.sh --dry-run --yes \ - | tee /tmp/chopsticks-get-existing.txt - grep -q 'Would update existing chopsticks repo' /tmp/chopsticks-get-existing.txt + - name: Run shell and installer tests + run: scripts/test.sh shell installer bootstrap docs: runs-on: ubuntu-latest @@ -210,4 +68,4 @@ jobs: - name: Install markdownlint run: npm install -g markdownlint-cli - name: Lint Markdown - run: markdownlint README.md QUICKSTART.md CONTRIBUTING.md CHANGELOG.md + run: scripts/test.sh docs diff --git a/CHANGELOG.md b/CHANGELOG.md index b2996df..2a1f24d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - `install.sh --configure-only` to update local profile config without reinstalling - `get.sh --dry-run` for safe bootstrap previews before clone/update/install - `CHOPSTICKS_DEST=/absolute/path` to test or install the bootstrap target elsewhere +- `scripts/test.sh` local test runner reused by GitHub Actions ### Fixed @@ -56,6 +57,8 @@ update it without appending duplicate bindings - Installer cleanup now restores the cursor after interrupted checkbox menus - Bootstrap dry-run now refuses unrelated existing git repos before any writes +- CI now shares shell, installer, bootstrap, docs, and Vim smoke checks with + the local test runner - Skip 2 more built-in plugins: openPlugin, manpager (10 → 12 total) - Remove deprecated `set ttyfast` (no-op since Vim 8) - Add `grepprg=rg --vimgrep` — `:grep` now uses ripgrep + quickfix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b52edc6..4746afc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,18 @@ 2. If it's not needed at startup, lazy-load it: `Plug 'foo/bar', { 'on': 'FooCommand' }` 3. Put config in the appropriate module 4. Update the cheat sheet in `modules/tools.vim` if you add keybindings -5. Test on both macOS and Linux +5. Run `scripts/test.sh vim` locally after installing plugins +6. Test on both macOS and Linux when changing terminal or package-manager behavior + +## Local tests + +```bash +scripts/test.sh shell docs installer bootstrap +scripts/test.sh vim +``` + +`scripts/test.sh vim` expects plugins to be installed under `~/.vim/plugged`. +Use `STARTUP_LIMIT_MS=150 scripts/test.sh vim` to match CI's startup threshold. ## Reporting bugs @@ -28,4 +39,4 @@ Open an issue. Include: - Named augroups with `autocmd!` - No comments explaining _what_ — only _why_ - `exists('g:plugs["..."]')` guards for plugin-dependent config -- Test with `vim -u .vimrc -i NONE --startuptime /tmp/s.log -es -N -c qa!` +- Test with `scripts/test.sh vim` diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..17f351e --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# Project test runner. CI calls the same groups that maintainers can run locally. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/chopsticks-test-XXXXXX")" +EMPTY_XDG="$TMP_ROOT/xdg-empty" +STARTUP_LIMIT_MS="${STARTUP_LIMIT_MS:-150}" + +cleanup() { + rm -rf "$TMP_ROOT" +} +trap cleanup EXIT + +cd "$ROOT" +mkdir -p "$EMPTY_XDG" + +step() { + printf '\n==> %s\n' "$1" +} + +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "Missing required command: $1" >&2 + exit 1 + } +} + +check_shell() { + step "Shell syntax and lint" + need bash + bash -n install.sh + bash -n get.sh + bash -n scripts/test.sh + test -x install.sh + test -x get.sh + test -x scripts/test.sh + + need shellcheck + shellcheck install.sh get.sh scripts/test.sh +} + +check_docs() { + step "Markdown lint" + need markdownlint + markdownlint README.md QUICKSTART.md CONTRIBUTING.md CHANGELOG.md +} + +check_installer_modes() { + step "Installer profile-only modes" + XDG_CONFIG_HOME="$TMP_ROOT/dry" ./install.sh --dry-run --profile=full \ + | tee "$TMP_ROOT/install-dry-run.txt" + grep -q 'Profile: full' "$TMP_ROOT/install-dry-run.txt" + test ! -e "$TMP_ROOT/dry/chopsticks.vim" + + XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=minimal + grep -q "let g:chopsticks_profile = 'minimal'" "$TMP_ROOT/config/chopsticks.vim" + + XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=full + grep -q "let g:chopsticks_profile = 'full'" "$TMP_ROOT/config/chopsticks.vim" + + XDG_CONFIG_HOME="$TMP_ROOT/default" ./install.sh --configure-only --yes + grep -q "let g:chopsticks_profile = 'engineer'" "$TMP_ROOT/default/chopsticks.vim" +} + +check_bootstrap() { + step "Bootstrap dry-run safety" + CHOPSTICKS_DEST="$TMP_ROOT/bootstrap" ./get.sh --dry-run --profile=minimal \ + | tee "$TMP_ROOT/get-dry-run.txt" + grep -q 'Would clone' "$TMP_ROOT/get-dry-run.txt" + test ! -e "$TMP_ROOT/bootstrap" + + mkdir -p "$TMP_ROOT/not-chopsticks" + git -c init.defaultBranch=main init "$TMP_ROOT/not-chopsticks" >/dev/null + git -C "$TMP_ROOT/not-chopsticks" remote add origin https://github.com/example/not-chopsticks.git + if CHOPSTICKS_DEST="$TMP_ROOT/not-chopsticks" ./get.sh --dry-run; then + echo "Expected get.sh to reject non-chopsticks repo" >&2 + exit 1 + fi + + mkdir -p "$TMP_ROOT/chopsticks-existing" + git -c init.defaultBranch=main init "$TMP_ROOT/chopsticks-existing" >/dev/null + git -C "$TMP_ROOT/chopsticks-existing" remote add origin https://github.com/m1ngsama/chopsticks.git + touch "$TMP_ROOT/chopsticks-existing/install.sh" "$TMP_ROOT/chopsticks-existing/.vimrc" + CHOPSTICKS_DEST="$TMP_ROOT/chopsticks-existing" ./get.sh --dry-run --yes \ + | tee "$TMP_ROOT/get-existing.txt" + grep -q 'Would update existing chopsticks repo' "$TMP_ROOT/get-existing.txt" +} + +check_plugin_dirs() { + step "Plugin directories" + for plugin in \ + fzf fzf.vim vim-fugitive vim-gitgutter ale vim-lsp vim-lsp-settings \ + asyncomplete.vim asyncomplete-lsp.vim vim-markdown + do + test -d "$HOME/.vim/plugged/$plugin" || { + echo "Missing plugin directory: $plugin" >&2 + exit 1 + } + done +} + +check_vim() { + step "Vim smoke tests" + need vim + check_plugin_dirs + + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N -c 'qa!' 2>&1 + + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \ + -c "redir! > $TMP_ROOT/plugs.txt" \ + -c 'silent echo len(g:plugs)' \ + -c 'redir END' \ + -c 'qa!' 2>/dev/null + PLUGS="$(tr -d '[:space:]' < "$TMP_ROOT/plugs.txt")" + echo "Plugins registered: $PLUGS" + if [ "$PLUGS" -lt 20 ]; then + echo "Expected 20+ plugins, got $PLUGS" >&2 + exit 1 + fi + + mkdir -p "$TMP_ROOT/chopsticks path/modules" + cp .vimrc "$TMP_ROOT/chopsticks path/.vimrc" + cp modules/*.vim "$TMP_ROOT/chopsticks path/modules/" + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u "$TMP_ROOT/chopsticks path/.vimrc" \ + -i NONE -es -N -c 'qa!' 2>&1 + + vim -u NONE -i NONE -es -N \ + -c 'let g:chopsticks_profile = "minimal"' \ + -c 'source .vimrc' \ + -c 'if has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "vim-lsp-settings") || has_key(g:plugs, "asyncomplete.vim") | cquit | endif' \ + -c 'qa!' 2>&1 + + mkdir -p "$TMP_ROOT/local" + printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/local/config.vim" + vim -u NONE -i NONE -es -N \ + -c "let g:chopsticks_local_config = '$TMP_ROOT/local/config.vim'" \ + -c 'source .vimrc' \ + -c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") | cquit | endif' \ + -c 'qa!' 2>&1 + + mkdir -p "$TMP_ROOT/xdg" + printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/xdg/chopsticks.vim" + XDG_CONFIG_HOME="$TMP_ROOT/xdg" vim -u NONE -i NONE -es -N \ + -c 'source .vimrc' \ + -c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") | cquit | endif' \ + -c 'qa!' 2>&1 + + vim -u NONE -i NONE -es -N \ + -c 'let g:chopsticks_profile = "minimal"' \ + -c 'source .vimrc' \ + -c 'normal ,?' \ + -c "redir! > $TMP_ROOT/cheat.txt" \ + -c 'silent %print' \ + -c 'redir END' \ + -c 'qa!' 2>&1 + if grep -Eq 'definition|LspInstallServer|ALE errors|undo tree|markdown preview' "$TMP_ROOT/cheat.txt"; then + cat "$TMP_ROOT/cheat.txt" + exit 1 + fi + grep -q ',cr run file' "$TMP_ROOT/cheat.txt" + + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N README.md \ + -c 'set filetype=markdown' \ + -c 'if &l:spell || &l:conceallevel != 0 || &l:signcolumn !=# "no" || exists("g:lsp_settings_filetype_markdown") | cquit | endif' \ + -c 'qa!' 2>&1 + + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \ + -c 'if maparg("s", "n") !=# "" | cquit | endif' \ + -c 'if maparg(",w", "n") =~# "!" | cquit | endif' \ + -c 'if !&swapfile || !&writebackup || &directory !~# "\.vim/.swap" | cquit | endif' \ + -c 'qa!' 2>&1 + + vim -u NONE -i NONE -es -N \ + -c 'let g:ale_fix_on_save = 0' \ + -c 'source .vimrc' \ + -c 'if g:ale_fix_on_save != 0 | cquit | endif' \ + -c 'qa!' 2>&1 + + truncate -s 11000000 "$TMP_ROOT/large.py" + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N "$TMP_ROOT/large.py" \ + -c 'set filetype=python' \ + -c 'if &l:syntax !=# "" || &l:undolevels != -1 || &l:swapfile || get(b:, "ale_enabled", 1) != 0 | cquit | endif' \ + -c 'qa!' 2>&1 + + XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE --startuptime "$TMP_ROOT/startup.log" \ + -es -N -c 'qa!' 2>/dev/null + tail -1 "$TMP_ROOT/startup.log" + STARTUP_MS="$(awk 'END { print $1 }' "$TMP_ROOT/startup.log")" + awk -v ms="$STARTUP_MS" -v limit="$STARTUP_LIMIT_MS" \ + 'BEGIN { if (ms > limit) exit 1 }' +} + +run_group() { + case "$1" in + shell) check_shell ;; + docs) check_docs ;; + installer) check_installer_modes ;; + bootstrap) check_bootstrap ;; + vim) check_vim ;; + all) + check_shell + check_docs + check_installer_modes + check_bootstrap + check_vim + ;; + *) + echo "Usage: scripts/test.sh [shell|docs|installer|bootstrap|vim|all]..." >&2 + exit 1 ;; + esac +} + +if [[ $# -eq 0 ]]; then + set -- all +fi + +for group in "$@"; do + run_group "$group" +done