Add atomic reporting and message categories
This commit is contained in:
295
reporter_test.go
295
reporter_test.go
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -172,3 +173,297 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user