Expose invocation reporting mode

This commit is contained in:
2026-11-08 14:59:37 +00:00
parent 5e5f61a306
commit 7dd30f7df3
8 changed files with 147 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
# Shared debug binding implementation plan # Shared debug binding implementation plan
Status: planned; no implementation stages complete. Status: in progress; Stage 1 complete.
Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`. Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`.
Execution audience: Luna coding agent. Execution audience: Luna coding agent.
@@ -70,6 +70,11 @@ publish releases, push refs, or implement Audita artifact capture.
## Stage 1 — Expose the resolved reporting mode ## Stage 1 — Expose the resolved reporting mode
**Complete.** The invocation exposes the existing reporter selection, including
zero and finalized values. Focused reporting and public accessor checks passed
with Go 1.27.0 and offline module resolution; concurrent reads also passed with
`-race -count=1`.
Primary files: [reporter.go](../../reporter.go), Primary files: [reporter.go](../../reporter.go),
[identity.go](../../identity.go), [execute.go](../../execute.go). [identity.go](../../identity.go), [execute.go](../../execute.go).

View File

@@ -59,6 +59,21 @@ type Invocation struct {
// manually constructed Invocation has no reporter. // manually constructed Invocation has no reporter.
func (inv Invocation) Reporter() *Reporter { return inv.reporter } func (inv Invocation) Reporter() *Reporter { return inv.reporter }
// ReportingMode returns the selected reporting mode, independently of delivery
// success. The selection is stable during the handler and remains available
// after finalization. A zero Invocation returns ReportingModeOrdinary. Reads
// are safe alongside concurrent reporting; reporting callbacks must not reenter
// the same reporter.
func (inv Invocation) ReportingMode() ReportingMode {
if inv.reporter == nil || inv.reporter.state == nil {
return ReportingModeOrdinary
}
state := inv.reporter.state
state.mu.Lock()
defer state.mu.Unlock()
return state.mode
}
// Help renders the selected command's help through this invocation's output. // Help renders the selected command's help through this invocation's output.
// It returns an operational failure when the invocation has no help renderer. // It returns an operational failure when the invocation has no help renderer.
func (inv Invocation) Help() Outcome { func (inv Invocation) Help() Outcome {

View File

@@ -10,13 +10,19 @@ import (
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta" commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
) )
type reportingMode uint8 // ReportingMode identifies the invocation's selected reporting visibility.
// Its zero value is ReportingModeOrdinary.
type ReportingMode uint8
const ( const (
modeOrdinary reportingMode = iota // ReportingModeOrdinary uses ordinary reporting visibility.
modeQuiet ReportingModeOrdinary ReportingMode = iota
modeVerbose // ReportingModeQuiet suppresses ordinary output and messages.
modeDebug ReportingModeQuiet
// ReportingModeVerbose enables verbose reporting.
ReportingModeVerbose
// ReportingModeDebug enables debug reporting.
ReportingModeDebug
) )
type reportingDestination uint8 type reportingDestination uint8
@@ -41,7 +47,7 @@ type reportingState struct {
diagnostics destinationState diagnostics destinationState
closed bool closed bool
deferredClosed bool deferredClosed bool
mode reportingMode mode ReportingMode
format Format format Format
progress bool progress bool
progressSet bool progressSet bool
@@ -109,7 +115,7 @@ func newReporter(streams Streams) *Reporter {
return &Reporter{state: &reportingState{ return &Reporter{state: &reportingState{
output: destinationState{writer: streams.Out}, output: destinationState{writer: streams.Out},
diagnostics: destinationState{writer: streams.Err}, diagnostics: destinationState{writer: streams.Err},
mode: modeOrdinary, mode: ReportingModeOrdinary,
}} }}
} }
@@ -125,13 +131,13 @@ func (r *Reporter) applySelection(selection commandmeta.ReportingSelection) {
} }
switch selection.Mode { switch selection.Mode {
case commandmeta.ReportingModeQuiet: case commandmeta.ReportingModeQuiet:
state.mode = modeQuiet state.mode = ReportingModeQuiet
case commandmeta.ReportingModeVerbose: case commandmeta.ReportingModeVerbose:
state.mode = modeVerbose state.mode = ReportingModeVerbose
case commandmeta.ReportingModeDebug: case commandmeta.ReportingModeDebug:
state.mode = modeDebug state.mode = ReportingModeDebug
default: default:
state.mode = modeOrdinary state.mode = ReportingModeOrdinary
} }
state.progress = selection.Progress state.progress = selection.Progress
state.progressSet = selection.ProgressSet state.progressSet = selection.ProgressSet
@@ -189,7 +195,7 @@ func (r *Reporter) RenderOutput(write func(io.Writer) error) error {
destination.submitted = true destination.submitted = true
writer := &coordinatedWriter{state: state, destination: destinationOutput} writer := &coordinatedWriter{state: state, destination: destinationOutput}
renderErr := write(writer) renderErr := write(writer)
if state.mode == modeQuiet { if state.mode == ReportingModeQuiet {
destination.suppressed = true destination.suppressed = true
} else if renderErr == nil && writer.writeError == nil { } else if renderErr == nil && writer.writeError == nil {
destination.delivered = true destination.delivered = true
@@ -489,17 +495,17 @@ func renderDetailsLocked(state *reportingState, details []Detail, visible bool)
func (state *reportingState) visible(category messageCategory) bool { func (state *reportingState) visible(category messageCategory) bool {
switch category { switch category {
case categoryInfo, categoryWarning: case categoryInfo, categoryWarning:
return state.mode == modeVerbose || state.mode == modeDebug return state.mode == ReportingModeVerbose || state.mode == ReportingModeDebug
case categoryDebug: case categoryDebug:
return state.mode == modeDebug return state.mode == ReportingModeDebug
case categoryProgress: case categoryProgress:
if state.mode == modeQuiet { if state.mode == ReportingModeQuiet {
return false return false
} }
if state.progressSet { if state.progressSet {
return state.progress return state.progress
} }
return state.mode == modeDebug return state.mode == ReportingModeDebug
default: default:
return false return false
} }
@@ -617,7 +623,7 @@ func (state *reportingState) destination(destination reportingDestination) *dest
func (state *reportingState) suppressed(destination reportingDestination) bool { func (state *reportingState) suppressed(destination reportingDestination) bool {
if destination == destinationOutput { if destination == destinationOutput {
return state.mode == modeQuiet return state.mode == ReportingModeQuiet
} }
return false return false
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) { func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
for _, mode := range []reportingMode{modeOrdinary, modeQuiet} { for _, mode := range []ReportingMode{ReportingModeOrdinary, ReportingModeQuiet} {
t.Run(fmt.Sprint(mode), func(t *testing.T) { t.Run(fmt.Sprint(mode), func(t *testing.T) {
reporter := newReporter(Streams{}) reporter := newReporter(Streams{})
setReporterVisibility(reporter, mode, false, false) setReporterVisibility(reporter, mode, false, false)
@@ -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})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, 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 {
@@ -87,7 +87,7 @@ func TestReporterDeferredAcceptanceEndsBeforeDelivery(t *testing.T) {
writer := &gatedDiagnosticWriter{entered: make(chan struct{}), release: make(chan struct{})} writer := &gatedDiagnosticWriter{entered: make(chan struct{}), release: make(chan struct{})}
entered := writer.entered entered := writer.entered
reporter := newReporter(Streams{Err: writer}) reporter := newReporter(Streams{Err: writer})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
deferred := reporter.AtEnd() deferred := reporter.AtEnd()
if err := deferred.Warn("accepted"); err != nil { if err := deferred.Warn("accepted"); err != nil {
t.Fatal(err) t.Fatal(err)

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 {
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, 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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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

@@ -12,7 +12,7 @@ import (
func TestReporterReportQuietAndClosed(t *testing.T) { func TestReporterReportQuietAndClosed(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
reporter := newReporter(Streams{Out: &out}) reporter := newReporter(Streams{Out: &out})
reporter.state.mode = modeQuiet reporter.state.mode = ReportingModeQuiet
err := reporter.Report(Report{Value: make(chan int)}) err := reporter.Report(Report{Value: make(chan int)})
if err == nil { if err == nil {
t.Fatal("expected serialization error") t.Fatal("expected serialization error")

View File

@@ -80,7 +80,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})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, 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") {
@@ -177,7 +177,7 @@ 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()
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
_ = reporter.finalize(Success(), nil) _ = 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) {
@@ -193,11 +193,11 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
if reporter.state.output.writer != io.Discard || reporter.state.diagnostics.writer != io.Discard { if reporter.state.output.writer != io.Discard || reporter.state.diagnostics.writer != io.Discard {
t.Fatalf("nil destinations were not normalized: %#v", reporter.state) t.Fatalf("nil destinations were not normalized: %#v", reporter.state)
} }
if reporter.state.mode != modeOrdinary || reporter.state.progress || reporter.state.progressSet { if reporter.state.mode != ReportingModeOrdinary || 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)
} }
setReporterVisibility(reporter, modeDebug, true, true) setReporterVisibility(reporter, ReportingModeDebug, true, true)
if reporter.state.mode != modeDebug || !reporter.state.progress || !reporter.state.progressSet { if reporter.state.mode != ReportingModeDebug || !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)
} }
} }
@@ -205,17 +205,17 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) { func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mode reportingMode mode ReportingMode
progress bool progress bool
progressSet bool progressSet bool
want string want string
}{ }{
{name: "ordinary", mode: modeOrdinary}, {name: "ordinary", mode: ReportingModeOrdinary},
{name: "verbose", mode: modeVerbose, want: "not used\nWARN: not used\n"}, {name: "verbose", mode: ReportingModeVerbose, want: "not used\nWARN: not used\n"},
{name: "debug", mode: modeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"}, {name: "debug", mode: ReportingModeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"},
{name: "quiet overrides progress", mode: modeQuiet, progress: true, progressSet: true}, {name: "quiet overrides progress", mode: ReportingModeQuiet, progress: true, progressSet: true},
{name: "explicit ordinary progress", mode: modeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"}, {name: "explicit ordinary progress", mode: ReportingModeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"},
{name: "explicit debug disable", mode: modeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"}, {name: "explicit debug disable", mode: ReportingModeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"},
} }
for _, test := range tests { for _, test := range tests {
@@ -237,7 +237,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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 {
@@ -258,7 +258,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})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, 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 {
@@ -284,7 +284,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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 })
@@ -297,7 +297,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})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, 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 {
@@ -321,7 +321,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)
} }
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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)
} }
@@ -409,7 +409,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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)
@@ -443,7 +443,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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)
@@ -481,7 +481,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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
@@ -499,7 +499,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})
setReporterVisibility(reporter, modeDebug, false, false) setReporterVisibility(reporter, ReportingModeDebug, false, false)
if err := reporter.Info("started"); err != nil { if err := reporter.Info("started"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -525,7 +525,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})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, 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}
@@ -545,7 +545,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})
setReporterVisibility(failedReporter, modeQuiet, false, false) setReporterVisibility(failedReporter, ReportingModeQuiet, 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)
} }
@@ -699,14 +699,14 @@ func TestReporterFinalizationClosesOutputAndDeferredHandles(t *testing.T) {
} }
} }
func setReporterVisibility(reporter *Reporter, mode reportingMode, progress, progressSet bool) { func setReporterVisibility(reporter *Reporter, mode ReportingMode, progress, progressSet bool) {
selection := commandmeta.ReportingSelection{Progress: progress, ProgressSet: progressSet} selection := commandmeta.ReportingSelection{Progress: progress, ProgressSet: progressSet}
switch mode { switch mode {
case modeQuiet: case ReportingModeQuiet:
selection.Mode = commandmeta.ReportingModeQuiet selection.Mode = commandmeta.ReportingModeQuiet
case modeVerbose: case ReportingModeVerbose:
selection.Mode = commandmeta.ReportingModeVerbose selection.Mode = commandmeta.ReportingModeVerbose
case modeDebug: case ReportingModeDebug:
selection.Mode = commandmeta.ReportingModeDebug selection.Mode = commandmeta.ReportingModeDebug
} }
reporter.applySelection(selection) reporter.applySelection(selection)

View File

@@ -0,0 +1,70 @@
package commandkit_test
import (
"context"
"io"
"sync"
"testing"
commandkit "go.maximumdirect.net/commandkit"
)
func TestInvocationReportingMode(t *testing.T) {
if got := (commandkit.Invocation{}).ReportingMode(); got != commandkit.ReportingModeOrdinary {
t.Fatalf("zero invocation mode = %v", got)
}
var retained commandkit.Invocation
var observed commandkit.ReportingMode
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(inv commandkit.Invocation, _ struct{}) commandkit.Outcome {
retained = inv
observed = inv.ReportingMode()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
flag string
want commandkit.ReportingMode
}{{"", commandkit.ReportingModeOrdinary}, {"--quiet", commandkit.ReportingModeQuiet}, {"--verbose", commandkit.ReportingModeVerbose}, {"--debug", commandkit.ReportingModeDebug}, {"", commandkit.ReportingModeOrdinary}} {
args := []string{"run"}
if tc.flag != "" {
args = append(args, tc.flag)
}
result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if result.Kind != commandkit.OutcomeSuccess || observed != tc.want || retained.ReportingMode() != tc.want {
t.Fatalf("%q: outcome=%#v mode=%v", tc.flag, result, retained.ReportingMode())
}
}
}
func TestInvocationReportingModeConcurrentReads(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(inv commandkit.Invocation, _ struct{}) commandkit.Outcome {
start := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for j := 0; j < 100; j++ {
if inv.ReportingMode() != commandkit.ReportingModeDebug {
t.Error("mode changed")
}
_ = inv.Reporter().Debug("diagnostic")
}
}()
}
close(start)
wg.Wait()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
if outcome := app.Execute(context.Background(), []string{"run", "--debug"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
t.Fatal(outcome)
}
}