{"id":47609001,"url":"https://github.com/fchimpan/mutest","last_synced_at":"2026-04-01T19:51:00.449Z","repository":{"id":344737072,"uuid":"1176017713","full_name":"fchimpan/mutest","owner":"fchimpan","description":"Fast, focused mutation testing for Go — targets comparison and equality operators for high signal-to-noise results","archived":false,"fork":false,"pushed_at":"2026-03-25T07:25:52.000Z","size":181,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-25T15:57:36.139Z","etag":null,"topics":["go","testing"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fchimpan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-08T14:02:19.000Z","updated_at":"2026-03-24T12:18:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fchimpan/mutest","commit_stats":null,"previous_names":["fchimpan/mutest"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/fchimpan/mutest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fchimpan%2Fmutest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fchimpan%2Fmutest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fchimpan%2Fmutest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fchimpan%2Fmutest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fchimpan","download_url":"https://codeload.github.com/fchimpan/mutest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fchimpan%2Fmutest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291246,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","testing"],"created_at":"2026-04-01T19:50:58.388Z","updated_at":"2026-04-01T19:51:00.443Z","avatar_url":"https://github.com/fchimpan.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mutest\n\n[![CI](https://github.com/fchimpan/mutest/actions/workflows/ci.yml/badge.svg)](https://github.com/fchimpan/mutest/actions/workflows/ci.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/fchimpan/mutest)](https://goreportcard.com/report/github.com/fchimpan/mutest)\n[![Go Version](https://img.shields.io/badge/Go-%3E%3D1.24-00ADD8?logo=go)](https://go.dev/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nA mutation testing tool for Go. By default, mutest focuses on boundary-value and equality operators. This keeps the mutant count low and execution fast.\n\n```\n$ mutest ./...\n--- KILLED: calc.go:13:11  \u003e to \u003e= (0.63s)\n--- SURVIVED: calc.go:5:7  \u003e to \u003e= (0.21s)   ← test gap found!\n\nKilled: 1  Survived: 3  Score: 25.0%  Duration: 633ms\n```\n\n---\n\n## What is Mutation Testing?\n\nMutation testing evaluates the quality of your test suite by introducing small changes (**mutations**) to your source code and checking whether your tests detect them.\n\n| Term | Meaning |\n|------|---------|\n| **Mutant** | A copy of your code with a single deliberate change (e.g., `\u003e` replaced with `\u003e=`) |\n| **Killed** | A test failed after the mutation was applied — your tests caught the bug |\n| **Survived** | All tests still passed after the mutation — **your tests have a gap** |\n| **Timeout** | The mutation caused tests to hang or exceed the time limit — treated as detected |\n| **Mutation Score** | `(Killed + Timeout) / (Killed + Timeout + Survived)` — higher is better |\n\nA high mutation score means your tests are good at catching real bugs. Survived mutants point you to exact lines where adding a boundary or equality test would improve coverage.\n\n---\n\n## Why mutest?\n\nMutation testing tools that mutate everything — arithmetic, logic, assignments, returns — generate thousands of mutants and take a long time to run. mutest takes a different approach: **focus on the operators that matter most and run fast.**\n\nRelational Operator Replacement (ROR) — mutating `\u003e`, `\u003e=`, `\u003c`, `\u003c=`, `==`, `!=` — is a well-studied subset of mutation operators known to be effective for fault detection. By limiting scope to ROR, mutest keeps the mutant count small enough to finish in seconds.\n\n---\n\n## Features\n\n- **Fast** — One build per package, all mutants activated at runtime. No per-mutant recompilation\n- **Parallel execution** — Worker pool bounded by CPU cores\n- **`go test`-compatible** — `--- KILLED:` / `--- SURVIVED:` output; `-v`, `-run`, `-timeout` work as expected\n- **`//mutest:skip`** — Exclude functions, blocks (`if`/`for`/`switch`/`select`), or individual lines from mutation\n- **`-diff`** — Only mutate lines changed relative to a git ref (e.g., `-diff origin/main`)\n- **`-threshold`** — CI quality gate (e.g., `-threshold 80` fails if score \u003c 80%)\n- **`-json`** — Machine-readable output for CI pipelines (`-json -v` for NDJSON streaming)\n- **`-dry-run`** — Preview mutations without running tests\n- **Low noise** — Automatically skips equivalent mutants (`len(x) \u003e 0`) and simple error propagation (`if err != nil { return err }`)\n- **Zero dependencies** — Go standard library only\n\n---\n\n## Installation\n\n```bash\ngo install github.com/fchimpan/mutest@latest\n```\n\nPre-built binaries for Linux, macOS, and Windows are available on the [Releases](https://github.com/fchimpan/mutest/releases) page.\n\n---\n\n## Quick Start\n\n```bash\n# Run against all packages (like go test ./...)\nmutest ./...\n\n# Target a specific package\nmutest ./pkg/calc\n\n# Show test output for each mutant (like go test -v)\nmutest -v ./pkg/calc\n\n# Only run tests matching a regex (like go test -run)\nmutest -run TestBoundary ./...\n\n# Tune parallelism and timeout\nmutest -workers 4 -timeout 60s ./...\n\n# Only mutate lines changed vs main (ideal for CI)\nmutest -diff origin/main ./...\n\n# CI quality gate: fail if kill rate is below 80%\nmutest -threshold 80 ./...\n\n# Preview mutations without running tests\nmutest -dry-run ./...\n\n# JSON output for CI pipelines\nmutest -json ./...\n```\n\n---\n\n## Example Output\n\n```\n$ mutest ./...\nmutest: discovered 4 mutation points\nmutest: testing with 10 workers, 30s timeout per mutant\n\n--- SURVIVED: calc.go:5:7  \u003e to \u003e= (0.21s)\n--- SURVIVED: calc.go:21:7  \u003e to \u003e= (0.21s)\n--- SURVIVED: calc.go:18:7  \u003c to \u003c= (0.21s)\n--- KILLED: calc.go:13:11  \u003e to \u003e= (0.63s)\n\n===== Mutation Testing Summary =====\nTotal:     4\nKilled:    1\nSurvived:  3\nScore:     25.0%\nDuration:  633ms\n\nSurvived mutants (test gaps):\n  1. calc.go:5:7  \u003e to \u003e=\n  2. calc.go:18:7  \u003c to \u003c=\n  3. calc.go:21:7  \u003e to \u003e=\n```\n\n### Reading the Results\n\n| Status | What it means |\n|--------|---------------|\n| **KILLED** | Your tests caught the mutation — the boundary is well-tested |\n| **TIMEOUT** | The mutation caused tests to hang — counted as detected |\n| **SURVIVED** | Your tests missed it — **a real test gap you should fix** |\n| **ERROR** | Infrastructure failure (not counted in the score) |\n\n**Mutation Score** = (Killed + Timeout) / (Killed + Timeout + Survived). Higher is better.\n\n\u003e **Note:** Mutations that cause a panic (e.g., index out of bounds) are counted as **KILLED**. Go's exit code does not distinguish panics from test assertion failures, so mutest treats both as detected mutations.\n\n### Fixing a Survived Mutant\n\nWhen mutest reports a survivor like this:\n\n```\n--- SURVIVED: calc.go:5:7  \u003e to \u003e= (0.21s)\n```\n\nIt means mutest swapped the operator and **no test noticed**:\n\n```go\nfunc Max(a, b int) int {\n    if a \u003e b {  // ← mutest changed this to \u003e=, tests still passed\n        return a\n    }\n    return b\n}\n```\n\nThe fix — add a test at the boundary:\n\n```go\nfunc TestMax_EqualValues(t *testing.T) {\n    // This test kills the \u003e → \u003e= mutation because Max(3,3)\n    // returns 3 with \u003e, but would return a (also 3) with \u003e=.\n    // More importantly, it verifies the boundary behavior is intentional.\n    if got := Max(3, 3); got != 3 {\n        t.Errorf(\"Max(3,3) = %d, want 3\", got)\n    }\n}\n```\n\nRe-run mutest and that mutation point will now show `--- KILLED:`.\n\n### Exit Codes\n\n| Code | Meaning |\n|------|---------|\n| `0` | All mutants killed (or kill rate meets `-threshold`) |\n| `1` | Surviving mutants detected (or kill rate below `-threshold`) |\n| `2` | Fatal error (e.g., project parse failure) |\n\n---\n\n## CLI Reference\n\n```\nmutest [flags] [packages]\n```\n\nPositional arguments are package patterns (default: `./...`), following the same conventions as `go test`.\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `-v` | `false` | Show test output for each mutant |\n| `-json` | `false` | Emit results as JSON (NDJSON when combined with `-v`) |\n| `-diff` | | Only mutate lines changed relative to this git ref (e.g., `origin/main`) |\n| `-dry-run` | `false` | Discover mutations without running tests |\n| `-run` | | Regexp to pass to `go test -run` |\n| `-workers` | `NumCPU` | Max parallel test processes |\n| `-timeout` | `30s` | Per-mutant test timeout |\n| `-threshold` | `0` | Minimum kill rate % (0-100); exit 1 if below. 0 = any survived fails |\n| `-skip-err-propagation` | `true` | Skip simple error propagation patterns (`if err != nil { return err }`) |\n| `-version` | | Print version and exit |\n\n### JSON Output\n\nThe `-json` flag produces machine-readable output suitable for CI pipelines and AI agents.\n\n**Summary mode** (`-json`): Emits a single JSON object with all results:\n\n```bash\n$ mutest -json ./...\n{\"total\":4,\"killed\":1,\"timed_out\":0,\"survived\":3,\"errors\":0,\"kill_rate\":25,\"duration\":\"633ms\",\"results\":[...]}\n```\n\n**Streaming mode** (`-json -v`): Emits one NDJSON line per mutant as results arrive, followed by a summary line:\n\n```bash\n$ mutest -json -v ./...\n{\"status\":\"killed\",\"file\":\"calc.go\",\"line\":13,\"column\":11,\"original\":\"\u003e\",\"mutated\":\"\u003e=\",\"desc\":\"\u003e to \u003e=\",\"duration\":\"632ms\"}\n{\"status\":\"survived\",\"file\":\"calc.go\",\"line\":5,\"column\":7,\"original\":\"\u003e\",\"mutated\":\"\u003e=\",\"desc\":\"\u003e to \u003e=\",\"duration\":\"207ms\"}\n...\n{\"total\":4,\"killed\":1,\"timed_out\":0,\"survived\":3,\"errors\":0,\"kill_rate\":25,\"duration\":\"633ms\",\"results\":null}\n```\n\nWhen `-json` is active, informational messages are sent to stderr to keep stdout machine-parseable.\n\n### Diff Mode\n\nThe `-diff` flag restricts mutation testing to lines changed relative to a git ref. This makes mutest practical for CI — instead of setting a threshold over the entire codebase, you enforce that **new and changed code** is well-tested.\n\n```bash\n# Lines changed vs main branch\nmutest -diff main ./...\n\n# Remote main (safer in CI where local main may be stale)\nmutest -diff origin/main ./...\n\n# Last N commits\nmutest -diff HEAD~3 ./...\n\n# Specific commit or tag\nmutest -diff v1.2.0 ./...\n```\n\nInternally, mutest runs `git diff --unified=0 \u003cref\u003e...HEAD` to identify changed lines, then filters mutation points to only those locations. The three-dot syntax diffs from the merge-base, so it correctly captures the PR's actual changes.\n\n```\n$ mutest -diff origin/main ./...\nmutest: diff mode: filtered to 5 of 42 mutation points (changed vs origin/main)\nmutest: discovered 5 mutation points\nmutest: testing with 10 workers, 30s timeout per mutant\n\n--- KILLED: handler.go:25:11  \u003e to \u003e= (0.42s)\n--- KILLED: handler.go:31:7   == to != (0.38s)\n--- SURVIVED: handler.go:44:9  \u003c to \u003c= (0.19s)\n\n===== Mutation Testing Summary =====\nTotal:     5\nKilled:    4\nSurvived:  1\nScore:     80.0%\nDuration:  423ms\n```\n\nCombine with `-threshold 100` to require all changed comparisons to be covered:\n\n```bash\nmutest -diff origin/main -threshold 100 ./...\n```\n\nIf the diff contains no mutation targets (e.g., only comments or non-Go files were changed), mutest exits 0.\n\n### Skip Directive\n\nUse `//mutest:skip` comments to exclude specific functions or lines from mutation testing.\n\n**Function-level skip** — add `//mutest:skip` as a doc comment to skip the entire function:\n\n```go\n//mutest:skip\nfunc legacyCompare(a, b int) bool {\n    return a \u003e b // this mutation will be skipped\n}\n```\n\n**Block-level skip** — add `//mutest:skip` as an inline comment on an `if`, `for`, `switch`, or `select` statement to skip the entire block:\n\n```go\nfunc fetch(url string) (*Response, error) {\n    resp, err := http.Get(url)\n    if err != nil { //mutest:skip\n        if errors.Is(err, context.Canceled) {\n            return nil, ErrCanceled\n        }\n        return nil, fmt.Errorf(\"fetch: %w\", err)\n    }\n    return resp, nil\n}\n```\n\n**Line-level skip** — add `//mutest:skip` as an inline comment on any other line to skip that line only:\n\n```go\nfunc compare(a, b int) int {\n    if a \u003e b { //mutest:skip\n        return 1\n    }\n    if a \u003c b { // this mutation will NOT be skipped\n        return -1\n    }\n    return 0\n}\n```\n\n\u003e **Note:** When `//mutest:skip` is placed on a block statement (`if`/`for`/`switch`/`select`), it skips the entire block including nested statements. On any other line, it skips only that line.\n\n### Dry-Run Mode\n\nThe `-dry-run` flag lists discovered mutation points without executing tests. Useful for previewing scope or counting mutations.\n\n```bash\n$ mutest -dry-run ./...\nmutest: discovered 4 mutation points (dry run)\n\n  1. calc.go:5:7  \u003e to \u003e=\n  2. calc.go:13:11  \u003e to \u003e=\n  3. calc.go:18:7  \u003c to \u003c=\n  4. calc.go:21:7  \u003e to \u003e=\n```\n\nCombine with `-json` for machine-readable output:\n\n```bash\n$ mutest -dry-run -json ./...\n[\n  {\"file\":\"calc.go\",\"package\":\"testproject\",\"line\":5,\"column\":7,\"original\":\"\u003e\",\"mutated\":\"\u003e=\",\"desc\":\"\u003e to \u003e=\"},\n  ...\n]\n```\n\n---\n\n## How It Works\n\n1. **Parse** — `go/parser` builds an AST from every non-test `.go` file\n2. **Discover** — Walk the AST to find `ast.BinaryExpr` with `\u003e`, `\u003e=`, `\u003c`, `\u003c=`, `==`, `!=` (respecting `//mutest:skip`)\n3. **Instrument** — Replace each mutation target with a generic helper function call (e.g., `a \u003e b` → `_mutest_cmp_1(a, b)`) and generate a runtime file that switches behavior based on `MUTEST_ID`\n4. **Build** — Compile one test binary per package with all mutations embedded\n5. **Test** — Run the pre-built binary once per mutation with `MUTEST_ID=N`, in a parallel worker pool\n6. **Judge** — `exit 0` = survived (test gap), `exit != 0` = killed (caught), timeout = detected (hung)\n\nThis **runtime mutation selection** approach compiles each package only once regardless of how many mutations it contains. Traditional per-mutation compilation (`N mutations × compile`) is replaced with `P packages × compile + N mutations × run`, dramatically reducing overhead. **Original source files are never modified.**\n\n### Skipped Mutations\n\nmutest automatically skips mutations that are known to produce false positives:\n\n| Pattern | Example | Why skipped |\n|---------|---------|-------------|\n| `len(x)` compared to `0` | `len(s) \u003e 0` → `len(s) \u003e= 0` | `len()` never returns negative, so the mutation cannot change behavior |\n| `cap(x)` compared to `0` | `cap(s) \u003e 0` → `cap(s) \u003e= 0` | Same as `len()` — `cap()` is always non-negative |\n| Simple error propagation | `if err != nil { return err }` | Go's idiomatic error propagation — mutating these generates noise without meaningful test gaps |\n\nComparisons with non-zero literals (e.g., `len(s) \u003e 1`) are **not** skipped — the boundary between 1 and 2 is meaningful.\n\nComplex error handling is **not** skipped — compound conditions (`err != nil \u0026\u0026 !timedOut`), fallback assignments, and multi-statement bodies represent real logic that should be tested.\n\nTo disable error propagation skipping and mutate all `err != nil` checks:\n\n```bash\nmutest -skip-err-propagation=false ./...\n```\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffchimpan%2Fmutest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffchimpan%2Fmutest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffchimpan%2Fmutest/lists"}