Expose invocation reporting mode
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||
|
|||||||
15
identity.go
15
identity.go
@@ -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 {
|
||||||
|
|||||||
40
reporter.go
40
reporter.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
70
shared_debug_external_test.go
Normal file
70
shared_debug_external_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user