diff --git a/internal/consolereporter/total_test.go b/internal/consolereporter/total_test.go index d4362dc..12c603d 100644 --- a/internal/consolereporter/total_test.go +++ b/internal/consolereporter/total_test.go @@ -9,10 +9,8 @@ import ( "github.com/Disble/ditto/internal/dittotesting/fakescorecalculator" "github.com/Disble/ditto/internal/dittotesting/stubdiffer" "github.com/Disble/ditto/internal/future" - "github.com/Disble/ditto/internal/gatedreporter" "github.com/Disble/ditto/internal/gomutatedfile" "github.com/Disble/ditto/internal/result" - "github.com/Disble/ditto/internal/verbosereporter" "github.com/stretchr/testify/assert" ) @@ -33,74 +31,3 @@ func TestTotalSeparatesAnEmptyRunFromAFailingOne(t *testing.T) { assert.Equal(t, 1, reporter.Total()) } - -// TestTheDecoratorsForwardTheCount is backlog entry 12's class, caught once -// already: a decorator that drops a capability is refused by nothing. Without -// this the count is unreadable exactly when the stack is deepest. -func TestTheDecoratorsForwardTheCount(t *testing.T) { - logger := fakelogger.New() - console := consolereporter.New(logger, stubdiffer.New(""), fakescorecalculator.Always(0), 0) - - console.AddDiagnostic(ditto.NewDiagnostic( - future.Resolved(result.Ok("mutant killed")), - gomutatedfile.New("dummy", "dummy.go", nil, nil), - )) - console.Summarize() - - // Both decorators, and the gated one over the verbose one, which is the - // stack the CI gate actually builds and the one the count was unreadable in. - for name, wrapped := range map[string]ditto.Reporter{ - "verbose": verbosereporter.New(logger, console), - "gated": gatedreporter.New(logger, noGates{}, console), - "gated over verbose": gatedreporter.New(logger, noGates{}, verbosereporter.New(logger, console)), - } { - t.Run(name, func(t *testing.T) { - counted, ok := wrapped.(interface{ Total() int }) - if !ok { - t.Fatalf("the %s reporter does not forward the count", name) - } - - assert.Equal(t, 1, counted.Total()) - }) - } -} - -// noGates is a gated laboratory that gated nothing, which is all this needs: the -// counters are not the subject here, the forwarding is. -type noGates struct{} - -func (noGates) Gated() int { return 0 } -func (noGates) FellBack() int { return 0 } - -// TestADecoratorOverSomethingThatCannotCountSaysUnknown is the other half of the -// forwarding, and the half its mutants found unguarded. -// -// A decorator whose delegate cannot answer must read as UNKNOWN, not as zero. -// Zero means "the scope produced nothing to judge", and a decorator that -// invented it would turn a genuinely failing run into a silent success. The -// sentinel is -1 because a count cannot be negative. -func TestADecoratorOverSomethingThatCannotCountSaysUnknown(t *testing.T) { - logger := fakelogger.New() - - for name, wrapped := range map[string]ditto.Reporter{ - "verbose": verbosereporter.New(logger, countlessReporter{}), - "gated": gatedreporter.New(logger, noGates{}, countlessReporter{}), - } { - t.Run(name, func(t *testing.T) { - counted, ok := wrapped.(interface{ Total() int }) - if !ok { - t.Fatalf("the %s reporter does not forward the count at all", name) - } - - if got := counted.Total(); got != -1 { - t.Fatalf("Total() = %d over a reporter that cannot count, want -1", got) - } - }) - } -} - -// countlessReporter is every reporter that existed before the count did. -type countlessReporter struct{} - -func (countlessReporter) AddDiagnostic(*ditto.Diagnostic) {} -func (countlessReporter) Summarize() result.Result[any] { return result.Err[any]("") } diff --git a/internal/dittotesting/fakereporter/counting.go b/internal/dittotesting/fakereporter/counting.go new file mode 100644 index 0000000..9018e52 --- /dev/null +++ b/internal/dittotesting/fakereporter/counting.go @@ -0,0 +1,31 @@ +package fakereporter + +import ( + "github.com/Disble/ditto/internal/ditto" + "github.com/Disble/ditto/internal/result" +) + +// Counting and Countless are the two answers a reporter can give when asked how +// much it scored, and every decorator has to be checked against both. +// +// They live here because three packages need the same pair: the release that +// reads the count, and each decorator that has to forward it. A decorator that +// drops a capability is refused by nothing — backlog entry 12 — so the check is +// written once and applied where each decorator lives. + +// Counting reports a fixed total. The field is named Scored because Total is +// the method the optional interface asks for, and a struct cannot have both. +type Counting struct{ Scored int } + +func (Counting) AddDiagnostic(*ditto.Diagnostic) {} +func (Counting) Summarize() result.Result[any] { return result.Err[any]("") } + +// Total is the count this reporter was built with. +func (r Counting) Total() int { return r.Scored } + +// Countless is every reporter that existed before the count did, and any a +// caller supplies. +type Countless struct{} + +func (Countless) AddDiagnostic(*ditto.Diagnostic) {} +func (Countless) Summarize() result.Result[any] { return result.Err[any]("") } diff --git a/internal/gatedreporter/total_test.go b/internal/gatedreporter/total_test.go new file mode 100644 index 0000000..8b75031 --- /dev/null +++ b/internal/gatedreporter/total_test.go @@ -0,0 +1,28 @@ +package gatedreporter_test + +import ( + "testing" + + "github.com/Disble/ditto/internal/dittotesting/fakelogger" + "github.com/Disble/ditto/internal/dittotesting/fakereporter" + "github.com/Disble/ditto/internal/gatedreporter" + "github.com/stretchr/testify/assert" +) + +// This lives here rather than beside the reporter it decorates, and the reason is +// a property of the gated path worth knowing: a gated mutant is compiled and run +// with the tests of ITS OWN package. The first version of these assertions sat in +// internal/consolereporter, every one of them passed, and the gate still reported +// four survivors -- the mutants in this file were never judged by them. + +func TestTotalIsForwarded(t *testing.T) { + assert.Equal(t, 47, gatedreporter.New(fakelogger.New(), fakeGates{}, fakereporter.Counting{Scored: 47}).Total(), "a decorator that drops a capability is refused by nothing") +} + +// A delegate that cannot answer must read as UNKNOWN, not as zero. Zero means +// "the scope produced nothing to judge", which RunChanged treats as nothing to +// do -- so answering it for "I could not tell" would turn a genuinely failing +// run into a silent success. The sentinel is negative because a count cannot be. +func TestAnUnreadableCountIsNotZero(t *testing.T) { + assert.Equal(t, -1, gatedreporter.New(fakelogger.New(), fakeGates{}, fakereporter.Countless{}).Total()) +} diff --git a/internal/verbosereporter/total_test.go b/internal/verbosereporter/total_test.go new file mode 100644 index 0000000..e91eb0c --- /dev/null +++ b/internal/verbosereporter/total_test.go @@ -0,0 +1,28 @@ +package verbosereporter_test + +import ( + "testing" + + "github.com/Disble/ditto/internal/dittotesting/fakelogger" + "github.com/Disble/ditto/internal/dittotesting/fakereporter" + "github.com/Disble/ditto/internal/verbosereporter" + "github.com/stretchr/testify/assert" +) + +// This lives here rather than beside the reporter it decorates, and the reason is +// a property of the gated path worth knowing: a gated mutant is compiled and run +// with the tests of ITS OWN package. The first version of these assertions sat in +// internal/consolereporter, every one of them passed, and the gate still reported +// four survivors -- the mutants in this file were never judged by them. + +func TestTotalIsForwarded(t *testing.T) { + assert.Equal(t, 47, verbosereporter.New(fakelogger.New(), fakereporter.Counting{Scored: 47}).Total(), "a decorator that drops a capability is refused by nothing") +} + +// A delegate that cannot answer must read as UNKNOWN, not as zero. Zero means +// "the scope produced nothing to judge", which RunChanged treats as nothing to +// do -- so answering it for "I could not tell" would turn a genuinely failing +// run into a silent success. The sentinel is negative because a count cannot be. +func TestAnUnreadableCountIsNotZero(t *testing.T) { + assert.Equal(t, -1, verbosereporter.New(fakelogger.New(), fakereporter.Countless{}).Total()) +} diff --git a/scored_internal_test.go b/scored_internal_test.go index ec0f280..8a66d4e 100644 --- a/scored_internal_test.go +++ b/scored_internal_test.go @@ -3,24 +3,9 @@ package ditto import ( "testing" - "github.com/Disble/ditto/internal/ditto" - "github.com/Disble/ditto/internal/result" + "github.com/Disble/ditto/internal/dittotesting/fakereporter" ) -// countlessReporter is a reporter that cannot say how much it scored, which is -// every reporter that existed before the count did and any a caller supplies. -type countlessReporter struct{} - -func (countlessReporter) AddDiagnostic(*ditto.Diagnostic) {} -func (countlessReporter) Summarize() result.Result[any] { return result.Err[any]("") } - -// countingReporter can. -type countingReporter struct{ total int } - -func (countingReporter) AddDiagnostic(*ditto.Diagnostic) {} -func (countingReporter) Summarize() result.Result[any] { return result.Err[any]("") } -func (r countingReporter) Total() int { return r.total } - // TestScoredIsUnknownRatherThanZero is the distinction the whole empty-scope fix // rests on, and the one its mutants found unguarded. // @@ -32,13 +17,13 @@ func (r countingReporter) Total() int { return r.total } func TestScoredIsUnknownRatherThanZero(t *testing.T) { t.Parallel() - unreadable := &release{reporter: countlessReporter{}} + unreadable := &release{reporter: fakereporter.Countless{}} if got := unreadable.scored(); got != -1 { t.Fatalf("a reporter that cannot count scored %d, want -1", got) } for _, total := range []int{0, 1, 47} { - readable := &release{reporter: countingReporter{total: total}} + readable := &release{reporter: fakereporter.Counting{Scored: total}} if got := readable.scored(); got != total { t.Fatalf("scored() = %d, want %d", got, total) } @@ -52,7 +37,7 @@ func TestAnUnreadableCountIsNotAnEmptyScope(t *testing.T) { rel := &release{ opts: Options{MinimumThreshold: 0.5}, - reporter: countlessReporter{}, + reporter: fakereporter.Countless{}, } if rel.scored() == 0 {