Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 0 additions & 73 deletions internal/consolereporter/total_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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]("") }
31 changes: 31 additions & 0 deletions internal/dittotesting/fakereporter/counting.go
Original file line number Diff line number Diff line change
@@ -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) {}

Check failure on line 20 in internal/dittotesting/fakereporter/counting.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Disble_ditto&issues=AaBUxqHK5SgV3A_zj_cE&open=AaBUxqHK5SgV3A_zj_cE&pullRequest=17
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) {}

Check failure on line 30 in internal/dittotesting/fakereporter/counting.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Disble_ditto&issues=AaBUxqHK5SgV3A_zj_cF&open=AaBUxqHK5SgV3A_zj_cF&pullRequest=17
func (Countless) Summarize() result.Result[any] { return result.Err[any]("") }
28 changes: 28 additions & 0 deletions internal/gatedreporter/total_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
28 changes: 28 additions & 0 deletions internal/verbosereporter/total_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
23 changes: 4 additions & 19 deletions scored_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down
Loading