Restore cancellation checks before report delivery

This commit is contained in:
2026-10-26 14:32:03 +00:00
parent 13a4d4eacd
commit de2266af75
9 changed files with 193 additions and 57 deletions

View File

@@ -3,7 +3,6 @@ package ckconfig
import (
"errors"
"fmt"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
@@ -31,16 +30,7 @@ func runConfigDiff[I, S any](invocation commandkit.Invocation, plan *configOpera
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration comparison was canceled.", acquired.Explanation, err)
}
err = reporter.RenderOutput(func(writer io.Writer) error {
report, err := renderDiffReport(acquired.Value)
if err != nil {
return err
}
_, err = writer.Write(report)
return err
return renderPreparedConfigReport(invocation, acquired.Explanation, func() ([]byte, error) {
return renderDiffReport(acquired.Value)
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration comparison was canceled.", acquired.Explanation, invocation.Context.Err())
}
return commandkit.Success()
}

View File

@@ -94,18 +94,13 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
}
return commandkit.Success()
}
err := reporter.RenderOutput(func(writer io.Writer) error {
return renderPreparedConfigReport(invocation, acquired.Explanation, func() ([]byte, error) {
var report bytes.Buffer
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
return err
return nil, err
}
_, err := writer.Write(report.Bytes())
return err
return report.Bytes(), nil
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
}
return commandkit.Success()
}
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {

View File

@@ -2,6 +2,7 @@ package ckconfig
import (
"fmt"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
@@ -74,3 +75,26 @@ func configAcquisitionFailure(
message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope)
return configFailure(message, acquired.Explanation, cause)
}
// Preserve the preparation/delivery cancellation checkpoint for buffered reports.
func renderPreparedConfigReport(invocation commandkit.Invocation, explanation commandkit.Explanation, prepare func() ([]byte, error)) commandkit.Outcome {
var canceled error
renderErr := invocation.Reporter().RenderOutput(func(writer io.Writer) error {
report, err := prepare()
if err != nil {
return err
}
if canceled = invocation.Context.Err(); canceled != nil {
return nil
}
_, err = writer.Write(report)
return err
})
if canceled == nil && renderErr != nil {
canceled = invocation.Context.Err()
}
if canceled != nil {
return configFailure("Configuration report preparation was canceled.", explanation, canceled)
}
return commandkit.Success()
}

View File

@@ -0,0 +1,69 @@
package ckconfig
import (
"context"
"errors"
"io"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
func TestBufferedReportCancellationBeforeDelivery(t *testing.T) {
for _, quiet := range []bool{false, true} {
t.Run(map[bool]string{false: "ordinary", true: "quiet"}[quiet], func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
prepared := false
app := newConfigTestApp(t, commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "report",
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
return renderPreparedConfigReport(invocation, commandkit.Explanation{Message: "selected context"}, func() ([]byte, error) {
prepared = true
cancel()
return []byte("prepared report"), nil
})
},
}))
args := []string{"report"}
if quiet {
args = append(args, "--quiet")
}
writes := 0
got := app.Execute(ctx, args, commandkit.Streams{Out: reportWriteFunc(func(p []byte) (int, error) {
writes++
return len(p), nil
})})
if !prepared || writes != 0 || got.Status != 1 || !errors.Is(got.Cause, context.Canceled) || got.Explanation.Message != "selected context" {
t.Fatalf("prepared=%t writes=%d outcome=%#v", prepared, writes, got)
}
if got.Presentation.Output.RenderError != nil || got.Presentation.Output.WriteError != nil {
t.Fatalf("cancellation became a presentation failure: %#v", got.Presentation.Output)
}
})
}
}
type reportWriteFunc func([]byte) (int, error)
func (write reportWriteFunc) Write(p []byte) (int, error) { return write(p) }
func TestBufferedReportDoesNotRejectCancellationAfterDeliveryStarts(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app := newConfigTestApp(t, commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "report",
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
return renderPreparedConfigReport(invocation, commandkit.Explanation{}, func() ([]byte, error) {
return []byte("report"), nil
})
},
}))
got := app.Execute(ctx, []string{"report"}, commandkit.Streams{Out: reportWriteFunc(func(p []byte) (int, error) {
cancel()
return io.Discard.Write(p)
})})
if got.Status != 0 || got.Cause != nil {
t.Fatalf("post-delivery cancellation changed the operation outcome: %#v", got)
}
}

View File

@@ -117,22 +117,17 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
}
err := reporter.RenderOutput(func(writer io.Writer) error {
return renderPreparedConfigReport(invocation, acquired.Explanation, func() ([]byte, error) {
var report bytes.Buffer
if plan.format == "yaml" {
if err := encodeYAML(&report, document); err != nil {
return err
return nil, err
}
} else {
writeSourceReportText(&report, document)
}
_, err := writer.Write(report.Bytes())
return err
return report.Bytes(), nil
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration source listing was canceled.", acquired.Explanation, invocation.Context.Err())
}
return commandkit.Success()
}
func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {