One function at a time (--run and --mutagens)

You, or an agent, are killing the survivors of one function. Each try means a new test and another run, and a full run tests every mutant in the file and rewrites the project report each time. --run mutates only the functions you name, --mutagens only the mutagens you pick, and the results go to a separate directory, so the project report stays as it was until you merge them.

Where you start

Take the calc package from the quick start, with one more function below InRange in calc.go, and its test in calc_test.go:

// Percent returns part as a whole percentage of total, rounded down.// It expects total > 0.func Percent(part, total int) int {	return part * 100 / total}
func TestPercent(t *testing.T) {	if got := Percent(1, 4); got != 25 {		t.Errorf("Percent(1, 4) = %d, want 25", got)	}	if got := Percent(3, 3); got != 100 {		t.Errorf("Percent(3, 3) = %d, want 100", got)	}}

A full run now tests 23 mutations, 13 in InRange and 10 in Percent:

gooze run ./...
Mutation score: 91.30% (21 killed, 2 survived; 0 cached, 23 tested)

The report shows that both survivors are in InRange, on lines 10 and 13. Percent has none:

 PATH          │ MUTATIONS │ KILLED │ SURVIVED │ NOT COVERED │  SCORE
───────────────┼───────────┼────────┼──────────┼─────────────┼────────
 calc.go       │        23 │     21 │        2 │           0 │ 91.30%
───────────────┼───────────┼────────┼──────────┼─────────────┼────────
 TOTAL FILES 1 │        23 │     21 │        2 │           0 │ 91.30%

