Close deferred reporting before final delivery

This commit is contained in:
2026-11-07 15:40:01 +00:00
parent c82235100e
commit 1fc6409f95
7 changed files with 123 additions and 56 deletions

View File

@@ -6,6 +6,8 @@ import (
"io"
"strings"
"testing"
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
)
type reporterTestWriter struct {
@@ -77,7 +79,7 @@ func TestReporterLeavesCallerWriterOpen(t *testing.T) {
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination})
reporter.setVisibility(modeQuiet, false, false)
setReporterVisibility(reporter, modeQuiet, false, false)
writer := reporter.Output()
if n, err := writer.Write([]byte("hidden")); err != nil || n != len("hidden") {
@@ -174,8 +176,8 @@ func TestReporterClosedHandlesReturnClosedErrorIncludingQuiet(t *testing.T) {
var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination})
writer := reporter.Output()
reporter.setVisibility(modeQuiet, false, false)
reporter.close()
setReporterVisibility(reporter, modeQuiet, false, false)
_ = reporter.finalize(Success(), nil)
if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) {
t.Fatalf("late Write() = %d, %v", n, err)
@@ -193,7 +195,7 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
if reporter.state.mode != modeOrdinary || reporter.state.progress || reporter.state.progressSet {
t.Fatalf("initial visibility = mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet)
}
reporter.setVisibility(modeDebug, true, true)
setReporterVisibility(reporter, modeDebug, true, true)
if reporter.state.mode != modeDebug || !reporter.state.progress || !reporter.state.progressSet {
t.Fatalf("visibility was not retained: mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet)
}
@@ -219,7 +221,7 @@ func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
var output bytes.Buffer
reporter := newReporter(Streams{Err: &output})
reporter.setVisibility(test.mode, test.progress, test.progressSet)
setReporterVisibility(reporter, test.mode, test.progress, test.progressSet)
_ = reporter.Info("not used")
_ = reporter.Warn("not used")
_ = reporter.Debug("DEBUG")
@@ -234,7 +236,7 @@ func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
if err := reporter.Warn("Native warning\nwith second line\n", nil,
func(w io.Writer) error { _, err := io.WriteString(w, "detail one\n"); return err },
func(w io.Writer) error { _, err := io.WriteString(w, "detail two\n"); return err }); err != nil {
@@ -255,7 +257,7 @@ func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) {
var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeQuiet, false, false)
setReporterVisibility(reporter, modeQuiet, false, false)
renderErr := errors.New("native renderer failed")
calls := 0
err := reporter.Warn("hidden warning", func(w io.Writer) error {
@@ -281,7 +283,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
renderErr := errors.New("output renderer failed")
diagnosticErr := errors.New("diagnostic renderer failed")
reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
_ = reporter.RenderOutput(func(io.Writer) error { return renderErr })
_ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr })
@@ -294,7 +296,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) {
destination := &reporterTestWriter{}
reporter := newReporter(Streams{Out: destination})
reporter.setVisibility(modeQuiet, false, false)
setReporterVisibility(reporter, modeQuiet, false, false)
renderErr := errors.New("quiet encoder failed")
calls := 0
_ = reporter.RenderOutput(func(w io.Writer) error {
@@ -318,7 +320,7 @@ func TestReporterCountsSuccessfulZeroByteRenderAsSubmitted(t *testing.T) {
if err := reporter.RenderOutput(func(io.Writer) error { return nil }); err != nil {
t.Fatal(err)
}
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil {
t.Fatal(err)
}
@@ -406,7 +408,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
t.Run("render callback and message", func(t *testing.T) {
var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
start := make(chan struct{})
ready := make(chan struct{}, 2)
done := make(chan struct{}, 2)
@@ -440,7 +442,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
t.Run("message details and render callback", func(t *testing.T) {
var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
start := make(chan struct{})
ready := make(chan struct{}, 2)
done := make(chan struct{}, 2)
@@ -478,7 +480,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
if err := reporter.Warn("", func(w io.Writer) error {
_, err := io.WriteString(w, "native multiline\nsecond line\n")
return err
@@ -496,7 +498,7 @@ func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) {
var output, diagnostics bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
reporter.setVisibility(modeDebug, false, false)
setReporterVisibility(reporter, modeDebug, false, false)
if err := reporter.Info("started"); err != nil {
t.Fatal(err)
}
@@ -522,7 +524,7 @@ func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *test
func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) {
var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeVerbose, false, false)
setReporterVisibility(reporter, modeVerbose, false, false)
writeFirst := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "first detail\n"); return err })
writeSecond := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "second detail\n"); return err })
details := []Detail{writeFirst}
@@ -542,7 +544,7 @@ func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *te
renderErr := errors.New("deferred renderer failed")
failedReporter := newReporter(Streams{Err: io.Discard})
failedReporter.setVisibility(modeQuiet, false, false)
setReporterVisibility(failedReporter, modeQuiet, false, false)
if err := failedReporter.AtEnd().Warn("hidden", func(io.Writer) error { return renderErr }); err != nil {
t.Fatal(err)
}
@@ -695,3 +697,16 @@ func TestReporterFinalizationClosesOutputAndDeferredHandles(t *testing.T) {
t.Fatalf("late output emitted: result=%q diagnostics=%q", output.String(), diagnostics.String())
}
}
func setReporterVisibility(reporter *Reporter, mode reportingMode, progress, progressSet bool) {
selection := commandmeta.ReportingSelection{Progress: progress, ProgressSet: progressSet}
switch mode {
case modeQuiet:
selection.Mode = commandmeta.ReportingModeQuiet
case modeVerbose:
selection.Mode = commandmeta.ReportingModeVerbose
case modeDebug:
selection.Mode = commandmeta.ReportingModeDebug
}
reporter.applySelection(selection)
}