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

@@ -136,7 +136,9 @@ installation alongside aliases and integration commands.
`Report()` for formatted substantive values and optional text templates; `Report()` for formatted substantive values and optional text templates;
use `Output()` or `RenderOutput()` for deliberate raw output. Use `Warn`, `Info`, use `Output()` or `RenderOutput()` for deliberate raw output. Use `Warn`, `Info`,
`Progress`, and `Debug` for categorized messages. Queue a summary with `Progress`, and `Debug` for categorized messages. Queue a summary with
`AtEnd().Info` when it belongs after the handler returns. The `AtEnd().Info` when it belongs after the handler returns. Deferred submissions
are accepted until finalization begins; later submissions return
`ErrReporterClosed`. The
[CLI reference](../cli.md#reporting) owns flag grammar, visibility, prefixes, [CLI reference](../cli.md#reporting) owns flag grammar, visibility, prefixes,
and status behavior. and status behavior.

View File

@@ -1,6 +1,6 @@
# Consolidated audit catalog # Consolidated audit catalog
Status: **25 distinct open findings: three P2 and 22 P3**, consolidated from 30 Status: **24 distinct open findings: two P2 and 22 P3**, consolidated from 30
numbered entries in seven audit documents. Reviewed audit state: numbered entries in seven audit documents. Reviewed audit state:
`db732e3a1de55c844f77870e1c6db683c2685d5d`. `db732e3a1de55c844f77870e1c6db683c2685d5d`.
@@ -76,6 +76,12 @@ Also noted in the [documentation cross-references](13-documentation.md#previousl
## F03 — P2: Finalization can discard an accepted deferred message ## F03 — P2: Finalization can discard an accepted deferred message
**Complete.** The acceptance cutoff and batch detachment now share one locked
transition. A deterministic regression reproduces the former lost submission and
checks late empty/hidden submissions, accepted delivery, completion, and terminal
ordering. Focused reporter tests passed normally and under the race detector;
existing tests cover visibility and presentation failures.
Core reporter finalization detaches the deferred batch before closing Core reporter finalization detaches the deferred batch before closing
acceptance. A concurrent submission can return success while presentation is in acceptance. A concurrent submission can return success while presentation is in
progress, then be discarded when finalization clears the queue. The returned progress, then be discarded when finalization clears the queue. The returned

View File

@@ -1,6 +1,6 @@
# Audit remediation implementation plan # Audit remediation implementation plan
Status: in progress; Stages 1–2 complete. Status: in progress; Stages 1–3 complete.
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
intervening changes recorded decisions and retired the completed older intervening changes recorded decisions and retired the completed older
@@ -163,6 +163,13 @@ Complete when both binding replay and per-kind seed boundaries are isolated.
## Stage 3 — Close deferred acceptance atomically ## Stage 3 — Close deferred acceptance atomically
**Complete.** Finalization closes deferred acceptance while detaching its
batch; completion and terminal reporting remain available. The channel-controlled
regression failed against the original code and now passes alongside focused
reporter and hidden-deferred tests, both normally and with `-race -count=1`, in
the aligned Go 1.27.0 workspace with offline, read-only module resolution. The
old reporter test wrappers were replaced with real selection and finalization.
Findings: F03 and the reporter-lifecycle portion of F24. Source: `reporter.go`. Findings: F03 and the reporter-lifecycle portion of F24. Source: `reporter.go`.
1. Add a private deferred-acceptance/finalization state distinct from the final 1. Add a private deferred-acceptance/finalization state distinct from the final

View File

@@ -36,15 +36,16 @@ const (
) )
type reportingState struct { type reportingState struct {
mu sync.Mutex mu sync.Mutex
output destinationState output destinationState
diagnostics destinationState diagnostics destinationState
closed bool closed bool
mode reportingMode deferredClosed bool
format Format mode reportingMode
progress bool format Format
progressSet bool progress bool
deferred []deferredMessage progressSet bool
deferred []deferredMessage
} }
type destinationState struct { type destinationState struct {
@@ -78,8 +79,8 @@ type Reporter struct {
} }
// DeferredReporter queues messages for delivery during invocation finalization. // DeferredReporter queues messages for delivery during invocation finalization.
// Copies share the same invocation reporting state. Submissions made after // Copies share the same invocation reporting state. Submissions return
// finalization return ErrReporterClosed. // ErrReporterClosed once finalization begins, before the accepted batch is delivered.
type DeferredReporter struct { type DeferredReporter struct {
state *reportingState state *reportingState
} }
@@ -222,7 +223,8 @@ func (r *Reporter) Progress(message string, details ...Detail) error {
// AtEnd returns a view for messages delivered after the handler returns and // AtEnd returns a view for messages delivered after the handler returns and
// before terminal explanation delivery. The deferred view shares this // before terminal explanation delivery. The deferred view shares this
// invocation's state. // invocation's state. Acceptance ends when finalization begins, before queued
// messages are delivered.
func (r *Reporter) AtEnd() DeferredReporter { func (r *Reporter) AtEnd() DeferredReporter {
if r == nil { if r == nil {
return DeferredReporter{} return DeferredReporter{}
@@ -258,7 +260,7 @@ func (r DeferredReporter) enqueue(category messageCategory, message string, deta
state := r.state state := r.state
state.mu.Lock() state.mu.Lock()
defer state.mu.Unlock() defer state.mu.Unlock()
if state.closed { if state.closed || state.deferredClosed {
return ErrReporterClosed return ErrReporterClosed
} }
if !hasDetails(details) { if !hasDetails(details) {
@@ -290,6 +292,7 @@ func (r *Reporter) finalize(outcome Outcome, completion func(*Reporter)) Outcome
state.mu.Unlock() state.mu.Unlock()
return normalizeOutcome(outcome) return normalizeOutcome(outcome)
} }
state.deferredClosed = true
deferred := state.deferred deferred := state.deferred
state.deferred = nil state.deferred = nil
state.mu.Unlock() state.mu.Unlock()
@@ -664,23 +667,3 @@ func (state *reportingState) deliveryResultLocked(destination reportingDestinati
} }
return result return result
} }
func (r *Reporter) setVisibility(mode reportingMode, progress bool, progressSet bool) {
if r == nil || r.state == nil {
return
}
r.state.mu.Lock()
r.state.mode = mode
r.state.progress = progress
r.state.progressSet = progressSet
r.state.mu.Unlock()
}
func (r *Reporter) close() {
if r == nil || r.state == nil {
return
}
r.state.mu.Lock()
r.state.closed = true
r.state.mu.Unlock()
}

View File

@@ -13,7 +13,7 @@ func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
for _, mode := range []reportingMode{modeOrdinary, modeQuiet} { for _, mode := range []reportingMode{modeOrdinary, modeQuiet} {
t.Run(fmt.Sprint(mode), func(t *testing.T) { t.Run(fmt.Sprint(mode), func(t *testing.T) {
reporter := newReporter(Streams{}) reporter := newReporter(Streams{})
reporter.setVisibility(mode, false, false) setReporterVisibility(reporter, mode, false, false)
for index := 0; index < 256; index++ { for index := 0; index < 256; index++ {
if err := reporter.AtEnd().Debug(fmt.Sprintf("hidden %d", index), nil); err != nil { if err := reporter.AtEnd().Debug(fmt.Sprintf("hidden %d", index), nil); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -45,7 +45,7 @@ func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) { func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
failure := errors.New("hidden rendering failed") failure := errors.New("hidden rendering failed")
var order []string var order []string
details := []Detail{func(w io.Writer) error { details := []Detail{func(w io.Writer) error {
@@ -67,3 +67,57 @@ func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) {
t.Fatalf("hidden content leaked or queue retained: %q", diagnostics.String()) t.Fatalf("hidden content leaked or queue retained: %q", diagnostics.String())
} }
} }
type gatedDiagnosticWriter struct {
bytes.Buffer
entered chan struct{}
release chan struct{}
}
func (w *gatedDiagnosticWriter) Write(p []byte) (int, error) {
if w.entered != nil {
close(w.entered)
w.entered = nil
<-w.release
}
return w.Buffer.Write(p)
}
func TestReporterDeferredAcceptanceEndsBeforeDelivery(t *testing.T) {
writer := &gatedDiagnosticWriter{entered: make(chan struct{}), release: make(chan struct{})}
entered := writer.entered
reporter := newReporter(Streams{Err: writer})
setReporterVisibility(reporter, modeVerbose, false, false)
deferred := reporter.AtEnd()
if err := deferred.Warn("accepted"); err != nil {
t.Fatal(err)
}
completionEntered, finishCompletion := make(chan struct{}), make(chan struct{})
done := make(chan Outcome, 1)
go func() {
done <- reporter.finalize(Failure("terminal"), func(r *Reporter) {
close(completionEntered)
<-finishCompletion
_ = r.Info("completion")
})
}()
<-entered
late := make(chan error, 1)
go func() { late <- deferred.Warn("late") }()
close(writer.release)
<-completionEntered
if err := <-late; !errors.Is(err, ErrReporterClosed) {
t.Errorf("late submission = %v", err)
}
if err := deferred.Debug(""); !errors.Is(err, ErrReporterClosed) {
t.Errorf("empty hidden submission = %v", err)
}
close(finishCompletion)
outcome := <-done
if got := writer.String(); got != "WARN: accepted\ncompletion\nERROR: terminal\n" {
t.Errorf("diagnostics = %q", got)
}
if outcome.Presentation.Diagnostics.State != DeliveryComplete {
t.Fatalf("outcome = %#v", outcome)
}
}

View File

@@ -16,7 +16,7 @@ func TestReporterExplainsItsOwnFailuresWithoutExposingCauses(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
if quiet { if quiet {
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
} }
failure := errors.New("private rendering detail") failure := errors.New("private rendering detail")
render := func(io.Writer) error { return failure } render := func(io.Writer) error { return failure }
@@ -84,7 +84,7 @@ func TestReporterPreservesJoinedRendererEvidence(t *testing.T) {
writeErr := errors.Join(errors.New("sink failure"), errors.New("sink detail")) writeErr := errors.Join(errors.New("sink failure"), errors.New("sink detail"))
destination := &reporterTestWriter{err: writeErr} destination := &reporterTestWriter{err: writeErr}
reporter := newReporter(Streams{Out: destination, Err: destination}) reporter := newReporter(Streams{Out: destination, Err: destination})
reporter.setVisibility(modeVerbose, false, false) setReporterVisibility(reporter, modeVerbose, false, false)
render := func(w io.Writer) error { render := func(w io.Writer) error {
_, err := io.WriteString(w, "report") _, err := io.WriteString(w, "report")
return test.combine(renderErr, err) return test.combine(renderErr, err)

View File

@@ -6,6 +6,8 @@ import (
"io" "io"
"strings" "strings"
"testing" "testing"
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
) )
type reporterTestWriter struct { type reporterTestWriter struct {
@@ -77,7 +79,7 @@ func TestReporterLeavesCallerWriterOpen(t *testing.T) {
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) { func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
var destination bytes.Buffer var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination}) reporter := newReporter(Streams{Out: &destination})
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
writer := reporter.Output() writer := reporter.Output()
if n, err := writer.Write([]byte("hidden")); err != nil || n != len("hidden") { 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 var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination}) reporter := newReporter(Streams{Out: &destination})
writer := reporter.Output() writer := reporter.Output()
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
reporter.close() _ = reporter.finalize(Success(), nil)
if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) { if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) {
t.Fatalf("late Write() = %d, %v", n, err) 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 { 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) 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 { 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) 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) { t.Run(test.name, func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
reporter := newReporter(Streams{Err: &output}) 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.Info("not used")
_ = reporter.Warn("not used") _ = reporter.Warn("not used")
_ = reporter.Debug("DEBUG") _ = reporter.Debug("DEBUG")
@@ -234,7 +236,7 @@ func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) { func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) 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, 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 one\n"); return err },
func(w io.Writer) error { _, err := io.WriteString(w, "detail two\n"); return err }); err != nil { 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) { func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
renderErr := errors.New("native renderer failed") renderErr := errors.New("native renderer failed")
calls := 0 calls := 0
err := reporter.Warn("hidden warning", func(w io.Writer) error { 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") renderErr := errors.New("output renderer failed")
diagnosticErr := errors.New("diagnostic renderer failed") diagnosticErr := errors.New("diagnostic renderer failed")
reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard}) 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.RenderOutput(func(io.Writer) error { return renderErr })
_ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr }) _ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr })
@@ -294,7 +296,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) { func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) {
destination := &reporterTestWriter{} destination := &reporterTestWriter{}
reporter := newReporter(Streams{Out: destination}) reporter := newReporter(Streams{Out: destination})
reporter.setVisibility(modeQuiet, false, false) setReporterVisibility(reporter, modeQuiet, false, false)
renderErr := errors.New("quiet encoder failed") renderErr := errors.New("quiet encoder failed")
calls := 0 calls := 0
_ = reporter.RenderOutput(func(w io.Writer) error { _ = 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 { if err := reporter.RenderOutput(func(io.Writer) error { return nil }); err != nil {
t.Fatal(err) 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 { if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -406,7 +408,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
t.Run("render callback and message", func(t *testing.T) { t.Run("render callback and message", func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output}) reporter := newReporter(Streams{Out: &output, Err: &output})
reporter.setVisibility(modeVerbose, false, false) setReporterVisibility(reporter, modeVerbose, false, false)
start := make(chan struct{}) start := make(chan struct{})
ready := make(chan struct{}, 2) ready := make(chan struct{}, 2)
done := 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) { t.Run("message details and render callback", func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output}) reporter := newReporter(Streams{Out: &output, Err: &output})
reporter.setVisibility(modeVerbose, false, false) setReporterVisibility(reporter, modeVerbose, false, false)
start := make(chan struct{}) start := make(chan struct{})
ready := make(chan struct{}, 2) ready := make(chan struct{}, 2)
done := make(chan struct{}, 2) done := make(chan struct{}, 2)
@@ -478,7 +480,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) { func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
reporter.setVisibility(modeVerbose, false, false) setReporterVisibility(reporter, modeVerbose, false, false)
if err := reporter.Warn("", func(w io.Writer) error { if err := reporter.Warn("", func(w io.Writer) error {
_, err := io.WriteString(w, "native multiline\nsecond line\n") _, err := io.WriteString(w, "native multiline\nsecond line\n")
return err return err
@@ -496,7 +498,7 @@ func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) { func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) {
var output, diagnostics bytes.Buffer var output, diagnostics bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &diagnostics}) reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
reporter.setVisibility(modeDebug, false, false) setReporterVisibility(reporter, modeDebug, false, false)
if err := reporter.Info("started"); err != nil { if err := reporter.Info("started"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -522,7 +524,7 @@ func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *test
func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) { func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) 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 }) 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 }) writeSecond := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "second detail\n"); return err })
details := []Detail{writeFirst} details := []Detail{writeFirst}
@@ -542,7 +544,7 @@ func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *te
renderErr := errors.New("deferred renderer failed") renderErr := errors.New("deferred renderer failed")
failedReporter := newReporter(Streams{Err: io.Discard}) 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 { if err := failedReporter.AtEnd().Warn("hidden", func(io.Writer) error { return renderErr }); err != nil {
t.Fatal(err) 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()) 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)
}