698 lines
26 KiB
Go
698 lines
26 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type reporterTestWriter struct {
|
|
writes int
|
|
n int
|
|
err error
|
|
output bytes.Buffer
|
|
}
|
|
|
|
func (w *reporterTestWriter) Write(p []byte) (int, error) {
|
|
w.writes++
|
|
if w.err != nil || w.n < len(p) {
|
|
n := w.n
|
|
if n > len(p) {
|
|
n = len(p)
|
|
}
|
|
if n > 0 {
|
|
_, _ = w.output.Write(p[:n])
|
|
}
|
|
return n, w.err
|
|
}
|
|
return w.output.Write(p)
|
|
}
|
|
|
|
func TestReporterOutputSubmitsOnWrite(t *testing.T) {
|
|
var destination bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &destination})
|
|
writer := reporter.Output()
|
|
|
|
if got := reporter.state.presentation().Output.State; got != DeliveryNone {
|
|
t.Fatalf("state after obtaining output = %v, want no delivery", got)
|
|
}
|
|
if n, err := writer.Write([]byte("report")); err != nil || n != len("report") {
|
|
t.Fatalf("Write() = %d, %v", n, err)
|
|
}
|
|
if destination.String() != "report" {
|
|
t.Fatalf("output = %q", destination.String())
|
|
}
|
|
if got := reporter.state.presentation().Output.State; got != DeliveryComplete {
|
|
t.Fatalf("output state = %v, want complete", got)
|
|
}
|
|
}
|
|
|
|
type callerOwnedWriter struct {
|
|
bytes.Buffer
|
|
closeCalls int
|
|
}
|
|
|
|
func (w *callerOwnedWriter) Close() error {
|
|
w.closeCalls++
|
|
return nil
|
|
}
|
|
|
|
func TestReporterLeavesCallerWriterOpen(t *testing.T) {
|
|
destination := &callerOwnedWriter{}
|
|
reporter := newReporter(Streams{Out: destination})
|
|
_, _ = reporter.Output().Write([]byte("report"))
|
|
_ = reporter.RenderOutput(func(writer io.Writer) error {
|
|
_, err := io.WriteString(writer, " rendered")
|
|
return err
|
|
})
|
|
_ = reporter.finalize(Success(), nil)
|
|
|
|
if destination.String() != "report rendered" || destination.closeCalls != 0 {
|
|
t.Fatalf("destination = %q, Close calls = %d", destination.String(), destination.closeCalls)
|
|
}
|
|
}
|
|
|
|
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
|
|
var destination bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &destination})
|
|
reporter.setVisibility(modeQuiet, false, false)
|
|
writer := reporter.Output()
|
|
|
|
if n, err := writer.Write([]byte("hidden")); err != nil || n != len("hidden") {
|
|
t.Fatalf("quiet Write() = %d, %v", n, err)
|
|
}
|
|
if destination.Len() != 0 {
|
|
t.Fatalf("quiet output reached destination: %q", destination.String())
|
|
}
|
|
if got := reporter.state.presentation().Output.State; got != DeliverySuppressed {
|
|
t.Fatalf("output state = %v, want suppressed", got)
|
|
}
|
|
}
|
|
|
|
func TestReporterRetainsFirstWriteFailureAndStopsUsingDestination(t *testing.T) {
|
|
first := errors.New("first write failed")
|
|
second := errors.New("later write failed")
|
|
destination := &reporterTestWriter{n: 1, err: first}
|
|
reporter := newReporter(Streams{Out: destination})
|
|
writer := reporter.Output()
|
|
|
|
if n, err := writer.Write([]byte("first")); n != 1 || !errors.Is(err, first) {
|
|
t.Fatalf("first Write() = %d, %v", n, err)
|
|
}
|
|
if n, err := writer.Write([]byte("second")); n != 0 || !errors.Is(err, first) || errors.Is(err, second) {
|
|
t.Fatalf("second Write() = %d, %v, want retained first error", n, err)
|
|
}
|
|
if destination.writes != 1 {
|
|
t.Fatalf("destination writes = %d, want 1", destination.writes)
|
|
}
|
|
result := reporter.state.presentation().Output
|
|
if result.State != DeliveryIncomplete || !errors.Is(result.WriteError, first) {
|
|
t.Fatalf("delivery result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterNormalizesShortWriteAndRetainsIgnoredFailure(t *testing.T) {
|
|
destination := &reporterTestWriter{n: 2}
|
|
reporter := newReporter(Streams{Out: destination})
|
|
_, _ = reporter.Output().Write([]byte("short"))
|
|
|
|
result := reporter.state.presentation().Output
|
|
if result.State != DeliveryIncomplete || !errors.Is(result.WriteError, io.ErrShortWrite) {
|
|
t.Fatalf("delivery result = %#v, want short-write failure", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterRetainsFirstRenderFailurePerDestination(t *testing.T) {
|
|
firstOutputErr := errors.New("first output render failed")
|
|
secondOutputErr := errors.New("later output render failed")
|
|
diagnosticErr := errors.New("diagnostic render failed")
|
|
reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard})
|
|
|
|
reporter.state.mu.Lock()
|
|
reporter.state.recordRenderErrorLocked(destinationOutput, firstOutputErr)
|
|
reporter.state.recordRenderErrorLocked(destinationOutput, secondOutputErr)
|
|
reporter.state.recordRenderErrorLocked(destinationDiagnostics, diagnosticErr)
|
|
reporter.state.mu.Unlock()
|
|
|
|
result := reporter.state.presentation()
|
|
if !errors.Is(result.Output.RenderError, firstOutputErr) || errors.Is(result.Output.RenderError, secondOutputErr) {
|
|
t.Fatalf("output render error = %v, want first error only", result.Output.RenderError)
|
|
}
|
|
if !errors.Is(result.Diagnostics.RenderError, diagnosticErr) {
|
|
t.Fatalf("diagnostic render error = %v", result.Diagnostics.RenderError)
|
|
}
|
|
if result.Output.State != DeliveryIncomplete || result.Diagnostics.State != DeliveryIncomplete {
|
|
t.Fatalf("render failures were not incomplete: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterDestinationsTrackFailuresIndependently(t *testing.T) {
|
|
outputErr := errors.New("output failed")
|
|
output := &reporterTestWriter{n: 1, err: outputErr}
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Out: output, Err: &diagnostics})
|
|
|
|
_, _ = reporter.Output().Write([]byte("out"))
|
|
if n, err := reporter.state.write(destinationDiagnostics, []byte("warning")); err != nil || n != len("warning") {
|
|
t.Fatalf("diagnostic Write() = %d, %v", n, err)
|
|
}
|
|
if diagnostics.String() != "warning" {
|
|
t.Fatalf("diagnostic output = %q", diagnostics.String())
|
|
}
|
|
result := reporter.state.presentation()
|
|
if result.Output.State != DeliveryIncomplete || !errors.Is(result.Output.WriteError, outputErr) {
|
|
t.Fatalf("output result = %#v", result.Output)
|
|
}
|
|
if result.Diagnostics.State != DeliveryComplete || result.Diagnostics.WriteError != nil {
|
|
t.Fatalf("diagnostic result = %#v", result.Diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestReporterClosedHandlesReturnClosedErrorIncludingQuiet(t *testing.T) {
|
|
var destination bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &destination})
|
|
writer := reporter.Output()
|
|
reporter.setVisibility(modeQuiet, false, false)
|
|
reporter.close()
|
|
|
|
if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) {
|
|
t.Fatalf("late Write() = %d, %v", n, err)
|
|
}
|
|
if destination.Len() != 0 {
|
|
t.Fatalf("late output reached destination: %q", destination.String())
|
|
}
|
|
}
|
|
|
|
func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
|
|
reporter := newReporter(Streams{})
|
|
if reporter.state.output.writer != io.Discard || reporter.state.diagnostics.writer != io.Discard {
|
|
t.Fatalf("nil destinations were not normalized: %#v", reporter.state)
|
|
}
|
|
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)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mode reportingMode
|
|
progress bool
|
|
progressSet bool
|
|
want string
|
|
}{
|
|
{name: "ordinary", mode: modeOrdinary},
|
|
{name: "verbose", mode: modeVerbose, want: "not used\nWARN: not used\n"},
|
|
{name: "debug", mode: modeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"},
|
|
{name: "quiet overrides progress", mode: modeQuiet, progress: true, progressSet: true},
|
|
{name: "explicit ordinary progress", mode: modeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"},
|
|
{name: "explicit debug disable", mode: modeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
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)
|
|
_ = reporter.Info("not used")
|
|
_ = reporter.Warn("not used")
|
|
_ = reporter.Debug("DEBUG")
|
|
_ = reporter.Progress("PROGRESS")
|
|
if got := output.String(); got != test.want {
|
|
t.Fatalf("output = %q, want %q", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
reporter.setVisibility(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 {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reporter.Info("", func(w io.Writer) error {
|
|
_, err := io.WriteString(w, "native labelled detail\n")
|
|
return err
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "WARN: Native warning\nwith second line\ndetail one\ndetail two\nnative labelled detail\n"
|
|
if got := diagnostics.String(); got != want {
|
|
t.Fatalf("diagnostics = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
reporter.setVisibility(modeQuiet, false, false)
|
|
renderErr := errors.New("native renderer failed")
|
|
calls := 0
|
|
err := reporter.Warn("hidden warning", func(w io.Writer) error {
|
|
calls++
|
|
if _, writeErr := io.WriteString(w, "hidden detail"); writeErr != nil {
|
|
return writeErr
|
|
}
|
|
return renderErr
|
|
})
|
|
if !errors.Is(err, renderErr) {
|
|
t.Fatalf("Warn() error = %v, want renderer error", err)
|
|
}
|
|
if calls != 1 || diagnostics.Len() != 0 {
|
|
t.Fatalf("hidden renderer calls=%d, destination=%q", calls, diagnostics.String())
|
|
}
|
|
result := reporter.state.presentation().Diagnostics
|
|
if result.State != DeliveryIncomplete || !errors.Is(result.RenderError, renderErr) || result.WriteError != nil {
|
|
t.Fatalf("diagnostic result = %#v", result)
|
|
}
|
|
}
|
|
|
|
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)
|
|
_ = reporter.RenderOutput(func(io.Writer) error { return renderErr })
|
|
_ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr })
|
|
|
|
result := reporter.state.presentation()
|
|
if !errors.Is(result.Output.RenderError, renderErr) || !errors.Is(result.Diagnostics.RenderError, diagnosticErr) {
|
|
t.Fatalf("ignored render errors were not retained: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) {
|
|
destination := &reporterTestWriter{}
|
|
reporter := newReporter(Streams{Out: destination})
|
|
reporter.setVisibility(modeQuiet, false, false)
|
|
renderErr := errors.New("quiet encoder failed")
|
|
calls := 0
|
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
|
calls++
|
|
if _, err := io.WriteString(w, "discarded report"); err != nil {
|
|
return err
|
|
}
|
|
return renderErr
|
|
})
|
|
result := reporter.state.presentation().Output
|
|
if calls != 1 || destination.writes != 0 {
|
|
t.Fatalf("quiet rendering calls=%d destination writes=%d", calls, destination.writes)
|
|
}
|
|
if result.State != DeliveryIncomplete || !errors.Is(result.RenderError, renderErr) || result.WriteError != nil {
|
|
t.Fatalf("quiet render result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterCountsSuccessfulZeroByteRenderAsSubmitted(t *testing.T) {
|
|
reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard})
|
|
if err := reporter.RenderOutput(func(io.Writer) error { return nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reporter.setVisibility(modeVerbose, false, false)
|
|
if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := reporter.state.presentation()
|
|
if result.Output.State != DeliveryComplete || result.Diagnostics.State != DeliveryComplete {
|
|
t.Fatalf("zero-byte successful render states = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestReporterCountsHiddenZeroByteRenderAsSuppressed(t *testing.T) {
|
|
reporter := newReporter(Streams{Err: io.Discard})
|
|
if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := reporter.state.presentation().Diagnostics.State; got != DeliverySuppressed {
|
|
t.Fatalf("hidden zero-byte render state = %v, want suppressed", got)
|
|
}
|
|
}
|
|
|
|
func TestReporterRenderOutputSeparatesRenderAndWriteFailures(t *testing.T) {
|
|
t.Run("renderer failure leaves destination usable", func(t *testing.T) {
|
|
var output bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &output})
|
|
renderErr := errors.New("render failed")
|
|
if err := reporter.RenderOutput(func(io.Writer) error { return renderErr }); !errors.Is(err, renderErr) {
|
|
t.Fatalf("RenderOutput() error = %v", err)
|
|
}
|
|
if _, err := reporter.Output().Write([]byte("still usable")); err != nil {
|
|
t.Fatalf("destination rejected later write: %v", err)
|
|
}
|
|
result := reporter.state.presentation().Output
|
|
if !errors.Is(result.RenderError, renderErr) || result.WriteError != nil || output.String() != "still usable" {
|
|
t.Fatalf("result=%#v output=%q", result, output.String())
|
|
}
|
|
})
|
|
|
|
t.Run("propagated write error is not a renderer error", func(t *testing.T) {
|
|
writeErr := errors.New("writer failed")
|
|
destination := &reporterTestWriter{err: writeErr}
|
|
reporter := newReporter(Streams{Out: destination})
|
|
err := reporter.RenderOutput(func(w io.Writer) error {
|
|
_, writeErr := io.WriteString(w, "report")
|
|
return writeErr
|
|
})
|
|
if !errors.Is(err, writeErr) {
|
|
t.Fatalf("RenderOutput() error = %v", err)
|
|
}
|
|
result := reporter.state.presentation().Output
|
|
if result.RenderError != nil || !errors.Is(result.WriteError, writeErr) {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
})
|
|
|
|
t.Run("ignored write error is retained and returned", func(t *testing.T) {
|
|
writeErr := errors.New("writer failed")
|
|
reporter := newReporter(Streams{Out: &reporterTestWriter{err: writeErr}})
|
|
err := reporter.RenderOutput(func(w io.Writer) error {
|
|
_, _ = w.Write([]byte("report"))
|
|
return nil
|
|
})
|
|
if !errors.Is(err, writeErr) {
|
|
t.Fatalf("RenderOutput() error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("independent rendering and writing failures remain separate", func(t *testing.T) {
|
|
writeErr := errors.New("writer failed")
|
|
renderErr := errors.New("renderer failed independently")
|
|
reporter := newReporter(Streams{Out: &reporterTestWriter{err: writeErr}})
|
|
err := reporter.RenderOutput(func(w io.Writer) error {
|
|
_, _ = w.Write([]byte("report"))
|
|
return renderErr
|
|
})
|
|
if !errors.Is(err, writeErr) || !errors.Is(err, renderErr) {
|
|
t.Fatalf("RenderOutput() error = %v", err)
|
|
}
|
|
result := reporter.state.presentation().Output
|
|
if !errors.Is(result.WriteError, writeErr) || !errors.Is(result.RenderError, renderErr) {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
})
|
|
}
|
|
|
|
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)
|
|
start := make(chan struct{})
|
|
ready := make(chan struct{}, 2)
|
|
done := make(chan struct{}, 2)
|
|
go func() {
|
|
ready <- struct{}{}
|
|
<-start
|
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
|
_, _ = io.WriteString(w, "report-")
|
|
_, err := io.WriteString(w, "complete\n")
|
|
return err
|
|
})
|
|
done <- struct{}{}
|
|
}()
|
|
go func() {
|
|
ready <- struct{}{}
|
|
<-start
|
|
_ = reporter.Info("message")
|
|
done <- struct{}{}
|
|
}()
|
|
<-ready
|
|
<-ready
|
|
close(start)
|
|
<-done
|
|
<-done
|
|
got := output.String()
|
|
if got != "report-complete\nmessage\n" && got != "message\nreport-complete\n" {
|
|
t.Fatalf("concurrent output interleaved complete operations: %q", got)
|
|
}
|
|
})
|
|
|
|
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)
|
|
start := make(chan struct{})
|
|
ready := make(chan struct{}, 2)
|
|
done := make(chan struct{}, 2)
|
|
go func() {
|
|
ready <- struct{}{}
|
|
<-start
|
|
_ = reporter.Info("first", func(w io.Writer) error {
|
|
_, _ = io.WriteString(w, "detail-")
|
|
_, err := io.WriteString(w, "complete\n")
|
|
return err
|
|
})
|
|
done <- struct{}{}
|
|
}()
|
|
go func() {
|
|
ready <- struct{}{}
|
|
<-start
|
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
|
_, err := io.WriteString(w, "report\n")
|
|
return err
|
|
})
|
|
done <- struct{}{}
|
|
}()
|
|
<-ready
|
|
<-ready
|
|
close(start)
|
|
<-done
|
|
<-done
|
|
got := output.String()
|
|
if got != "first\ndetail-complete\nreport\n" && got != "report\nfirst\ndetail-complete\n" {
|
|
t.Fatalf("concurrent output interleaved complete operations: %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
reporter.setVisibility(modeVerbose, false, false)
|
|
if err := reporter.Warn("", func(w io.Writer) error {
|
|
_, err := io.WriteString(w, "native multiline\nsecond line\n")
|
|
return err
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := diagnostics.String(); got != "native multiline\nsecond line\n" {
|
|
t.Fatalf("diagnostics = %q", got)
|
|
}
|
|
if strings.HasPrefix(diagnostics.String(), "WARN:") {
|
|
t.Fatal("empty heading added a category prefix")
|
|
}
|
|
}
|
|
|
|
func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) {
|
|
var output, diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
|
|
reporter.setVisibility(modeDebug, false, false)
|
|
if err := reporter.Info("started"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deferred := reporter.AtEnd()
|
|
if err := deferred.Warn("first deferred"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := deferred.Info("second deferred"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outcome := reporter.finalize(Failure("operation failed"), func(r *Reporter) {
|
|
_ = r.Debug("handler complete")
|
|
})
|
|
want := "started\nWARN: first deferred\nsecond deferred\nDEBUG: handler complete\nERROR: operation failed\n"
|
|
if got := diagnostics.String(); got != want {
|
|
t.Fatalf("diagnostics = %q, want %q", got, want)
|
|
}
|
|
if outcome.Presentation.Diagnostics.State != DeliveryComplete || outcome.Presentation.Output.State != DeliveryNone {
|
|
t.Fatalf("presentation = %#v", outcome.Presentation)
|
|
}
|
|
}
|
|
|
|
func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
reporter.setVisibility(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}
|
|
deferred := reporter.AtEnd()
|
|
if err := deferred.Warn("queued", details...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
details[0] = writeSecond
|
|
details = append(details, writeSecond)
|
|
outcome := reporter.finalize(Success(), nil)
|
|
if got, want := diagnostics.String(), "WARN: queued\nfirst detail\n"; got != want {
|
|
t.Fatalf("diagnostics = %q, want %q", got, want)
|
|
}
|
|
if outcome.Kind != OutcomeSuccess || outcome.Status != 0 || outcome.Presentation.Diagnostics.State != DeliveryComplete {
|
|
t.Fatalf("outcome = %#v", outcome)
|
|
}
|
|
|
|
renderErr := errors.New("deferred renderer failed")
|
|
failedReporter := newReporter(Streams{Err: io.Discard})
|
|
failedReporter.setVisibility(modeQuiet, false, false)
|
|
if err := failedReporter.AtEnd().Warn("hidden", func(io.Writer) error { return renderErr }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failed := failedReporter.finalize(Success(), nil)
|
|
if failed.Kind != OutcomeOperational || failed.Status != 1 || !errors.Is(failed.Presentation.Diagnostics.RenderError, renderErr) {
|
|
t.Fatalf("deferred renderer failure outcome = %#v", failed)
|
|
}
|
|
}
|
|
|
|
func TestReporterFinalizationFallbackAndCauseRetention(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
cause := errors.New("sensitive operation detail")
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
outcome := reporter.finalize(Failure("").WithCause(cause), nil)
|
|
if outcome.Explanation.Message != "Command failed." {
|
|
t.Fatalf("fallback explanation = %#v", outcome.Explanation)
|
|
}
|
|
if !errors.Is(outcome.Cause, cause) || !strings.Contains(diagnostics.String(), "ERROR: Command failed.\n") {
|
|
t.Fatalf("outcome=%#v output=%q", outcome, diagnostics.String())
|
|
}
|
|
if strings.Contains(diagnostics.String(), cause.Error()) {
|
|
t.Fatalf("cause was rendered automatically: %q", diagnostics.String())
|
|
}
|
|
|
|
var zeroDiagnostics bytes.Buffer
|
|
zeroReporter := newReporter(Streams{Err: &zeroDiagnostics})
|
|
zero := zeroReporter.finalize(Outcome{Cause: cause}, nil)
|
|
if zero.Kind != OutcomeSuccess || zero.Status != 0 || !errors.Is(zero.Cause, cause) || zeroDiagnostics.Len() != 0 {
|
|
t.Fatalf("zero-value outcome with cause changed classification: %#v output=%q", zero, zeroDiagnostics.String())
|
|
}
|
|
|
|
var native bytes.Buffer
|
|
nativeReporter := newReporter(Streams{Err: &native})
|
|
nativeOutcome := nativeReporter.finalize(Failure("", func(io.Writer) error { return nil }), nil)
|
|
if nativeOutcome.Explanation.Message != "" || native.String() != "" {
|
|
t.Fatalf("explicit zero-byte explanation changed: outcome=%#v output=%q", nativeOutcome, native.String())
|
|
}
|
|
}
|
|
|
|
func TestReporterPresentationFailureChangesOutcomeAndPreservesDomain(t *testing.T) {
|
|
outputErr := errors.New("report write failed")
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &reporterTestWriter{err: outputErr}, Err: &diagnostics})
|
|
_, _ = reporter.Output().Write([]byte("partial"))
|
|
cause := errors.New("operation cause")
|
|
outcome := reporter.finalize(Success().WithCause(cause), nil)
|
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 || !errors.Is(outcome.Cause, cause) {
|
|
t.Fatalf("success presentation failure outcome = %#v", outcome)
|
|
}
|
|
if !errors.Is(outcome.Presentation.Output.WriteError, outputErr) || (!strings.Contains(diagnostics.String(), "deliver") || !strings.Contains(diagnostics.String(), "output")) {
|
|
t.Fatalf("presentation=%#v diagnostics=%q", outcome.Presentation, diagnostics.String())
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
outcome Outcome
|
|
wantKind OutcomeKind
|
|
wantStatus int
|
|
}{
|
|
{name: "usage becomes operational", outcome: Usage("invalid request").WithCause(cause), wantKind: OutcomeOperational, wantStatus: 1},
|
|
{name: "operational stays operational", outcome: Failure("operation failed").WithCause(cause), wantKind: OutcomeOperational, wantStatus: 1},
|
|
{name: "domain nonzero is retained", outcome: Domain("validation", 5).WithExplanation("rejected").WithCause(cause), wantKind: OutcomeDomain, wantStatus: 5},
|
|
{name: "domain zero becomes nonzero", outcome: Domain("validation", 0), wantKind: OutcomeDomain, wantStatus: 1},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
failing := newReporter(Streams{Out: &reporterTestWriter{err: outputErr}, Err: io.Discard})
|
|
_, _ = failing.Output().Write([]byte("partial"))
|
|
got := failing.finalize(test.outcome, nil)
|
|
if got.Kind != test.wantKind || got.Status != test.wantStatus {
|
|
t.Fatalf("outcome = %#v, want kind %q status %d", got, test.wantKind, test.wantStatus)
|
|
}
|
|
if got.Cause != nil && !errors.Is(got.Cause, cause) {
|
|
t.Fatalf("cause changed: %v", got.Cause)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReporterDoesNotRetryFailedDiagnosticDestination(t *testing.T) {
|
|
diagnosticErr := errors.New("diagnostic sink failed")
|
|
destination := &reporterTestWriter{err: diagnosticErr}
|
|
reporter := newReporter(Streams{Err: destination})
|
|
_, _ = reporter.state.write(destinationDiagnostics, []byte("warning"))
|
|
outcome := reporter.finalize(Success(), nil)
|
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 || destination.writes != 1 {
|
|
t.Fatalf("outcome=%#v diagnostic writes=%d", outcome, destination.writes)
|
|
}
|
|
if !errors.Is(outcome.Presentation.Diagnostics.WriteError, diagnosticErr) {
|
|
t.Fatalf("diagnostic delivery = %#v", outcome.Presentation.Diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestReporterUsesHealthyDiagnosticsAfterOutputRenderFailure(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Out: io.Discard, Err: &diagnostics})
|
|
renderErr := errors.New("report preparation failed")
|
|
_ = reporter.RenderOutput(func(io.Writer) error { return renderErr })
|
|
outcome := reporter.finalize(Success(), nil)
|
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 {
|
|
t.Fatalf("outcome = %#v", outcome)
|
|
}
|
|
if !errors.Is(outcome.Presentation.Output.RenderError, renderErr) || (!strings.Contains(diagnostics.String(), "render") || !strings.Contains(diagnostics.String(), "output")) {
|
|
t.Fatalf("presentation=%#v diagnostics=%q", outcome.Presentation, diagnostics.String())
|
|
}
|
|
}
|
|
|
|
func TestReporterTerminalDeliveryFailureIsRecordedWithoutRetry(t *testing.T) {
|
|
diagnosticErr := errors.New("terminal write failed")
|
|
destination := &reporterTestWriter{err: diagnosticErr}
|
|
reporter := newReporter(Streams{Err: destination})
|
|
outcome := reporter.finalize(Failure("explicit failure"), nil)
|
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 || destination.writes != 1 {
|
|
t.Fatalf("outcome=%#v diagnostic writes=%d", outcome, destination.writes)
|
|
}
|
|
if !errors.Is(outcome.Presentation.Diagnostics.WriteError, diagnosticErr) {
|
|
t.Fatalf("terminal delivery = %#v", outcome.Presentation.Diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestReporterTerminalRenderFailureIsRecordedWithoutRetry(t *testing.T) {
|
|
var diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Err: &diagnostics})
|
|
renderErr := errors.New("terminal renderer failed")
|
|
calls := 0
|
|
outcome := reporter.finalize(Failure("", func(io.Writer) error {
|
|
calls++
|
|
return renderErr
|
|
}), nil)
|
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 || calls != 1 || diagnostics.Len() != 0 {
|
|
t.Fatalf("outcome=%#v calls=%d output=%q", outcome, calls, diagnostics.String())
|
|
}
|
|
if !errors.Is(outcome.Presentation.Diagnostics.RenderError, renderErr) {
|
|
t.Fatalf("terminal presentation = %#v", outcome.Presentation.Diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestReporterFinalizationClosesOutputAndDeferredHandles(t *testing.T) {
|
|
var output, diagnostics bytes.Buffer
|
|
reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
|
|
writer := reporter.Output()
|
|
deferred := reporter.AtEnd()
|
|
_ = reporter.finalize(Success(), nil)
|
|
if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) {
|
|
t.Fatalf("late output = %d, %v", n, err)
|
|
}
|
|
if err := deferred.Warn("late warning"); !errors.Is(err, ErrReporterClosed) {
|
|
t.Fatalf("late deferred submission error = %v", err)
|
|
}
|
|
if output.Len() != 0 || diagnostics.Len() != 0 {
|
|
t.Fatalf("late output emitted: result=%q diagnostics=%q", output.String(), diagnostics.String())
|
|
}
|
|
}
|