diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index e634d3b..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "05:30" - timezone: "UTC" - target-branch: "next" - open-pull-requests-limit: 5 - groups: - github-actions: - patterns: - - "*" diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 5e85861..383ffa4 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -41,7 +41,12 @@ jobs: checked=0 while IFS= read -r sha; do - if git show -s --format='%B' "$sha" | grep -qiE "$DISALLOWED_TRAILER_PATTERN"; then + # grep -c, not grep -q. Under pipefail, grep -q exits on its first + # match while git is still writing, git takes SIGPIPE and returns + # 141, and the pipeline result turns a real match into a miss for + # any message larger than the pipe buffer. grep -c reads to the end, + # so the writer always finishes (z-shell/zi#486). + if [ "$(git show -s --format='%B' "$sha" | grep -ciE "$DISALLOWED_TRAILER_PATTERN")" -gt 0 ]; then echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" errors=$((errors + 1)) fi @@ -95,14 +100,62 @@ jobs: env: BRANCH: ${{ github.head_ref }} run: | - PATTERN='^(feature|bug|hotfix)-[1-9][0-9]*$' - if echo "$BRANCH" | grep -qE '^(dependabot|renovate)/' || \ + # decisions/0022: a shape check, not an identifier check. + # Traceability moved to Validate Issue Link. + PATTERN='^((feature|bug|hotfix)-[1-9][0-9]*(-[a-z0-9]+)*|(feat|fix|perf|refactor|docs|test|build|ci|style|chore|revert|feature|bug|hotfix)/[a-z0-9]+(-[a-z0-9]+)*)$' + if echo "$BRANCH" | grep -qE '^(dependabot|renovate|copilot|codex)/' || \ [ "$BRANCH" = "next" ]; then echo "βœ… OK" exit 0 fi if ! echo "$BRANCH" | grep -qE "$PATTERN"; then - echo "::error::Branch name must be feature-, bug-, or hotfix-" + echo "::error::Branch name must be feature-, bug-, or hotfix- with an optional lowercase slug, or / over the Conventional Commits types (z-shell/.github decisions/0022)" exit 1 fi echo "βœ… Branch name valid" + + issue-link: + name: Validate Issue Link + runs-on: ubuntu-latest + steps: + - name: "πŸ”— Check the pull request is traceable to an issue" + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + BRANCH: ${{ github.head_ref }} + EXEMPT_LABEL: meta:no-issue + run: | + set -euo pipefail + + # z-shell/.github decisions/0022 moves traceability off the branch + # name and onto the pull request. Three outcomes pass, and the job + # says which applied, so an exemption is visible in review rather + # than silent. Everything comes from the pull_request event payload, + # so the job needs no token. + + if printf '%s\n' "$BRANCH" | grep -qE '^(dependabot|renovate|copilot|codex)/'; then + echo "βœ… Exempt: $BRANCH is an automation branch" + exit 0 + fi + + if [ "$BRANCH" = "next" ]; then + echo "βœ… Exempt: next is the persistent integration branch" + exit 0 + fi + + if printf '%s\n' "$PR_LABELS" | tr ',' '\n' | grep -qxF "$EXEMPT_LABEL"; then + echo "βœ… Exempt: labelled $EXEMPT_LABEL" + exit 0 + fi + + # A bare #123, the cross-repository owner/repo#123 shorthand, or a + # full issue or pull-request URL. + ISSUE_REFERENCE_PATTERN='(^|[^A-Za-z0-9_])#[1-9][0-9]*([^0-9]|$)|[A-Za-z0-9._-]+/[A-Za-z0-9._-]+#[1-9][0-9]*|https://github\.com/[^/ ]+/[^/ ]+/(issues|pull)/[1-9][0-9]*' + + if printf '%s\n' "${PR_BODY:-}" | grep -qE "$ISSUE_REFERENCE_PATTERN"; then + echo "βœ… The pull request references an issue" + exit 0 + fi + + echo "::error::No issue reference found. Link the owning issue in the pull-request body (Closes #123, or a plain #123 for work an issue tracks but this does not close). If this pull request genuinely has no owning issue, a maintainer applies the ${EXEMPT_LABEL} label (z-shell/.github decisions/0022)." + exit 1 diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 0000000..cd15c77 --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,21 @@ +--- +name: Release Prepare + +on: + push: + branches: [main] + +permissions: + contents: read + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + propose: + uses: z-shell/.github/.github/workflows/release-prepare.yml@6f3d88335ca0ae77b795ec2883b4402b51f15c6a # main + with: + signed_tag: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..00ee8cd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +--- +name: Release + +on: + push: + tags: ["v*.*.*"] + +permissions: + actions: read + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Verify and publish + if: github.repository == 'z-shell/zi' + runs-on: ubuntu-latest + steps: + - name: Check out the tagged commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Verify release authorization + env: + GH_TOKEN: ${{ github.token }} + run: zsh -f scripts/verify-release-tag.zsh + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "Release $TAG already exists." + exit 0 + fi + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "Zi $TAG" \ + --generate-notes \ + --latest diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index b117eb4..3aa32b4 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -15,11 +15,16 @@ on: - "tests/message-formatting.zsh" - "tests/path-resolution.zsh" - "tests/parallel-update.zsh" + - "tests/plugin-autoload-fpath-scope.zsh" + - "tests/plugin-autoload-ice.zsh" + - "tests/nested-load-state.zsh" + - "tests/plugin-autoload-ownership.zsh" - "tests/plugin-standard-callbacks.zsh" - "tests/scheduler-idle.zsh" - "tests/fixtures/plugin-standard-callbacks/**" - "tests/self-update-reload.zsh" - "tests/snippet-directory-mirror.zsh" + - "tests/subst-nesting.zsh" - "tests/version-reporting.zsh" pull_request: paths: @@ -31,11 +36,16 @@ on: - "tests/message-formatting.zsh" - "tests/path-resolution.zsh" - "tests/parallel-update.zsh" + - "tests/plugin-autoload-fpath-scope.zsh" + - "tests/plugin-autoload-ice.zsh" + - "tests/nested-load-state.zsh" + - "tests/plugin-autoload-ownership.zsh" - "tests/plugin-standard-callbacks.zsh" - "tests/scheduler-idle.zsh" - "tests/fixtures/plugin-standard-callbacks/**" - "tests/self-update-reload.zsh" - "tests/snippet-directory-mirror.zsh" + - "tests/subst-nesting.zsh" - "tests/version-reporting.zsh" workflow_dispatch: {} @@ -117,6 +127,48 @@ jobs: - name: Test parallel update run: zsh -f tests/parallel-update.zsh + plugin-autoload-fpath-scope: + name: Plugin Autoload Fpath Scope + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test plugin autoload fpath scope + run: zsh -f tests/plugin-autoload-fpath-scope.zsh + plugin-autoload-ice: + name: Plugin Autoload Ice + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test autoload ice forms + run: zsh -f tests/plugin-autoload-ice.zsh + nested-load-state: + name: Nested Load State + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test nested load state + run: zsh -f tests/nested-load-state.zsh + + plugin-autoload-ownership: + name: Plugin Autoload Ownership + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test plugin autoload ownership + run: zsh -f tests/plugin-autoload-ownership.zsh + plugin-standard-callbacks: name: Plugin Standard Callbacks runs-on: ubuntu-latest @@ -194,6 +246,17 @@ jobs: - name: Test message formatting run: zsh -f tests/message-formatting.zsh + subst-nesting: + name: Subst Nesting + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test substitution nesting + run: zsh -f tests/subst-nesting.zsh + snippet-directory-mirror: name: Snippet directory mirror runs-on: ubuntu-latest diff --git a/scripts/verify-release-tag.zsh b/scripts/verify-release-tag.zsh new file mode 100755 index 0000000..c6dcd30 --- /dev/null +++ b/scripts/verify-release-tag.zsh @@ -0,0 +1,55 @@ +#!/usr/bin/env zsh + +emulate -L zsh +setopt err_return no_unset pipe_fail + +fail() { + print -u2 -- "release verification: $*" + return 1 +} + +tag=${GITHUB_REF_NAME:-} +repository=${GITHUB_REPOSITORY:-} + +[[ $repository == z-shell/zi ]] || fail "unexpected repository: ${repository:-unset}" +[[ $tag =~ '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' ]] || + fail "tag must match vX.Y.Z: ${tag:-unset}" + +tag_ref="refs/tags/${tag}" +[[ $(git cat-file -t "$tag_ref" 2>/dev/null) == tag ]] || + fail "tag must be annotated: $tag" + +git fetch --quiet --force --no-tags origin \ + refs/heads/main:refs/remotes/origin/main || + fail "could not fetch origin/main" + +target=$(git rev-parse "${tag_ref}^{}") || fail "could not resolve tag target" +main=$(git rev-parse refs/remotes/origin/main) || fail "could not resolve origin/main" +[[ $target == $main ]] || fail "tag target is not the current origin/main" + +tag_object=$(git rev-parse "$tag_ref") || fail "could not resolve tag object" +tag_json=$(gh api "repos/${repository}/git/tags/${tag_object}") || + fail "could not read tag verification" +jq -e --arg target "$target" \ + '.verification.verified == true and + .object.type == "commit" and + .object.sha == $target' <<<"$tag_json" >/dev/null || + fail "GitHub did not verify the signed tag and target" + +runs_json=$(gh api --method GET "repos/${repository}/actions/runs" \ + -f branch=main -f head_sha="$target" -f per_page=100) || + fail "could not read workflow runs" + +for workflow in Zsh 'ZD Integration' CodeQL 'Trunk Code Quality'; do + jq -e --arg name "$workflow" --arg target "$target" \ + '.workflow_runs | any( + .name == $name and + .head_branch == "main" and + .head_sha == $target and + .status == "completed" and + .conclusion == "success" + )' <<<"$runs_json" >/dev/null || + fail "required workflow did not succeed: $workflow" +done + +print -- "Release authorization verified for ${tag} at ${target}." diff --git a/tests/nested-load-state.zsh b/tests/nested-load-state.zsh new file mode 100755 index 0000000..71e56a1 --- /dev/null +++ b/tests/nested-load-state.zsh @@ -0,0 +1,87 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-nested-load-test.XXXXXXXX")" || + fail "create temporary directory" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" \ + "${temp_root}/inner" \ + "${temp_root}/plugin" \ + "${temp_root}/cloneonly" \ + "${temp_root}/snippetter" || fail "create isolated environment" + +builtin print -r -- ':' \ + > "${temp_root}/inner/inner.plugin.zsh" || fail "write inner plug-in" +builtin print -r -- ':' \ + > "${temp_root}/inner/snippet.zsh" || fail "write inner snippet" + +# Each outer plug-in records the current plug-in before and after a nested load +# of one shape, so the assertions can run outside the load. +record() { # record + builtin print -rl -- \ + 'builtin print -r -- "before ${ZI[CUR_USPL2]}" >> $ZI_TEST_LOG' \ + "$2" \ + 'builtin print -r -- "after ${ZI[CUR_USPL2]}" >> $ZI_TEST_LOG' \ + > "${temp_root}/${1}/${1}.plugin.zsh" || fail "write ${1} plug-in" +} +record plugin 'zi light $ZI_TEST_ROOT/inner >/dev/null 2>&1' +record cloneonly 'zi ice cloneonly; zi light $ZI_TEST_ROOT/inner >/dev/null 2>&1' +record snippetter 'zi snippet $ZI_TEST_ROOT/inner/snippet.zsh >/dev/null 2>&1' + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + ZI_TEST_LOG="${temp_root}/log" \ + zsh -f <<'ZSH' || fail "a nested load does not restore the enclosing plug-in" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 + +typeset shape +for shape ( plugin cloneonly snippetter ) { + : > "$ZI_TEST_LOG" + zi light "${ZI_TEST_ROOT}/${shape}" >/dev/null 2>&1 + typeset -a lines + lines=( ${(f)"$(<$ZI_TEST_LOG)"} ) + [[ ${lines[1]} == "before %${ZI_TEST_ROOT}/${shape}" ]] || { + builtin print -u2 -r -- "${shape}: unexpected state before the nested load: ${lines[1]}" + return 1 + } + [[ ${lines[2]} == "after %${ZI_TEST_ROOT}/${shape}" ]] || { + builtin print -u2 -r -- "${shape}: the nested load did not restore the enclosing plug-in: ${lines[2]}" + return 1 + } +} + +# Back at the top level nothing is loading, which is what clearing used to mean. +[[ -z ${ZI[CUR_USPL2]} && -z ${ZI[CUR_USR]} && -z ${ZI[CUR_PLUGIN]} ]] || { + builtin print -u2 -r -- "a load leaked to the top level: [${ZI[CUR_USR]}] [${ZI[CUR_PLUGIN]}] [${ZI[CUR_USPL2]}]" + return 1 +} +ZSH + +builtin print -r -- "ok - a nested load restores the enclosing plug-in and leaves the top level clear" diff --git a/tests/plugin-autoload-fpath-scope.zsh b/tests/plugin-autoload-fpath-scope.zsh new file mode 100755 index 0000000..421152b --- /dev/null +++ b/tests/plugin-autoload-fpath-scope.zsh @@ -0,0 +1,189 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-fpath-scope-test.XXXXXXXX")" || + fail "create temporary directory" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" \ + "${temp_root}/cwd" \ + "${temp_root}/foreign" \ + "${temp_root}/plugins/plusx/lib" || fail "create isolated environment" + +# The plug-in's own functions, one directly in the plug-in directory and one in +# an $fpath subdirectory it registers. Immediate `autoload +X' of either has to +# resolve while the plug-in is loading. +builtin print -r -- 'builtin print -r -- own-body' \ + > "${temp_root}/plugins/plusx/_issue_475_own" || fail "write own function" +builtin print -r -- 'builtin print -r -- lib-body' \ + > "${temp_root}/plugins/plusx/lib/_issue_475_lib" || fail "write lib function" + +# A function file that exists only in the directory the shell happens to be in. +# The working directory is not a search path and must never be consulted. +builtin print -r -- 'builtin print -r -- cwd-body' \ + > "${temp_root}/cwd/_issue_475_cwd" || fail "write working-directory function" + +# A function the plug-in does not own, in a directory the caller had in $fpath +# before the load began. This is what zsh itself ships to a plug-in, and what +# `Aloxaf/fzf-tab' needs when it copies $functions[_main_complete] at load time. +# Immediate `autoload +X' has to resolve it from the caller's search path, which +# means the +X branch must carry that path across its own localisation of $fpath. +builtin print -r -- 'builtin print -r -- foreign-body' \ + > "${temp_root}/foreign/_issue_488_foreign" || fail "write foreign function" + +builtin print -rl -- \ + '0=${(%):-%N}' \ + 'fpath+=( ${0:A:h}/lib )' \ + 'autoload +X -Uz _issue_475_own' \ + 'autoload +X -Uz _issue_475_lib' \ + 'autoload +X -Uz _issue_475_cwd 2>/dev/null' \ + 'autoload +X -Uz _issue_488_foreign 2>/dev/null' \ + ': a failed +X must not change the plug-in exit status' \ + > "${temp_root}/plugins/plusx/plusx.plugin.zsh" || fail "write plug-in" + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + zsh -f <<'ZSH' || fail "immediate autoload does not keep to the plug-in's own directories" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 + +fpath+=( "${ZI_TEST_ROOT}/foreign" ) +typeset before_fpath="${(j.:.)fpath}" before_FPATH="$FPATH" +builtin cd -q "${ZI_TEST_ROOT}/cwd" || return 1 +# blockf so that the plug-in's own `fpath+=' is reverted and anything left +# behind afterwards is a leak from the substitution rather than from the +# plug-in itself. +zi ice blockf +zi load "${ZI_TEST_ROOT}/plugins/plusx" >/dev/null 2>&1 + +# The `+X' branch replaces $fpath for the duration of the call. Both the array +# and the tied scalar have to be restored for the caller. +[[ ${(j.:.)fpath} == $before_fpath ]] || { + builtin print -u2 -r -- "\$fpath leaked out of the immediate autoload" + return 1 +} +[[ $FPATH == $before_FPATH ]] || { + builtin print -u2 -r -- "\$FPATH leaked out of the immediate autoload: $FPATH" + return 1 +} + +typeset result +result="$(_issue_475_own 2>&1)" || { + builtin print -u2 -r -- "immediate autoload of the plug-in's own function failed: $result" + return 1 +} +[[ $result == own-body ]] || { + builtin print -u2 -r -- "unexpected own body resolved: $result" + return 1 +} + +result="$(_issue_475_lib 2>&1)" || { + builtin print -u2 -r -- "immediate autoload from the plug-in's \$fpath subdirectory failed: $result" + return 1 +} +[[ $result == lib-body ]] || { + builtin print -u2 -r -- "unexpected lib body resolved: $result" + return 1 +} + +[[ ${functions[_issue_475_cwd]} != *cwd-body* ]] || { + builtin print -u2 -r -- "the working directory was searched: _issue_475_cwd was loaded from \$PWD" + return 1 +} + +result="$(_issue_488_foreign 2>&1)" || { + builtin print -u2 -r -- "immediate autoload of a function the plug-in does not own failed: $result" + return 1 +} +[[ $result == foreign-body ]] || { + builtin print -u2 -r -- "unexpected foreign body resolved: $result" + return 1 +} +ZSH + +builtin print -r -- "ok - immediate autoload resolves the plug-in's own and the caller's functions, and restores \$fpath" + +# The `-w' branch is the deliberate counter-case to everything above. It appends +# $PLUGIN_DIR to the caller's $fpath and, unlike the `+X' branch, must NOT +# localise it: `autoload -w ' only declares the functions the digest +# holds, and each one resolves lazily when it is first called, long after +# :zi-tmp-subst-autoload has returned. Localising that append reads like a leak +# fix and silently breaks every -w plug-in. Pin it. +# +# This block is independent of the one above: separate plug-in directory, and +# each check runs in its own `zsh -f', so neither the $fpath this one leaves +# behind nor its load order can reach the other. Insertions between them are +# safe. +command mkdir -p "${temp_root}/plugins/wdigest" || fail "create -w plug-in directory" +builtin print -r -- 'builtin print -r -- digest-body' \ + > "${temp_root}/plugins/wdigest/_issue_492_digest" || fail "write digest function" +# Compile from inside the directory: zcompile records the name it is given, so an +# absolute path would name the function by its full path. +( builtin cd -q "${temp_root}/plugins/wdigest" && + zsh -fc 'zcompile -U _issue_492_digest.zwc _issue_492_digest' ) || + fail "compile the digest" +command rm -f "${temp_root}/plugins/wdigest/_issue_492_digest" || fail "remove plain function" + +builtin print -rl -- \ + '0=${(%):-%N}' \ + 'autoload -w ${0:A:h}/_issue_492_digest.zwc' \ + > "${temp_root}/plugins/wdigest/wdigest.plugin.zsh" || fail "write -w plug-in" + +# No blockf here: blockf restores $fpath wholesale after the load and would mask +# both the append under test and any regression to it. +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + zsh -f <<'ZSH' || fail "-w autoload does not resolve after the load" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 + +zi load "${ZI_TEST_ROOT}/plugins/wdigest" >/dev/null 2>&1 + +# Called after the load, which is the whole point: the search path the -w branch +# appended has to still be there. +typeset result +result="$(_issue_492_digest 2>&1)" || { + builtin print -u2 -r -- "-w autoload did not resolve after the load: $result" + return 1 +} +[[ $result == digest-body ]] || { + builtin print -u2 -r -- "unexpected digest body resolved: $result" + return 1 +} +ZSH + +builtin print -r -- "ok - -w autoload keeps the plug-in directory on \$fpath for later resolution" diff --git a/tests/plugin-autoload-ice.zsh b/tests/plugin-autoload-ice.zsh new file mode 100755 index 0000000..f427cde --- /dev/null +++ b/tests/plugin-autoload-ice.zsh @@ -0,0 +1,110 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-autoload-ice-test.XXXXXXXX")" || + fail "create temporary directory" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" \ + "${temp_root}/fns" || fail "create isolated environment" + +# zi skips a plug-in it has already loaded, and the autoload substitution never +# touches a function that already exists, so every ice form needs both its own +# plug-in directory and its own function name. +typeset case_name +for case_name ( plain spaced tight bang ) { + command mkdir -p "${temp_root}/plugins/${case_name}" || fail "create plug-in ${case_name}" + builtin print -r -- "builtin print -r -- ${case_name}-body" \ + > "${temp_root}/plugins/${case_name}/_issue_476_${case_name}" || fail "write function for ${case_name}" + builtin print -r -- ': nothing, the ice does the work' \ + > "${temp_root}/plugins/${case_name}/${case_name}.plugin.zsh" || fail "write plug-in ${case_name}" +} + +# Reachable through $fpath only, for the @autoload helper, which runs outside +# any plug-in load. +builtin print -r -- 'builtin print -r -- at-plain-body' \ + > "${temp_root}/fns/_issue_476_at_plain" || fail "write @autoload function" +builtin print -r -- 'builtin print -r -- at-rename-body' \ + > "${temp_root}/fns/_issue_476_at_src" || fail "write @autoload rename source" + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + zsh -f <<'ZSH' || fail "the autoload'' ice does not resolve its forms" +builtin emulate -R zsh +setopt pipe_fail + +fpath=( "${ZI_TEST_ROOT}/fns" $fpath ) +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 + +typeset result + +check() { # check