Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,35 @@ assert before plugin_unloaded loaded user_changed after
For each resource, this assertion restores the pre-load value when the user did
not change the plugin-owned value, otherwise it requires unload to preserve the
user's newer value. Use separate clean-shell tests for hostile initial state,
partial initialization failure, and interactive behavior. The repository's
`tests/_support/plugin-contract/scenario.zsh` demonstrates repeated source,
partial failure, hostile state, post-load changes, and both `zsh -f` and
`zsh -f -i` execution.
partial initialization failure, and interactive behavior.

Repeated source does not prove that a plugin works after it was unloaded. In
another clean shell, load, unload, and load again, then compare the second
load with the first and observe that each restored handler, hook, widget or
other resource still does its job; its presence or a zero status alone would
accept an inert stub. Every restored resource with its own behavior needs its
own check:

```zsh
zunit_plugin_contract_prime
source ./example.plugin.zsh
zunit_plugin_contract_snapshot loaded

example_plugin_unload
source ./example.plugin.zsh
zunit_plugin_contract_snapshot reloaded
assert loaded plugin_restored reloaded

run example_refresh
assert $state equals 0
assert "$output" same_as 'refreshed'
```

The repository's `tests/_support/plugin-contract/scenario.zsh` demonstrates
repeated source, partial failure, hostile state, post-load changes, reload
after unload, and both `zsh -f` and `zsh -f -i` execution. Its reload
scenario checks the fixture's one callback, which the fixture's hook and
widget both dispatch to.

## 📖 Documentation

Expand Down
4 changes: 3 additions & 1 deletion tests/_support/plugin-contract/fixture.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

