Close callback contexts when callbacks panic

This commit is contained in:
2026-10-25 21:25:57 +00:00
parent ea27f17f5c
commit 08a8c25542
4 changed files with 90 additions and 6 deletions

View File

@@ -0,0 +1,73 @@
package engine
import (
"errors"
"testing"
)
func capturePanic(call func()) (recovered any) {
defer func() { recovered = recover() }()
call()
return nil
}
func requirePanicValue(t *testing.T, want any, call func()) {
t.Helper()
if got := capturePanic(call); got != want {
t.Fatalf("panic = %#v, want %#v", got, want)
}
}
func TestCallbackContextsCloseWhenCallbacksPanic(t *testing.T) {
type config struct {
Value string `yaml:"value"`
Version string `yaml:"version"`
}
t.Run("resolution", func(t *testing.T) {
sentinel := errors.New("resolution panic sentinel")
var retained *Resolution
got := capturePanic(func() {
_, _ = testLoad[config](NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
retained = resolution
panic(sentinel)
}}})
})
if got != sentinel {
t.Fatalf("callback panic = %#v, want original sentinel", got)
}
requirePanicValue(t, closedCallbackContextMessage, func() { retained.IsExplicit(Keys("value")) })
})
t.Run("validation", func(t *testing.T) {
sentinel := errors.New("validation panic sentinel")
var retained *Validation
got := capturePanic(func() {
_, _ = testLoad[config](NoSource(), Options[config]{Definition: Definition[config]{Validate: func(_ *config, validation *Validation) {
retained = validation
panic(sentinel)
}}})
})
if got != sentinel {
t.Fatalf("callback panic = %#v, want original sentinel", got)
}
requirePanicValue(t, closedCallbackContextMessage, func() { retained.Error(Keys("value"), "too late") })
})
t.Run("version", func(t *testing.T) {
sentinel := errors.New("version panic sentinel")
var retained *Validation
got := capturePanic(func() {
_, _ = testLoad[config](Bytes("version.yml", []byte("version: v1\n")), Options[config]{Definition: Definition[config]{
Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error {
retained = validation
panic(sentinel)
}},
}})
})
if got != sentinel {
t.Fatalf("callback panic = %#v, want original sentinel", got)
}
requirePanicValue(t, closedCallbackContextMessage, func() { retained.Warning(Keys("version"), "too late") })
})
}

View File

@@ -287,8 +287,11 @@ func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error
records: make(map[string]*resolutionRecord),
}
context := &Resolution{state: state}
err := resolver(configuration, context)
state.active = false
var err error
func() {
defer func() { state.active = false }()
err = resolver(configuration, context)
}()
if err != nil && !hasErrors(state.diagnostics) {
state.addDiagnostic(CodeResolveHookFailed, Path{}, "application resolution failed")
}

View File

@@ -102,8 +102,10 @@ func runValidation[T any](configuration *T, validator func(*T, *Validation), sch
invalidPath: CodeValidateInvalidPath, schema: schema, document: input,
}
context := &Validation{state: state}
validator(configuration, context)
state.active = false
func() {
defer func() { state.active = false }()
validator(configuration, context)
}()
after, issue := projectConfiguration(schema, reflect.ValueOf(*configuration), defaultProjectionLimits())
if issue != nil {
state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(issue.path))
@@ -253,8 +255,11 @@ func reportVersion(rule *VersionRule, version Version, root *document.Node) []Di
invalidPath: CodeVersionError, document: root,
}
validation := &Validation{state: state}
err := rule.Check(version, validation)
state.active = false
var err error
func() {
defer func() { state.active = false }()
err = rule.Check(version, validation)
}()
if err != nil && !hasErrors(state.diagnostics) {
location, _ := nearestDocumentLocation(root, rule.Path)
state.diagnostics = append(state.diagnostics, Diagnostic{