mutate

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultBaselinePath is committed, unlike the caches: it is curated policy
	// about which gaps are accepted, not derived data.
	DefaultBaselinePath = ".assay/mutation-baseline.json"
)
View Source
const MutHarnessFileName = "assay_mut_harness_test.go"

MutHarnessFileName is the TestMain file injected into a covering test package through -overlay. It never touches the source tree.

View Source
const PlanEnvVar = "ASSAY_MUT_PLAN"

PlanEnvVar points the harness at its instruction file. Unset, the injected TestMain is a plain os.Exit(m.Run()) — which is what lets the same binary serve the env-selected fallback path with per-process semantics intact.

View Source
const SchemataEnvVar = "ASSAY_MUTANT"

SchemataEnvVar names the mutant to activate. Zero, and anything unparseable, leaves every site behaving exactly as the original — which is what makes the same binary usable as its own control.

View Source
const SchemataFileName = "assay_schemata.go"

SchemataFileName is the file injected into a mutated package through -overlay. It never touches the source tree; the overlay makes the compiler see a file that does not exist on disk.

View Source
const TrackEnvVar = "ASSAY_MUTANT_TRACK"

TrackEnvVar switches on init-site tracking. Only the mutant harness sets it: tracking costs a sync.Map store per site execution until it is turned off, and the env-selected path never turns it off because it never needs the answer.

Variables

This section is empty.

Functions

func Modes

func Modes(results []Result) map[Mode]int

Modes counts how each mutant was judged, so a run can report how much of it took the fast path.

func MutHarnessSource

func MutHarnessSource(testPackageName, mutatedPath string, samePackage bool) []byte

MutHarnessSource renders the injected TestMain for a covering test package.

The protocol mirrors the index harness, with one addition: a begin line before each m.Run. The executor needs it to tell two deaths apart — a process that dies after announcing an instruction was killed by that mutant's tests, while a fresh process that dies before its first announcement has a broken harness, and the remaining mutants must fall back rather than be blamed.

func RunPattern

func RunPattern(tests []string) string

func SchemataSource

func SchemataSource(packageName string, helpers []string) []byte

SchemataSource renders the injected file for a package, emitting only the helpers the package's sites actually reference.

Types

type Advice

type Advice struct {
	// TestPackage and Test name the most focused covering test — the cheapest
	// place to add the missing assertion, because it already executes the line.
	TestPackage string `json:"testPackage"`
	Test        string `json:"test"`
	// Focus is how many lines that test covers in total. Smaller is closer.
	Focus int `json:"focusLines,omitempty"`
	// Duration is the test's indexed cost, so the reader knows the price of
	// iterating on it.
	Duration     time.Duration `json:"durationNanos,omitempty"`
	Prescription string        `json:"prescription"`
}

Advice turns a surviving mutant into a prescription: the covering test whose assertions sit closest to the mutated line, and what input would have told the original from the mutant. A survivor without advice is a metric; with it, it is a to-do item.

func Advise

func Advise(m Mutant, profile TestProfile) *Advice

Advise builds the prescription for one surviving mutant. Nil when nothing covers it — an uncovered site is a coverage gap, not an assertion gap, and the coverage report already owns that story.

type Baseline

type Baseline struct {
	Version   int             `json:"version"`
	Survivors []BaselineEntry `json:"survivors"`
	// contains filtered or unexported fields
}

func FromResults

func FromResults(results []Result) *Baseline

FromResults builds a baseline that accepts exactly the survivors observed.

func LoadBaseline

func LoadBaseline(path string) (*Baseline, error)

func NewBaseline

func NewBaseline() *Baseline

func (*Baseline) Accepts

func (b *Baseline) Accepts(id string) bool

func (*Baseline) Len

func (b *Baseline) Len() int

func (*Baseline) Save

func (b *Baseline) Save(path string) error

type BaselineEntry

type BaselineEntry struct {
	ID       string `json:"id"`
	Package  string `json:"package"`
	Function string `json:"function"`
	Kind     Kind   `json:"kind"`
	Note     string `json:"note,omitempty"`
}

type ExecuteOptions

type ExecuteOptions struct {
	Root       string
	Tags       []string
	Env        []string
	Jobs       int
	Budget     func(plan TestPlan) time.Duration
	MinTimeout time.Duration
	Progress   func(phase string, done, total int)

	// PackageDir resolves a test package's directory. Test binaries have to run
	// there, the way `go test` does, or a test that opens testdata/ fails for
	// reasons that have nothing to do with the mutant.
	PackageDir func(importPath string) string

	// PackageOrder orders a plan's covering test packages before judging. A
	// killed mutant exits at the first package that catches it, so an order
	// informed by cost finds the kill sooner. Nil, or an order that is not a
	// permutation of the plan, falls back to lexicographic.
	PackageOrder func(plan TestPlan) []string

	// NoSchemata forces every mutant to compile its own binary. Slow, and the only
	// way to cross-check the shared-binary path.
	NoSchemata bool

	// NoHarness keeps the shared binary but spawns a process per (mutant, test
	// package) instead of switching mutants inside one; the only way to
	// cross-check the harness path.
	NoHarness bool
}