_contract_fixture_callback() {
builtin emulate -L zsh
:
# Observable effect, so a reload test can prove the callback still works.
(( ${+parameters[_contract_fixture_effect]} )) && _contract_fixture_effect=ran
return 0
}
typeset -g _contract_fixture_callback_body=${functions[_contract_fixture_callback]}
typeset -g _contract_fixture_value=plugin
Expand Down
57 changes: 57 additions & 0 deletions tests/_support/plugin-contract/reload-broken.plugin.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Negative fixture: the shipped fixture with the reload defect of
Comment thread
ss-o marked this conversation as resolved.
# z-shell/z-a-meta-plugins#54. Load defines its callback only when the name
# is free, and unload leaves the callback as an inert stub instead of removing
# it (as a plugin must when a manager can still dispatch to the name). The
# first load and repeated source are correct; after unload and a second load
# the stub survives, so the callback exists and returns 0 but does nothing.
() {
builtin emulate -L zsh
builtin setopt local_options typeset_silent

(( ${+parameters[_contract_fixture_active]} )) && return 0

(( ${+functions[_contract_fixture_callback]} )) || _contract_fixture_callback() {
builtin emulate -L zsh
(( ${+parameters[_contract_fixture_effect]} )) && _contract_fixture_effect=ran
return 0
}
typeset -g _contract_fixture_value=plugin

typeset -gi _contract_fixture_owned_hook=0
if (( ${precmd_functions[(Ie)_contract_fixture_callback]:-0} == 0 )); then
precmd_functions+=(_contract_fixture_callback)
_contract_fixture_owned_hook=1
fi

typeset -gi _contract_fixture_interactive=0
if [[ -o interactive ]]; then
zle -N contract-fixture-widget _contract_fixture_callback
bindkey -N contract-fixture-map emacs
bindkey -M contract-fixture-map '^Xz' contract-fixture-widget
_contract_fixture_interactive=1
fi

contract_fixture_plugin_unload() {
builtin emulate -L zsh
builtin setopt local_options typeset_silent

[[ ${_contract_fixture_value-} == plugin ]] && unset _contract_fixture_value
if (( _contract_fixture_owned_hook )); then
precmd_functions=(${precmd_functions:#_contract_fixture_callback})
(( ${#precmd_functions} )) || unset precmd_functions
fi
if (( _contract_fixture_interactive )); then
bindkey -D contract-fixture-map
zle -D contract-fixture-widget
fi

# The defect: keep the name defined but inert.
functions[_contract_fixture_callback]='return 0'

unset _contract_fixture_active _contract_fixture_owned_hook
unset _contract_fixture_interactive
unfunction contract_fixture_plugin_unload
}

typeset -gi _contract_fixture_active=1
}
36 changes: 36 additions & 0 deletions tests/_support/plugin-contract/reload-stale.plugin.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Negative fixture: a plugin whose reloaded surface matches its first load
# while its callback no longer works. A one-time setup is guarded by a flag
# that unload forgets to clear, so a second load skips it. The setup's state
# lives under a name the contract observer excludes (`_zunit_*`), standing
# in for state no snapshot family covers, such as a file or a daemon. After
# unload and a second load the callback exists, returns 0 and is
# byte-identical, but its effect is gone, so only an effect check catches it.
() {
builtin emulate -L zsh
builtin setopt local_options typeset_silent

(( ${+parameters[_contract_fixture_active]} )) && return 0

if (( ! ${+parameters[_zunit_reload_stale_setup_done]} )); then
Comment thread
ss-o marked this conversation as resolved.
typeset -g _zunit_reload_stale_ready=1
typeset -gi _zunit_reload_stale_setup_done=1
fi

_contract_fixture_callback() {
builtin emulate -L zsh
(( ${_zunit_reload_stale_ready:-0} )) || return 0
(( ${+parameters[_contract_fixture_effect]} )) && _contract_fixture_effect=ran
return 0
}

contract_fixture_plugin_unload() {
builtin emulate -L zsh
unset _zunit_reload_stale_ready
unfunction _contract_fixture_callback
# The defect: _zunit_reload_stale_setup_done is left set.
unset _contract_fixture_active
unfunction contract_fixture_plugin_unload
}

typeset -gi _contract_fixture_active=1
}
21 changes: 20 additions & 1 deletion tests/_support/plugin-contract/scenario.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ builtin setopt extended_glob typeset_silent
0="${${(M)0:#/*}:-$PWD/$0}"
typeset scenario=${1:-ordinary}
typeset expected_mode=${2:-noninteractive}
typeset fixture_name=${3:-fixture}
[[ $fixture_name == [A-Za-z0-9_-]## ]] || { print -u2 -r -- "invalid fixture: $fixture_name"; exit 2 }
Comment thread
ss-o marked this conversation as resolved.
typeset support_dir=${0:A:h}
typeset repository_dir=${support_dir:h:h:h}
typeset fixture=${support_dir}/fixture.plugin.zsh
typeset fixture=${support_dir}/${fixture_name}.plugin.zsh
Comment thread
ss-o marked this conversation as resolved.
[[ -r $fixture ]] || { print -u2 -r -- "missing fixture: $fixture_name"; exit 2 }

source "${repository_dir}/src/plugin-contract.zsh" || exit
zunit_plugin_contract_prime || exit
Expand Down Expand Up @@ -62,6 +65,22 @@ case $scenario in
zunit_plugin_contract_snapshot after || exit
_zunit_assert_plugin_unloaded before loaded user after
;;
reload)
Comment thread
ss-o marked this conversation as resolved.
# Load, unload, and load again in one clean shell. The second load must
# restore the first load's surface, and its callback must still do its
# job: presence or a zero status alone would accept an inert stub. Exact
# unload restoration is the ordinary scenario's job, so it is not
# repeated here.
source "$fixture" || exit
zunit_plugin_contract_snapshot loaded || exit
contract_fixture_plugin_unload || exit
source "$fixture" || exit
zunit_plugin_contract_snapshot reloaded || exit
_zunit_assert_plugin_restored loaded reloaded || exit
typeset -g _contract_fixture_effect=
_contract_fixture_callback || exit 23
[[ $_contract_fixture_effect == ran ]] || exit 24
;;
*)
print -u2 -r -- "unknown scenario: $scenario"
exit 2
Expand Down
35 changes: 35 additions & 0 deletions tests/plugin-contract.zunit
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,38 @@
assert $state equals 0
assert "$output" is_empty
}

@test 'Fixture restores its surface and behavior after reload' {
run zsh -f tests/_support/plugin-contract/scenario.zsh reload noninteractive

assert $state equals 0
assert "$output" is_empty
}

@test 'Fixture reloads in an interactive shell' {
run zsh -f -i tests/_support/plugin-contract/scenario.zsh reload interactive

assert $state equals 0
assert "$output" is_empty
}

@test 'Reload rejects a plugin that unload leaves as an inert stub' {
run zsh -f tests/_support/plugin-contract/scenario.zsh reload noninteractive reload-broken

assert $state equals 1
assert "$output" contains 'function:_contract_fixture_callback'
}

@test 'Reload rejects a restored callback that no longer has its effect' {
run zsh -f tests/_support/plugin-contract/scenario.zsh reload noninteractive reload-stale

assert $state equals 24
Comment thread
ss-o marked this conversation as resolved.
assert "$output" is_empty
}

@test 'Scenario refuses a fixture that does not exist' {
run zsh -f tests/_support/plugin-contract/scenario.zsh reload noninteractive does-not-exist

assert $state equals 2
assert "$output" same_as 'missing fixture: does-not-exist'
}
Loading