{"id":47611047,"url":"https://github.com/unclebob/clj-mutate","last_synced_at":"2026-04-01T20:11:52.385Z","repository":{"id":340428017,"uuid":"1165994736","full_name":"unclebob/clj-mutate","owner":"unclebob","description":"A mutation tester for clojure, specific to speclj but easy to modify.","archived":false,"fork":false,"pushed_at":"2026-03-12T18:44:26.000Z","size":101,"stargazers_count":13,"open_issues_count":2,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-12T20:39:30.142Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","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/unclebob.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-02-24T19:09:52.000Z","updated_at":"2026-03-12T20:25:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/unclebob/clj-mutate","commit_stats":null,"previous_names":["unclebob/clj-mutate"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/unclebob/clj-mutate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Fclj-mutate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Fclj-mutate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Fclj-mutate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Fclj-mutate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/unclebob","download_url":"https://codeload.github.com/unclebob/clj-mutate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Fclj-mutate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291409,"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":[],"created_at":"2026-04-01T20:11:51.782Z","updated_at":"2026-04-01T20:11:52.379Z","avatar_url":"https://github.com/unclebob.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# clj-mutate\n\nMutation testing for Clojure. Discovers mutation sites, applies each one, runs your specs, reports killed/survived.\n\n## Setup\n\nAdd a `:mutate` alias to your project's `deps.edn`:\n\n```clojure\n:mutate {:main-opts [\"-m\" \"clj-mutate.core\"]\n         :extra-deps {clj-mutate/clj-mutate {:local/root \"/path/to/clj-mutate\"}\n                      org.clojure/tools.reader {:mvn/version \"1.4.2\"}}\n         :extra-paths [\"spec\"]}\n```\n\nRequires [Speclj](https://github.com/slagyr/speclj) as your test runner.\n\n## Usage\n\n```bash\n# Analyze spec structure and SCRAP scores\nclj -M:scrap spec\n\n# Mutate-test a source file.\n# If the file already has a footer manifest, this defaults to changed top-level forms only.\nclj -M:mutate src/myapp/foo.cljc\n\n# Scan a file for mutation counts without running coverage or specs\nclj -M:mutate src/myapp/foo.cljc --scan\n\n# Rewrite the embedded manifest without running coverage or mutations\nclj -M:mutate src/myapp/foo.cljc --update-manifest\n\n# Retest only specific lines (e.g. survivors from a previous run)\nclj -M:mutate src/myapp/foo.cljc --lines 45,67,89\n\n# Force differential mutation even if you want to be explicit\nclj -M:mutate src/myapp/foo.cljc --since-last-run\n\n# Override the default differential behavior and mutate all covered sites\nclj -M:mutate src/myapp/foo.cljc --mutate-all\n\n# Reuse existing LCOV data without refreshing coverage\nclj -M:mutate src/myapp/foo.cljc --reuse-lcov\n\n# Warn when a module exceeds a mutation-count threshold\nclj -M:mutate src/myapp/foo.cljc --mutation-warning 75\n\n# Limit parallel worker count\nclj -M:mutate src/myapp/foo.cljc --max-workers 4\n\n# Use a custom infinite-loop timeout multiplier (baseline factor)\nclj -M:mutate src/myapp/foo.cljc --timeout-factor 15\n\n# Use a custom test command (quote commands containing spaces)\nclj -M:mutate src/myapp/foo.cljc --test-command \"clj -M:spec --tag ~slow\"\n\n# Show command usage help\nclj -M:mutate --help\n```\n\nThe tool automatically:\n- Runs a baseline test (`clj -M:spec --tag ~no-mutate`) to verify all included specs pass unmodified\n- Applies each mutation, runs all specs with a timeout (`--timeout-factor`, default 10x baseline)\n- Restores the original file after each mutation\n- Writes an embedded footer manifest with the last test date and top-level form hashes\n- Updates that embedded manifest after successful differential runs as well as full runs\n- Defaults to differential mutation when that footer manifest is already present\n- Prints a warning when mutation count exceeds `--mutation-warning` (default `50`)\n- Excludes specs tagged `:no-mutate` by default so mutation workers do not recursively launch nested mutation runs\n- Can reuse existing LCOV data with `--reuse-lcov`\n\n`--scan` is the fast structural mode. It skips coverage, skips test execution, and reports:\n- total mutation sites\n- changed mutation sites relative to the embedded manifest\n- the standard mutation-count warning\n\n`--update-manifest` rewrites the embedded footer manifest for the file's current contents without running coverage, baseline specs, or mutation workers.\n\n## Recommended Workflow\n\nRun mutation testing one file at a time.\n\nBefore running mutation work, run SCRAP on your specs:\n\n```bash\nclj -M:scrap spec\nclj -M:spec\n```\n\n`clj -M:scrap` includes the structural checks that were previously handled by `speclj-structure-check`, and also reports SCRAP scores for the worst examples in each spec file. The alias pulls SCRAP from [`github.com/unclebob/scrap`](https://github.com/unclebob/scrap).\n\nIf you have specs that should never run from inside mutation workers, tag them `:no-mutate`. `clj-mutate` excludes those by default with `clj -M:spec --tag ~no-mutate`. You can override that behavior with `--test-command`.\n\nAfter specs pass, run `--scan` on the files you changed:\n\n```bash\nclj -M:mutate src/myapp/foo.cljc --scan\n```\n\nIf a changed file reports more than `50` mutation sites, consider splitting it before doing full mutation work.\n\nThen mutate exactly one source file with `--max-workers 3`:\n\n```bash\nclj -M:mutate src/myapp/foo.cljc --max-workers 3\n```\n\nWorkflow rules:\n- Mutate only one file at a time.\n- Before moving to the next file, cover every uncovered mutation in the current file.\n- Before moving to the next file, kill every surviving mutation in the current file.\n- `clj-mutate` uses LCOV coverage data and regenerates it when stale or missing.\n- In a batch of mutation runs, let the first run generate coverage, then consider `--reuse-lcov` for the remaining files if you accept stale-coverage risk.\n\nRecommended loop for each file:\n1. Run `clj -M:mutate path/to/file.clj --max-workers 3`.\n2. If any mutations are uncovered, add or fix specs until they are covered.\n3. If any mutations survive, change code or specs until they are killed.\n4. Rerun the same single-file mutation command.\n5. Only start the next file when the current file has no uncovered mutations and no survivors.\n\nFor local incremental work, once a file has a footer manifest the default run is differential. You can still be explicit:\n\n```bash\nclj -M:mutate src/myapp/foo.cljc --since-last-run\n```\n\nBefore baseline and worker execution, a mutation run prints:\n- total mutation sites\n- covered mutation sites\n- uncovered mutation sites\n- changed mutation sites\n- whether a manifest exists\n- whether the module hash changed\n- differential surface area\n- manifest-violating surface area\n\nTo force a full rerun on a file with a manifest:\n\n```bash\nclj -M:mutate src/myapp/foo.cljc --mutate-all\n```\n\nBefore a push or major release, consider running `--mutate-all` on the files you changed to verify the full file instead of relying only on differential mutation.\n\nThe footer manifest is embedded at the end of the source file and records:\n- the last successful mutation test date\n- each top-level form's id\n- its line span\n- a hash of its normalized form\n\nDifferential mutation runs update the footer manifest on success, so the next differential run compares against the latest successful mutation baseline.\n\n## Mutation Rules\n\n| Category | Mutations |\n|----------|-----------|\n| Arithmetic | `+` ↔ `-`, `*` → `/`, `inc` ↔ `dec` |\n| Comparison | `\u003e` ↔ `\u003e=`, `\u003c` ↔ `\u003c=` |\n| Equality | `=` ↔ `not=` |\n| Boolean | `true` ↔ `false` |\n| Conditional | `if` ↔ `if-not`, `when` ↔ `when-not` |\n| Constant | `0` ↔ `1` |\n\nKnown-equivalent mutations (e.g. comparisons on `(rand)`, constants inside `rand-nth` pools) are auto-suppressed.\n\n## Coverage Integration\n\nIf a `:cov` alias is configured with [Cloverage](https://github.com/cloverage/cloverage) and `--lcov` output, the tool reads `target/coverage/lcov.info` to skip mutations on uncovered lines.\n\nCoverage freshness is checked automatically:\n- If `target/coverage/lcov.info` is missing, `clj-mutate` regenerates it with `clj -M:cov --lcov`.\n- If LCOV is older than current source/spec inputs, `clj-mutate` regenerates it with `clj -M:cov --lcov`.\n- The run prints a diagnostic message when regeneration is triggered.\n- If a mutation site sits on a `recur` argument line or a nested loop-state update expression, LCOV may emit no `DA` entry for that line. In that case `clj-mutate` classifies the site as uncovered even when behavior-level tests exercise the path.\n\nWith `--reuse-lcov`:\n- `clj-mutate` uses the existing `target/coverage/lcov.info` as-is\n- stale coverage is allowed\n- the run prints a warning that covered/uncovered classification may be inaccurate\n- the run prints whether the LCOV file exists, its last modified time when present, and whether the target source is newer than the LCOV file\n- if `target/coverage/lcov.info` is missing, the run prints a clear error and exits with status `1`\n\n```clojure\n:cov {:main-opts [\"-m\" \"speclj.cloverage\" \"--\" \"-p\" \"src\" \"-s\" \"spec\" \"--lcov\"]\n      :extra-deps {cloverage/cloverage {:mvn/version \"1.2.4\"}\n                   speclj/speclj {:mvn/version \"3.10.0\"}}\n      :extra-paths [\"spec\"]}\n```\n\n## Parallel Worker Isolation\n\nParallel mutation runs now use a unique worker root per run:\n\n`target/mutation-workers/run-\u003cuuid\u003e/worker-N`\n\nThis avoids collisions when two mutation runs overlap or when a prior run exits unexpectedly.\n\n## Claude Code Skill\n\nThis repo includes a [Claude Code skill](skills/using-clj-mutate/SKILL.md) for AI-assisted mutation testing. Add it to your project's `.claude/settings.json`:\n\n```json\n{\n  \"skills\": [\"github.com/unclebob/clj-mutate\"]\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funclebob%2Fclj-mutate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Funclebob%2Fclj-mutate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funclebob%2Fclj-mutate/lists"}