func (ExecuteOptions) PackageBudget

func (o ExecuteOptions) PackageBudget(plan TestPlan, testPkg string) time.Duration

PackageBudget derives the timeout for one test package's run from that package's own tests. Handing every package the whole plan's budget lets a mutant judged by N packages consume N times its allowance.

type GenerateOptions

type GenerateOptions struct {
	Root     string
	Package  string
	Tags     []string
	Env      []string
	Eligible LineFilter
	Tests    func(absPath string, line int) TestPlan
}

type Kind

type Kind string
const (
	KindArithmetic Kind = "arithmetic"
	KindBoundary   Kind = "boundary"
	KindEquality   Kind = "equality"
	KindConnector  Kind = "connector"
	KindBoolean    Kind = "boolean"
	KindBranch     Kind = "branch"
	KindIncDec     Kind = "incdec"
)

func Kinds

func Kinds() []Kind

type LineFilter

type LineFilter func(absPath string, line int) bool

LineFilter answers whether a mutation site is eligible. Sites outside it are never generated, so an uncovered or unchanged line costs nothing.

type Mode

type Mode string

Mode records how a mutant was judged. Every mode must reach the same verdict; they differ only in what they pay. Overlay compiles a binary per mutant, schemata shares one compile across a package's mutants but still spawns a process per (mutant, test package), and harness shares the processes too — one per (test package, shard), switching mutants between test runs.

const (
	ModeSchemata Mode = "schemata"
	ModeOverlay  Mode = "overlay"
	ModeHarness  Mode = "harness"
)

type Mutant

type Mutant struct {
	ID          string `json:"id"`
	Kind        Kind   `json:"kind"`
	Package     string `json:"package"`
	File        string `json:"file"`
	Line        int    `json:"line"`
	Function    string `json:"function"`
	Original    string `json:"original"`
	Replacement string `json:"replacement"`
	// contains filtered or unexported fields
}

func Exclude

func Exclude(mutants []Mutant, failing map[string][]string) []Mutant

Exclude drops known-failing tests from every mutant's plan so the remaining judgements stay meaningful. A mutant left with no tests becomes no-coverage rather than being silently counted as killed.

func Generate

func Generate(ctx context.Context, opts GenerateOptions) ([]Mutant, error)

func (Mutant) Location

func (m Mutant) Location() string

func (Mutant) Schemata

func (m Mutant) Schemata() bool

Schemata reports whether this mutant can be selected at run time inside a shared binary. When it cannot, judging it costs a compile of its own.

func (Mutant) Source

func (m Mutant) Source() []byte

Source renders the mutant by splicing its edits into the original bytes.

func (Mutant) Tests

func (m Mutant) Tests() TestPlan

type Outcome

type Outcome string
const (
	// OutcomeKilled means a covering test noticed the change, which is the result
	// the suite is supposed to produce.
	OutcomeKilled Outcome = "killed"
	// OutcomeSurvived means every covering test still passed. A survivor is a gap.
	OutcomeSurvived Outcome = "survived"
	// OutcomeTimeout counts as killed: the mutant changed observable behaviour.
	OutcomeTimeout Outcome = "timeout"
	// OutcomeNotBuilt is a defect in assay, not in the suite under test, so it is
	// excluded from the score and reported separately.
	OutcomeNotBuilt Outcome = "not-built"
	// OutcomeNoCoverage means no indexed test executes the line, so there is
	// nothing to learn. Excluded from the score.
	OutcomeNoCoverage Outcome = "no-coverage"
)

func (Outcome) Killed

func (o Outcome) Killed() bool

func (Outcome) Scored

func (o Outcome) Scored() bool

type Preflight

type Preflight struct {
	Failing map[string][]string
	// Ran lists the tests each package's preflight executed, pass or fail.
	// Every verdict here is evidence about test reliability, and callers that
	// keep a flake journal want the passes as much as the failures.
	Ran      map[string][]string
	Duration time.Duration
}

Preflight runs the tests that mutants will be judged by, against unmutated code, and reports any that already fail.

This is not optional rigour. A covering test that fails for reasons unrelated to a mutant marks that mutant killed, and every such false kill inflates the mutation score. Silently reporting a flattering number is worse than reporting none, so a red suite has to be surfaced before any mutant is judged.

func RunPreflight

func RunPreflight(ctx context.Context, mutants []Mutant, opts ExecuteOptions) (Preflight, error)

func (Preflight) Clean

func (p Preflight) Clean() bool

