Repository navigation
ci(mutation): mutmut pilot over the stats, gates and cache key #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
432f037
d57ed0e
c5c6715
e54316b
c6f1bfe
91d9ae6
b5785d6
60a99b6
55b32d8
60eb92b
705e015
d27b2e3
9f5e03e
3ea86b2
e854307
b74f999
9b13d65
ad72f2a
a9c43fd
edd5ab2
31e9e2c
6968fb5
5060fea
536a2f8
c4ddf2c
8c765d4
c253fa1
eb605b5
985dd86
4d83c76
2fe56da
7a0dc61
7f7f5e6
0bb8187
b14891f
00e2bf5
51eecbc
1418e6f
3234c2e
4b3147c
1317896
b7d043b
5450d63
601a67f
9357193
91db7ee
25dc6a8
17c78e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # Copyright (C) 2026 Intel Corporation | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| name: mutation | ||
|
|
||
| # Mutation pilot: mutmut over the measurement core (statistics, gate decisions, cache key), where a surviving mutant | ||
| # is a wrong number that ships silently. Weekly and informational: the score goes to the step summary, never a failure. | ||
| on: | ||
| workflow_dispatch: | ||
| schedule: | ||
| - cron: '17 5 * * 1' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| defaults: | ||
| run: | ||
| working-directory: dokimi | ||
|
|
||
| jobs: | ||
| mutmut: | ||
| name: mutmut (pilot modules) | ||
| runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} | ||
| timeout-minutes: 90 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: '3.13' | ||
| # The runtime lock for the tests, and mutmut's own lock (requirements-mutation.txt), | ||
| # which no other workflow installs. | ||
| - name: Install deps | ||
| run: pip install --require-hashes -r requirements.lock.txt -r requirements-mutation.lock.txt | ||
| - name: Run mutmut | ||
| # Crashes are reported below, not failed on. The step timeout sits inside the job's 90 minutes, so the summary | ||
| # step still runs; a job-level timeout would fail the whole job instead. | ||
| timeout-minutes: 75 | ||
| continue-on-error: true | ||
| run: mutmut run --max-children "$(nproc)" | ||
|
antonwolfy marked this conversation as resolved.
Resolved
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping |
||
| - name: Score and survivors | ||
| if: always() | ||
| run: | | ||
| mutmut results --all true > mutmut-results.txt || true | ||
| python - <<'EOF' >> "$GITHUB_STEP_SUMMARY" | ||
| import collections | ||
|
|
||
| counts = collections.defaultdict(collections.Counter) | ||
| survivors = collections.defaultdict(list) | ||
| with open("mutmut-results.txt", encoding="utf-8") as fh: | ||
| for line in fh: | ||
| name, sep, status = line.strip().rpartition(": ") | ||
| if not sep: | ||
| continue | ||
| # <module>.x_<function>__mutmut_<n>, or x<sep><class><sep><method> | ||
| module = name.rsplit(".", 1)[0] | ||
| counts[module][status] += 1 | ||
| if status == "survived": | ||
| survivors[module].append(name) | ||
|
|
||
| print("## Mutation pilot\n") | ||
| if not counts: | ||
| print("No results: see the `Run mutmut` step log.") | ||
| else: | ||
| print("| Module | Killed | Survived | Other | Score |") | ||
| print("|---|---:|---:|---:|---:|") | ||
| for module in sorted(counts): | ||
| c = counts[module] | ||
| killed, survived = c["killed"], c["survived"] | ||
| other = sum(c.values()) - killed - survived | ||
| scored = killed + survived | ||
| score = f"{100 * killed / scored:.1f}%" if scored else "n/a" | ||
| print(f"| `{module}` | {killed} | {survived} | {other} | {score} |") | ||
| unchecked = sum(c["not checked"] for c in counts.values()) | ||
| unscored = sorted(m for m, c in counts.items() if not c["killed"] + c["survived"]) | ||
| if unchecked or unscored: | ||
| parts = [] | ||
| if unchecked: | ||
| parts.append(f"{unchecked} mutants were not checked (a step timeout or a crash cut the run short)") | ||
| if unscored: | ||
| parts.append(f"nothing was scored in {', '.join(f'`{m}`' for m in unscored)}") | ||
| print("\n> **Incomplete run: do not read the scores as final.** " | ||
| + "; ".join(parts) + ". See the `Run mutmut` step log.") | ||
| print("\nOther: no tests, timeout, suspicious or not checked.") | ||
| print("`mutmut show <name>` prints a mutant's diff.\n") | ||
| for module in sorted(survivors): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Has
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not dispatched: |
||
| print(f"<details><summary>{module}: {len(survivors[module])} survivors</summary>\n") | ||
| print("```") | ||
| print("\n".join(survivors[module])) | ||
| print("```\n</details>\n") | ||
| EOF | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| ### Added | ||
|
|
||
| - **Mutation pilot over the measurement core** — `.github/workflows/mutation.yml` | ||
| runs mutmut 3.8.0 (hash-pinned in its own `requirements-mutation.lock.txt`, installed by that workflow alone) weekly and on demand over | ||
| `eval/stats.py`, `eval/binary_stats.py`, the score, regression, critical-band and | ||
| trust gates and `caching.py`, and writes the score per module and the surviving | ||
| mutants to the job summary. It never fails a build. New tests kill the survivors | ||
| that were real gaps (inclusive thresholds, the Wilcoxon veto, per-condition metric | ||
| keys, the bootstrap tail index, the cache key's role and sampling parameters), which | ||
| takes the pilot from 75.3% to 92.0% of mutants killed. Configuration: | ||
| `[tool.mutmut]` in `pyproject.toml`; usage: CONTRIBUTING.md. | ||
|
|
||
| ### Changed | ||
|
|
||
| - **`run --strict` reads the critical bands strictly** — a NaN metric now violates its band; it passed silently before. A band the gate cannot read (an unknown condition, or a `None` metric) exits 2 and names the spec, instead of ending in a traceback. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: dependency caching is not enabled, so each weekly/manual run downloads the same pinned dependency closure again. Enable setup-python's pip cache, following docs.yml, and include both installed lockfiles in cache-dependency-path so changes to either invalidate the cache.
Suggestion:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed it is worth doing. Keeping this PR as is so the approved head stays unchanged; the pip cache (same pattern as docs.yml) goes in a follow-up.