From 39c1d22c41279fcad3dff3b359aed9e5872da53a Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sat, 19 Sep 2026 12:31:07 -0700 Subject: [PATCH] test(formal): compile the M11 mutant after the settings rename and guard mutant compilation The Go API reshape renamed Cache.options to Cache.settings but left the M11 replacement text ("renew the local insertion TTL on every cache hit") referring to c.options.Clock. The mutant no longer compiled, so the full mutation lane reported a measurement error for shard 3 instead of a detection result. The replacement now reads c.settings.clock and the mutant is caught by the whole-millisecond expiry tests. Only the weekly full run compiled mutants; the audit lane checks nothing about the Go catalog. A native test now applies every catalogued edit to a temporary copy of the production sources, requires each anchor to match exactly once, and builds the mutant, so a rename that strands a mutation fails the PR checks rather than the next full formal run. --- formal/go-mutations.json | 2 +- go/mutation_catalog_test.go | 89 +++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 go/mutation_catalog_test.go diff --git a/formal/go-mutations.json b/formal/go-mutations.json index 0c588bd1..d8e507f3 100644 --- a/formal/go-mutations.json +++ b/formal/go-mutations.json @@ -230,7 +230,7 @@ { "path": "go/cache.go", "before": "\tc.local.Get(key)\n\treturn item.value, true", - "after": "\titem.insertedMS = c.options.Clock.ElapsedMS()\n\tc.local.Add(key, item)\n\treturn item.value, true" + "after": "\titem.insertedMS = c.settings.clock.ElapsedMS()\n\tc.local.Add(key, item)\n\treturn item.value, true" } ], "requiredDetections": [ diff --git a/go/mutation_catalog_test.go b/go/mutation_catalog_test.go new file mode 100644 index 00000000..58490cdf --- /dev/null +++ b/go/mutation_catalog_test.go @@ -0,0 +1,89 @@ +package dialcache + +import ( + "encoding/json" + "errors" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +// The Go mutation catalog anchors textual edits in production sources. A +// rename can leave an anchor unmatched or a replacement uncompilable, which +// the full mutation lane reports as a measurement error rather than a +// detection result. This guard catches both at PR time: every edit must match +// exactly once and every mutant must still build. +func TestMutationCatalogMutantsCompile(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "formal", "go-mutations.json")) + if errors.Is(err, fs.ErrNotExist) { + t.Skip("the mutation catalog is not part of the module") + } + if err != nil { + t.Fatal(err) + } + var catalog struct { + Mutations []struct { + ID string + Edits []struct{ Path, Before, After string } + } + } + if err := json.Unmarshal(raw, &catalog); err != nil { + t.Fatal(err) + } + sources, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + var production []string + for _, source := range sources { + if !strings.HasSuffix(source, "_test.go") { + production = append(production, source) + } + } + production = append(production, "go.mod", "go.sum") + goBinary := filepath.Join(runtime.GOROOT(), "bin", "go") + if _, err := os.Stat(goBinary); err != nil { + if goBinary, err = exec.LookPath("go"); err != nil { + t.Skip("no go toolchain to compile mutants with") + } + } + for _, mutation := range catalog.Mutations { + t.Run(mutation.ID, func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + for _, name := range production { + content, err := os.ReadFile(name) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, name), content, 0o644); err != nil { + t.Fatal(err) + } + } + for _, edit := range mutation.Edits { + name := strings.TrimPrefix(edit.Path, "go/") + target := filepath.Join(dir, name) + content, err := os.ReadFile(target) + if err != nil { + t.Fatalf("edit path %s: %v", edit.Path, err) + } + if n := strings.Count(string(content), edit.Before); n != 1 { + t.Fatalf("anchor in %s occurs %d times, want exactly once: %q", edit.Path, n, edit.Before) + } + if err := os.WriteFile(target, []byte(strings.Replace(string(content), edit.Before, edit.After, 1)), 0o644); err != nil { + t.Fatal(err) + } + } + build := exec.Command(goBinary, "build", "./...") + build.Dir = dir + build.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("mutant does not compile: %v\n%s", err, output) + } + }) + } +}