func (Preflight) Names

func (p Preflight) Names() []string

type Result

type Result struct {
	Mutant   Mutant  `json:"mutant"`
	Outcome  Outcome `json:"outcome"`
	Mode     Mode    `json:"mode,omitempty"`
	KilledBy string  `json:"killedBy,omitempty"`
	Detail   string  `json:"detail,omitempty"`
	// Fallback records why a schemata-capable mutant was judged on a binary of its
	// own. A silent fallback costs a compile per mutant, which is exactly the
	// slowdown the shared binary exists to remove, so the reason must surface.
	Fallback string        `json:"fallback,omitempty"`
	Tests    int           `json:"tests"`
	Duration time.Duration `json:"durationNanos"`
	// Advice is attached to survivors after judging: the covering test closest
	// to the mutated line and the input that would have killed the mutant.
	Advice *Advice `json:"advice,omitempty"`
}

func Execute

func Execute(ctx context.Context, mutants []Mutant, opts ExecuteOptions) ([]Result, error)

Execute judges every mutant, sharing one compile across all the mutants of a package wherever the schemata rewrite is provably type-correct and falling back to a compile per mutant where it is not.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

func NewScope

func NewScope(opts ScopeOptions) (*Scope, error)

func (*Scope) Budget

func (s *Scope) Budget(plan TestPlan) time.Duration

Budget sums the indexed durations of the tests a mutant will run, so its timeout is a multiple of what those tests actually cost rather than a fixed guess.

func (*Scope) Eligible

func (s *Scope) Eligible(absPath string, line int) bool

Eligible restricts mutation to executable statements, and to changed lines when diff-scoped. Coverage is deliberately not consulted here: an uncovered site is still generated so it can be reported as no-coverage, which costs nothing because such mutants are never built.

func (*Scope) PackageOrder

func (s *Scope) PackageOrder(plan TestPlan) []string

PackageOrder orders a plan's covering packages cheapest-first by their indexed durations. A killed mutant exits at the first package that catches it, and a kill found by the cheap package never pays for the expensive one; lexicographic order was paying that cost by accident of naming. Unindexed packages sort last — their cost is unknown, not zero.

func (*Scope) Packages

func (s *Scope) Packages() []string

Packages lists the packages worth mutating: those owning at least one eligible changed file, or every testable package when not diff-scoped.

func (*Scope) TestProfile

func (s *Scope) TestProfile(pkg, test string) (int, time.Duration, bool)

TestProfile resolves a covering test's coverage breadth and indexed duration for survivor advice.

func (*Scope) Tests

func (s *Scope) Tests(absPath string, line int) TestPlan

Tests finds the tests that execute a line, across every package whose tests can reach the mutated one.

Always-run tests are deliberately excluded. Their attribution is unknown by definition, and a test that was already failing when the index was built would mark every mutant killed and silently inflate the score. A survivor therefore means "no test with known coverage of this line killed it", which is the honest claim.

type ScopeOptions

type ScopeOptions struct {
	Graph      *graph.Graph
	Loader     *index.Loader
	BaseCommit string

	// Changes restricts mutation to lines the diff touched. Nil means the whole
	// package, which is the expensive mode and must be asked for explicitly.
	Changes []vcs.Change
}

type Score

type Score struct {
	Killed       int      `json:"killed"`
	Survived     int      `json:"survived"`
	Timeout      int      `json:"timeout"`
	NotBuilt     int      `json:"notBuilt"`
	NoCoverage   int      `json:"noCoverage"`
	Accepted     int      `json:"accepted"`
	NewSurvivors []Result `json:"newSurvivors,omitempty"`
}

func Evaluate

func Evaluate(results []Result, baseline *Baseline) Score

func (Score) MSI

func (s Score) MSI() float64

MSI is the mutation score: the share of mutants a test actually noticed. Mutants that could not be built, or that no test covers, are excluded — the first is an assay defect and the second is already visible as a coverage gap.

func (Score) Scored

func (s Score) Scored() int

type SurvivorGroup

type SurvivorGroup struct {
	Package   string
	Function  string
	File      string
	Survivors []Result
}

SurvivorGroup clusters a report's survivors by the function they mutate.

func GroupSurvivors

func GroupSurvivors(survivors []Result) []SurvivorGroup

GroupSurvivors ranks survivors worst-first: the function with the most survivors is the biggest assertion gap, and scattering its mutants through a flat list hid that. Within a group, source order; groups of equal size in deterministic package/function order.

type TestPlan

type TestPlan map[string][]string

TestPlan is the set of tests that cover the mutated line, grouped by the test package that owns them.

type TestProfile

type TestProfile func(pkg, test string) (lines int, duration time.Duration, ok bool)

TestProfile resolves a covering test's coverage breadth and indexed duration. Unknown tests report ok=false and rank last.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL