Honor cancellation at standard inspection boundaries
This commit is contained in:
120
cancellation_test.go
Normal file
120
cancellation_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package commandkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardConfigurationCancellationBoundaries(t *testing.T) {
|
||||
for _, operation := range []string{"validate", "show", "sources", "explain"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("%s/before=%t", operation, before), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if before {
|
||||
cancel()
|
||||
}
|
||||
loads, contexts := 0, 0
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
|
||||
loads++
|
||||
cancel()
|
||||
return configkit.Load(configkit.NoSource(), configkit.Options[struct{}]{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}, NewContext: func() struct{} { contexts++; return struct{}{} }}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{"config", operation}
|
||||
if operation == "explain" {
|
||||
args = append(args, "--path", "")
|
||||
}
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(ctx, args, commandkit.Streams{Out: &out})
|
||||
want := 1
|
||||
if before {
|
||||
want = 0
|
||||
}
|
||||
if result.Kind != commandkit.OutcomeOperational || result.Status != 1 || !errors.Is(result.Cause, context.Canceled) || loads != want || contexts != want || out.Len() != 0 {
|
||||
t.Fatalf("result=%#v loads=%d contexts=%d out=%q", result, loads, contexts, out.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateCancellationRetainsCompletedValidationAndDiagnostics(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
calls := 0
|
||||
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
if calls == 2 {
|
||||
cancel()
|
||||
}
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}}, nil
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, diagnostics bytes.Buffer
|
||||
result := app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &out, Err: &diagnostics})
|
||||
if !errors.Is(result.Cause, context.Canceled) || !result.ReportIncomplete || calls != 2 || out.String() != "app: valid\n" || strings.Count(diagnostics.String(), "acquired warning") != 2 {
|
||||
t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, calls, out.String(), diagnostics.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefinitionInspectionCancellationBoundaries(t *testing.T) {
|
||||
for _, resource := range []string{"prompt", "profile"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("%s/before=%t", resource, before), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if before {
|
||||
cancel()
|
||||
}
|
||||
calls, contexts := 0, 0
|
||||
diagnostics := []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}
|
||||
suite := commandkit.PromptKitSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} },
|
||||
InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
calls++
|
||||
cancel()
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}, Diagnostics: diagnostics}, nil
|
||||
},
|
||||
InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
calls++
|
||||
cancel()
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}, Diagnostics: diagnostics}, nil
|
||||
},
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, errs bytes.Buffer
|
||||
result := app.Execute(ctx, []string{"promptkit", "show", resource, "example"}, commandkit.Streams{Out: &out, Err: &errs})
|
||||
want := 1
|
||||
if before {
|
||||
want = 0
|
||||
}
|
||||
if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, context.Canceled) || calls != want || contexts != want || out.Len() != 0 || (!before && !strings.Contains(errs.String(), "acquired warning")) {
|
||||
t.Fatalf("result=%#v calls=%d contexts=%d out=%q diagnostics=%q", result, calls, contexts, out.String(), errs.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@ architecture. The issues below affect behavior within the implemented structure.
|
||||
- R-02 complete: inline document starts are recognized and native content is
|
||||
retained, including block scalars.
|
||||
|
||||
- R-03 complete: standard inspection handlers check cancellation before runtime
|
||||
construction/acquisition, after acquisition, and before prepared report delivery.
|
||||
|
||||
## Findings and priority
|
||||
|
||||
P1 means a supported invocation is blocked or its report content is corrupted.
|
||||
|
||||
16
explain.go
16
explain.go
@@ -83,6 +83,9 @@ type secretReferenceDTO struct {
|
||||
}
|
||||
|
||||
func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigExplain)
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
@@ -100,11 +103,18 @@ func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], in
|
||||
if !ok || scope.Acquire == nil {
|
||||
return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name))
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return explanationFailure(err, nil, false)
|
||||
}
|
||||
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigExplain, Selection: input.Selection}, contexts[name])
|
||||
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
|
||||
if acquireErr != nil {
|
||||
return explanationFailure(acquireErr, diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return explanationFailure(err, diagnostics, false)
|
||||
}
|
||||
|
||||
if acquired.Value.Explanation == nil {
|
||||
return explanationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
|
||||
}
|
||||
@@ -112,6 +122,9 @@ func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], in
|
||||
if !found {
|
||||
return explanationFailure(fmt.Errorf("configuration path %q is not known in scope %q", path.String(), name), diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return explanationFailure(err, diagnostics, false)
|
||||
}
|
||||
if input.Format != "yaml" {
|
||||
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, path, diagnostics)
|
||||
}
|
||||
@@ -119,6 +132,9 @@ func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], in
|
||||
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
|
||||
return explanationFailure(err, diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return explanationFailure(err, diagnostics, false)
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
||||
outcome := explanationFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
|
||||
26
promptkit.go
26
promptkit.go
@@ -70,6 +70,11 @@ func loadPromptKitInspection[C any](ctx context.Context, selection Selection, ap
|
||||
if engine == nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, fmt.Errorf("PromptKit configuration loader returned a nil engine")
|
||||
}
|
||||
if ctx != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
}
|
||||
inspection, err := engine.InspectConfiguration()
|
||||
if err != nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
@@ -264,11 +269,20 @@ func newPromptKitProfileCommand[C any](suite PromptKitSuite[C]) (Command, error)
|
||||
}
|
||||
|
||||
func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
contextValue := promptKitContext(suite, input)
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
acquired, err := suite.InspectPrompt(invocation.Context, input.Selection, contextValue, input.ID, input.Version)
|
||||
if err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if acquired.Value == nil {
|
||||
return promptKitFailure(fmt.Errorf("PromptKit prompt inspection returned no result"), acquired.Diagnostics)
|
||||
}
|
||||
@@ -277,11 +291,20 @@ func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], i
|
||||
}
|
||||
|
||||
func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
contextValue := promptKitContext(suite, input)
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
acquired, err := suite.InspectProfile(invocation.Context, input.Selection, contextValue, input.ID)
|
||||
if err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if acquired.Value == nil {
|
||||
return promptKitFailure(fmt.Errorf("PromptKit profile inspection returned no result"), acquired.Diagnostics)
|
||||
}
|
||||
@@ -308,6 +331,9 @@ func writePromptKitReport(invocation Invocation, format string, report any, text
|
||||
} else if _, err := io.WriteString(&output, textReport); err != nil {
|
||||
return promptKitFailure(err, diagnostics)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, diagnostics)
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, []byte(output.String())); err != nil {
|
||||
outcome := incompleteReport(err)
|
||||
outcome.Diagnostics = diagnostics
|
||||
|
||||
19
show.go
19
show.go
@@ -31,6 +31,9 @@ type effectiveDocument struct {
|
||||
}
|
||||
|
||||
func runConfigShow[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigShow)
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
@@ -43,17 +46,27 @@ func runConfigShow[C any](invocation Invocation, suite ConfigSuite[C], input con
|
||||
if !ok || scope.Acquire == nil {
|
||||
return showFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return showFailure(err, diagnostics, false)
|
||||
}
|
||||
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigShow, Selection: input.Selection}, contexts[name])
|
||||
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
||||
if acquireErr != nil {
|
||||
return showFailure(acquireErr, diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return showFailure(err, diagnostics, false)
|
||||
}
|
||||
|
||||
if acquired.Value.Effective == nil {
|
||||
return showFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", name), diagnostics, false)
|
||||
}
|
||||
documents = append(documents, effectiveDocument{name: name, view: acquired.Value})
|
||||
}
|
||||
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return showFailure(err, diagnostics, false)
|
||||
}
|
||||
if len(documents) == 1 && input.Scope != "all" {
|
||||
checked := &checkedWriter{writer: invocation.Streams.Out}
|
||||
renderErr := documents[0].view.Effective.WriteEffective(checked)
|
||||
@@ -76,10 +89,16 @@ func runConfigShow[C any](invocation Invocation, suite ConfigSuite[C], input con
|
||||
|
||||
written := false
|
||||
for index, document := range documents {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return showFailure(err, diagnostics, written)
|
||||
}
|
||||
var native bytes.Buffer
|
||||
if err := document.view.Effective.WriteEffective(&native); err != nil {
|
||||
return showFailure(err, diagnostics, written)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return showFailure(err, diagnostics, written)
|
||||
}
|
||||
if index > 0 && hasYAMLDirective(native.Bytes()) {
|
||||
if err := writeReport(invocation.Streams.Out, []byte("...\n")); err != nil {
|
||||
outcome := showFailure(err, diagnostics, true)
|
||||
|
||||
13
sources.go
13
sources.go
@@ -44,6 +44,9 @@ type locationDTO struct {
|
||||
}
|
||||
|
||||
func runConfigSources[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigSources)
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
@@ -54,11 +57,18 @@ func runConfigSources[C any](invocation Invocation, suite ConfigSuite[C], input
|
||||
if !ok || scope.Acquire == nil {
|
||||
return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return sourceReportFailure(err, nil, false)
|
||||
}
|
||||
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigSources, Selection: input.Selection}, contexts[name])
|
||||
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
|
||||
if acquireErr != nil {
|
||||
return sourceReportFailure(acquireErr, diagnostics, false)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return sourceReportFailure(err, diagnostics, false)
|
||||
}
|
||||
|
||||
if acquired.Value.Sources == nil {
|
||||
return sourceReportFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
|
||||
}
|
||||
@@ -87,6 +97,9 @@ func runConfigSources[C any](invocation Invocation, suite ConfigSuite[C], input
|
||||
} else {
|
||||
writeSourceReportText(&report, document)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return sourceReportFailure(err, diagnostics, false)
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
||||
outcome := sourceReportFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
|
||||
10
validate.go
10
validate.go
@@ -34,6 +34,9 @@ func newConfigValidateCommand[C any](suite ConfigSuite[C]) (Command, error) {
|
||||
}
|
||||
|
||||
func runConfigValidation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigValidate)
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
@@ -46,12 +49,19 @@ func runConfigValidation[C any](invocation Invocation, suite ConfigSuite[C], inp
|
||||
if !ok || scope.Acquire == nil {
|
||||
return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return validationFailure(err, diagnostics, writtenRecord)
|
||||
}
|
||||
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigValidate, Selection: input.Selection}, contexts[name])
|
||||
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
||||
if acquireErr != nil {
|
||||
return validationFailure(acquireErr, diagnostics, writtenRecord)
|
||||
}
|
||||
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return validationFailure(err, diagnostics, writtenRecord)
|
||||
}
|
||||
|
||||
record, err := validationSuccessRecord(name, input.Format)
|
||||
if err != nil {
|
||||
return validationFailure(err, diagnostics, writtenRecord)
|
||||
|
||||
Reference in New Issue
Block a user