Survivors (2):
calc.go:10: survived (comparison): if n < lo { -> if n <= lo {
calc.go:13: survived (comparison): if n > hi { -> if n >= hi {

Mutation score: 91.30% (21 killed, 2 survived)

You want to work on InRange alone, without testing Percent's mutants on every try.

Preview the function

Add the first boundary case to the table in TestInRange:

		{0, 0, 10, true},  // lower bound is inside

Then check what a run limited to InRange would test:

gooze run --estimate --run '^InRange$' ./...
Only functions matching ^InRange$.

 PATH          │ MUTATIONS
───────────────┼───────────
 calc.go       │    13
───────────────┼───────────
 TOTAL FILES 1 │    13

The first line names the selection. The count is 13: the mutations of InRange only. The 10 in Percent are left out.

Run it

gooze run --run '^InRange$' ./...
Mutation score: 92.31% (12 killed, 1 survived; 0 cached, 13 tested; functions matching ^InRange$)
Reports of this run are in .gooze-reports/run; add them to the main reports with gooze report merge.

The score covers only the 13 mutations of InRange, and the end of the line says so (functions matching ^InRange$). The new case killed the survivor on line 10. The next line says where the reports of this run are.

See what still survives

Point gooze report view at the run's directory with -o:

gooze report view -o .gooze-reports/run
 PATH          │ MUTATIONS │ KILLED │ SURVIVED │ NOT COVERED │  SCORE
───────────────┼───────────┼────────┼──────────┼─────────────┼────────
 calc.go       │        13 │     12 │        1 │           0 │ 92.31%
───────────────┼───────────┼────────┼──────────┼─────────────┼────────
 TOTAL FILES 1 │        13 │     12 │        1 │           0 │ 92.31%

Survivors (1):
calc.go:13: survived (comparison): if n > hi { -> if n >= hi {

Mutation score: 92.31% (12 killed, 1 survived)

One survivor is left, on line 13. The project report hasn't changed; without -o, gooze report view still shows the full run:

gooze report view
Mutation score: 91.30% (21 killed, 2 survived)

Write the next test, run again

Add the second boundary case and run the same command:

		{10, 0, 10, true}, // upper bound is inside
gooze run --run '^InRange$' ./...
Mutation score: 100.00% (13 killed, 0 survived; 0 cached, 13 tested; functions matching ^InRange$)
Reports of this run are in .gooze-reports/run; add them to the main reports with gooze report merge.

Every narrowed run empties .gooze-reports/run/ first, so it holds only the latest run. Repeat this loop (write a test, run, read the survivors) until the function has no survivors you can kill.

Add the results to the project report

When you're done, fold the run into the main reports:

gooze report merge
Mutation score: 100.00% (13 killed, 0 survived)

merge removes run/. What it keeps of the main reports depends on whether the file changed since they were written:

  • Unchanged file (same source and test file hashes): the run's results replace only the matching mutations, and the file stays complete.
  • Changed or new file: the run's results replace all of the file's results. If the run tested only part of the file, the file is marked partial, and the next full gooze run re-tests it in full.

Here calc_test.go changed, so calc.go is now partial: the report has the 13 results of InRange, and Percent's 10 are gone until the next full run:

 PATH          │ MUTATIONS │ KILLED │ SURVIVED │ NOT COVERED │   SCORE
───────────────┼───────────┼────────┼──────────┼─────────────┼─────────
 calc.go       │        13 │     13 │        0 │           0 │ 100.00%
───────────────┼───────────┼────────┼──────────┼─────────────┼─────────
 TOTAL FILES 1 │        13 │     13 │        0 │           0 │ 100.00%

No survivors.

Mutation score: 100.00% (13 killed, 0 survived)

The next full run re-tests the whole file and gives the project score:

gooze run ./...
Mutation score: 100.00% (23 killed, 0 survived; 0 cached, 23 tested)

Unchanged files are skipped

A narrowed run uses the main reports as its cache. A file whose source and test file haven't changed since the main reports is skipped, and it isn't in the run's score. Run --run '^InRange$' again right after that full run, and there is nothing to test:

Mutation score: 100.00% (no mutations; functions matching ^InRange$)
Reports of this run are in .gooze-reports/run; add them to the main reports with gooze report merge.

That's why the loop starts with a new test: changing the test file is what puts the function back in the run. To test it without a change, add --no-cache.

How --run matches functions

--run works like go test -run, but on the functions being mutated instead of the tests:

  • The pattern is a Go regular expression and isn't anchored: Range matches InRange. Write ^InRange$ for an exact match.
  • It is matched against the function each mutation is in: its name, and for a method also Type.Name. Pointer, value and generic receivers all count as the type name, so a method Push on Stack[T] matches Push and Stack.Push.
  • Code in a closure belongs to the function that contains it.
  • Package-level code (var and const declarations) matches no pattern, so a --run run never mutates it.
  • Each mutant still runs all the tests of its own package, never the whole module.
  • --run is a flag only; there is no config key or environment variable. It works with --estimate, path arguments, -x/--exclude, --mutagens and --diff.

A pattern that doesn't compile exits with code 2:

Running with 1 worker(s) (shard 0/1)
Error: invalid --run pattern "(": error parsing regexp: missing closing ): `(`

--run with --shard is rejected: a run of a few functions is small enough for one job.

Error: --run can't be combined with --shard: a function-level run is small, so run it in one job

Only some mutagens (--mutagens)

--mutagens generates only the mutagens you list. The names are the nine that run by default (arithmetic, boolean, numbers, comparison, logical, unary, bitwise, increment, assignment) and the three that run only when you name them (branch, statement, loop, see Mutagens you select), in any case, comma-separated or with the flag repeated. Only the arithmetic and numbers mutants of calc, all of them in Percent:

gooze run --estimate --mutagens arithmetic,numbers ./...
Only mutagens arithmetic, numbers.

 PATH          │ MUTATIONS │   CACHE
───────────────┼───────────┼───────────
 calc.go       │    10     │ cached
───────────────┼───────────┼───────────
 TOTAL FILES 1 │    10     │ 10 CACHED

10 of 10 mutations are in cached files and will not be re-tested; use --no-cache to re-test everything.

Nothing changed since the full run, so the 10 mutations are cached. Combine it with --run to narrow both ways, for example --run '^InRange$' --mutagens comparison. An unknown name exits with code 2 and lists the valid ones:

Error: unknown mutagen "conditional"; gooze has arithmetic, boolean, numbers, comparison, logical, unary, bitwise, increment, assignment, branch, statement, loop
exit code 2

Unlike --run, --mutagens also has a config key, run.mutagens (a list, empty by default, which means the nine defaults), and the environment variable GOOZE_RUN_MUTAGENS. A value there narrows every run, so every run writes to run/ and none updates the main reports. Prefer the flag for one-off runs.

The run directory

Every run with --diff, --run or --mutagens is a narrowed run, and they all follow the same rules:

  • Its reports go to a fresh <output>/run/ (by default .gooze-reports/run/), or run/shard_N when sharded. It never changes the main reports.
  • It uses the main reports as its cache: an unchanged file is skipped, --no-cache tests it anyway, and editing its test file makes it re-test.
  • Its score covers only the selection and names it at the end of the line, followed by a line that points to run/ and gooze report merge.
  • gooze report view -o .gooze-reports/run shows its results and survivors.
  • The main reports, the cache, gooze report view without -o, and goozels ignore run/: they don't read subdirectories of the reports directory.
  • gooze report merge folds run/ and run/shard_* into the main reports (see above) and removes run/.

Each report file in run/ records what the run selected in a scope field (see Report format).

When not to use them

  • Don't gate CI on a --run or --mutagens score. It covers only the selection. Gate on a full run, or on a diff run for pull requests.
  • Don't use --mutagens to hide a mutagen whose survivors you don't like. If a survivor is an equivalent mutant that no test can kill, mark it with //gooze:ignore and a reason (see Ignoring code).
  • Don't push a narrowed run's reports as the OCI baseline. Push after a full run, or after gooze report merge.
  • Merge before you read the project score. Until you run gooze report merge, gooze report view shows the main reports without the narrowed run.
  • Don't expect faster tests per mutant. --run selects fewer mutants; each one still runs the whole package suite.

FAQ

How do I run Gooze on one function only?

Run gooze run --run '^InRange$' ./... with the function's name. The pattern is a Go regular expression, like go test -run, matched against the name of the function each mutation is in (Name, and Type.Name for methods). It is not anchored, so write ^Name$ for an exact match.

Does --run make each mutant faster to test?

No. Every mutant still runs all the tests of its own package (never the whole module). --run only generates fewer mutants, so the run is shorter because there is less to test.

Does a --run or --mutagens run change my project reports?

No. It writes its reports to .gooze-reports/run/ and leaves the main reports as they were. gooze report merge adds them to the main reports and removes run/.

Why does a --run run print no mutations?

The file has stored results in the main reports and neither it nor its test file changed, so the run skips it. Change the test file, or pass --no-cache to test the function anyway.

Which names does --mutagens accept?

Every mutagen Gooze has, in any case: the nine that run by default (arithmetic, boolean, numbers, comparison, logical, unary, bitwise, increment, assignment) and the three that run only when named (branch, statement, loop). Separate them with commas or repeat the flag. Any other name exits with code 2.

Can I combine --run with --shard?

No. Gooze exits with --run can't be combined with --shard: a function-level run is small, so run it in one job. --mutagens and --diff work with --shard.

Should CI gate on the score of a --run or --mutagens run?

No. That score covers only the selected functions or mutagens, so it says nothing about the rest of the code. Gate on a full run, or on a diff run for pull requests.