unimut 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
unimut-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sander Bos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
unimut-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: unimut
3
+ Version: 0.1.0
4
+ Summary: Mutation testing for hand-marked regions of source files
5
+ Author: Sander
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/grug-lang/unimut
8
+ Project-URL: Issues, https://github.com/grug-lang/unimut/issues
9
+ Keywords: mutation testing,testing,c,pycparser
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pycparser>=2.21
20
+ Dynamic: license-file
21
+
22
+ # unimut
23
+
24
+ `unimut` (universal mutator) is a [mutation testing](https://en.wikipedia.org/wiki/Mutation_testing) tool that finds tests you are missing, built to scale across a project's whole lifecycle: a fast, precise gate on individual PRs (`--diff`), an exhaustive nightly audit of legacy code (`--whole-file`), and parallel execution across CI cores (`--jobs`) -- all from one tool, working on any language with a registered backend.
25
+
26
+ `unimut` tries mutations of your code and runs a `--run` command (typically "rebuild, then test") against each one:
27
+
28
+ - A mutant your tests catch (`--run` fails) is **killed** -- the good outcome, and hidden by default.
29
+ - A mutant your tests miss (`--run` still exits 0) **survived** -- a sign you're missing a test -- and gets printed.
30
+
31
+ ```diff
32
+ $ unimut --file src/lj_ffrecord.c --run 'make -j$(nproc) && PATH="$PWD/src:$PATH" perl t/unpack.t'
33
+
34
+ src/lj_ffrecord.c:13
35
+ - if (tref_isnil(tri)) i = 1;
36
+
37
+ src/lj_ffrecord.c:27
38
+ - if (i > e) { rd->nres = 0; return; }
39
+
40
+ Survived: 2/35
41
+ ```
42
+
43
+ `- ` lines (removed code) print red; `+ ` lines (replacement code, for mutation kinds that have one) print green. `unimut` exits `0` if nothing survived, `1` otherwise (or on error) -- usable as a CI gate.
44
+
45
+ ## Installing
46
+
47
+ ```
48
+ pip install unimut
49
+ ```
50
+
51
+ or `pip install -e .` from a checkout for development. Requires Python 3.9+; the bundled C backend pulls in [`pycparser`](https://github.com/eliben/pycparser) automatically.
52
+
53
+ ## Choosing what to mutate
54
+
55
+ | Mode | What it mutates |
56
+ |---|---|
57
+ | Default | Code wrapped in `// unimut on` / `// unimut off` markers |
58
+ | `--diff REF` | Whole file, filtered to lines that differ from `REF` |
59
+ | `--whole-file` | Whole file, exhaustively |
60
+
61
+ ### Default: marker-based
62
+
63
+ ```c
64
+ // unimut on
65
+ static void LJ_FASTCALL recff_unpack(jit_State *J, RecordFFData *rd)
66
+ {
67
+ ...
68
+ }
69
+ // unimut off
70
+ ```
71
+
72
+ Wrap a whole function, or just a run of statements as they'd appear inside one. A file can hold multiple `on`/`off` pairs; they cannot be nested.
73
+
74
+ ### `--diff REF`: a fast PR gate
75
+
76
+ ```
77
+ unimut --file src/api.c --diff main --run 'make -j$(nproc) && make test'
78
+ ```
79
+
80
+ Scans the whole file, but keeps only mutants on a line `git diff REF...HEAD -- src/api.c` reports as changed. A PR only has to prove the lines it touched are covered, not the whole file -- turning an hours-long whole-codebase run into a seconds-long diff-sized one. `REF` is anything `git diff` accepts (`main`, `origin/main`, a SHA); requires `--file` to be inside a git repo with `REF` resolvable.
81
+
82
+ `--diff` implies whole-file scanning, so the marker inversion below applies to it too.
83
+
84
+ ### `--whole-file`: a slow nightly audit
85
+
86
+ ```
87
+ unimut --file src/api.c --whole-file --run 'make -j$(nproc) && make test'
88
+ ```
89
+
90
+ Mutates every statement in the file -- the right mode for periodically auditing legacy code that never got markers. If `// unimut on`/`off` markers *are* still present, their meaning **inverts**: an `off`/`on` pair now marks a range to *exclude*, the same way tools like clang-format reuse on/off markers:
91
+
92
+ ```c
93
+ // unimut off
94
+ die("Out of memory"); // no test can reliably trigger this allocator failure
95
+ // unimut on
96
+ ```
97
+
98
+ Excluded text still has to be part of a file that parses as C overall -- exclusion hides a range from *mutation*, not from parsing.
99
+
100
+ ## Running it
101
+
102
+ ```
103
+ unimut --file <path> --run '<shell command>' [--diff REF | --whole-file] [--jobs N]
104
+ ```
105
+
106
+ `unimut` never mutates your real files. It copies the whole repository (via `git rev-parse --show-toplevel`, or the current directory if that isn't a git checkout) into an isolated temp directory per job, and mutates and builds/tests that copy instead -- your working tree is untouched even if `--run` crashes or you hit Ctrl-C.
107
+
108
+ ### Options
109
+
110
+ | Flag | Meaning |
111
+ |---|---|
112
+ | `--file PATH` | source file to mutate (required) |
113
+ | `--run CMD` | shell command to build/test each mutant (required unless `--print-mutant-counts`) |
114
+ | `--lang {c}` | override language detection from `--file`'s extension |
115
+ | `--diff REF` | PR-gate mode (see above) |
116
+ | `--whole-file` | nightly-audit mode (see above) |
117
+ | `--jobs N` | run N mutants at a time, each in its own isolated copy (default: 1) |
118
+ | `--print-mutant-counts` | print how many mutants would be tried, and exit |
119
+ | `--include-killed-mutants` | also print killed mutants, not just survivors |
120
+
121
+ ### A note on "ignored" compile failures
122
+
123
+ A mutation that fails to compile just makes `--run` fail like any other test failure, so it's counted and treated as killed -- there's no separate "ignored" bucket. That's intentional: a mutant that doesn't compile is indistinguishable from one a test caught, and *should* be, since either way nothing survived.
124
+
125
+ ## The C backend (`mutate_c`)
126
+
127
+ The C backend does one kind of mutation so far: **remove a statement**. It walks every `{ ... }` block and generates one mutant per statement/declaration removed, recursing into nested blocks.
128
+
129
+ It's built on `pycparser`, which has no preprocessor and no idea what your project's types are called. Real code (like the LuaJIT recorder functions this was built for) uses unknown types (`TRef`, `jit_State`) and calling-convention macros (`LJ_FASTCALL`) that won't parse as-is, so `mutate_c.py` does a heuristic pre-pass first: strip comments (preserving line numbers), drop bare ALL_CAPS tokens in front of `name(`, and synthesize fake `typedef int Name;` stand-ins for identifiers that look like unknown types, on top of a small `<stdint.h>`-style preamble.
130
+
131
+ This recovers the *statement structure* of typical C -- enough for statement removal -- but it's not a general C frontend, and treats every unknown type as `int`-sized. A region that genuinely can't be parsed this way raises a clear error rather than silently doing the wrong thing.
132
+
133
+ Because `pycparser`'s generator doesn't preserve formatting, applying a mutant regenerates the whole marked (or whole-file) region through `pycparser`'s `CGenerator`; everything outside it is left byte-for-byte identical.
134
+
135
+ Run its own test suite (hardcoded C strings, no fixture files, compiled with whatever of `cc`/`gcc`/`clang` is on `PATH`) with:
136
+
137
+ ```
138
+ python -m unittest unimut.mutate_c -v
139
+ ```
140
+
141
+ ## Adding another language
142
+
143
+ Language backends are plain modules exposing:
144
+
145
+ ```python
146
+ EXTENSIONS: set[str] # e.g. {".c"}
147
+
148
+ def generate_mutants(file_path: str, source: str) -> list[Mutant]:
149
+ ...
150
+ ```
151
+
152
+ where `Mutant` has `.file`, `.line`, `.original`, `.mutated` (`str | None`), and an `.apply(source: str) -> str` method. To support `--diff`/`--whole-file`, also accept `whole_file: bool = False` and `changed_lines: set[int] | None = None` keyword arguments (see `mutate_c.py`); backends that don't will simply refuse those flags. Register the module in `_LANGUAGES` in `unimut.py`, keyed by the `--lang` value.
153
+
154
+ ## Planned
155
+
156
+ * More C mutation kinds beyond statement removal (operator flips, constant tweaks, condition negation) -- these will populate `Mutant.mutated` and print a `+` line.
157
+ * Additional language backends.
158
+
159
+ ## Contributing
160
+
161
+ This project uses Black and Pyright. Run once to install a pre-commit hook that formats/checks staged files on every `git commit`:
162
+
163
+ ```sh
164
+ pip install pre-commit && pre-commit install
165
+ ```
unimut-0.1.0/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # unimut
2
+
3
+ `unimut` (universal mutator) is a [mutation testing](https://en.wikipedia.org/wiki/Mutation_testing) tool that finds tests you are missing, built to scale across a project's whole lifecycle: a fast, precise gate on individual PRs (`--diff`), an exhaustive nightly audit of legacy code (`--whole-file`), and parallel execution across CI cores (`--jobs`) -- all from one tool, working on any language with a registered backend.
4
+
5
+ `unimut` tries mutations of your code and runs a `--run` command (typically "rebuild, then test") against each one:
6
+
7
+ - A mutant your tests catch (`--run` fails) is **killed** -- the good outcome, and hidden by default.
8
+ - A mutant your tests miss (`--run` still exits 0) **survived** -- a sign you're missing a test -- and gets printed.
9
+
10
+ ```diff
11
+ $ unimut --file src/lj_ffrecord.c --run 'make -j$(nproc) && PATH="$PWD/src:$PATH" perl t/unpack.t'
12
+
13
+ src/lj_ffrecord.c:13
14
+ - if (tref_isnil(tri)) i = 1;
15
+
16
+ src/lj_ffrecord.c:27
17
+ - if (i > e) { rd->nres = 0; return; }
18
+
19
+ Survived: 2/35
20
+ ```
21
+
22
+ `- ` lines (removed code) print red; `+ ` lines (replacement code, for mutation kinds that have one) print green. `unimut` exits `0` if nothing survived, `1` otherwise (or on error) -- usable as a CI gate.
23
+
24
+ ## Installing
25
+
26
+ ```
27
+ pip install unimut
28
+ ```
29
+
30
+ or `pip install -e .` from a checkout for development. Requires Python 3.9+; the bundled C backend pulls in [`pycparser`](https://github.com/eliben/pycparser) automatically.
31
+
32
+ ## Choosing what to mutate
33
+
34
+ | Mode | What it mutates |
35
+ |---|---|
36
+ | Default | Code wrapped in `// unimut on` / `// unimut off` markers |
37
+ | `--diff REF` | Whole file, filtered to lines that differ from `REF` |
38
+ | `--whole-file` | Whole file, exhaustively |
39
+
40
+ ### Default: marker-based
41
+
42
+ ```c
43
+ // unimut on
44
+ static void LJ_FASTCALL recff_unpack(jit_State *J, RecordFFData *rd)
45
+ {
46
+ ...
47
+ }
48
+ // unimut off
49
+ ```
50
+
51
+ Wrap a whole function, or just a run of statements as they'd appear inside one. A file can hold multiple `on`/`off` pairs; they cannot be nested.
52
+
53
+ ### `--diff REF`: a fast PR gate
54
+
55
+ ```
56
+ unimut --file src/api.c --diff main --run 'make -j$(nproc) && make test'
57
+ ```
58
+
59
+ Scans the whole file, but keeps only mutants on a line `git diff REF...HEAD -- src/api.c` reports as changed. A PR only has to prove the lines it touched are covered, not the whole file -- turning an hours-long whole-codebase run into a seconds-long diff-sized one. `REF` is anything `git diff` accepts (`main`, `origin/main`, a SHA); requires `--file` to be inside a git repo with `REF` resolvable.
60
+
61
+ `--diff` implies whole-file scanning, so the marker inversion below applies to it too.
62
+
63
+ ### `--whole-file`: a slow nightly audit
64
+
65
+ ```
66
+ unimut --file src/api.c --whole-file --run 'make -j$(nproc) && make test'
67
+ ```
68
+
69
+ Mutates every statement in the file -- the right mode for periodically auditing legacy code that never got markers. If `// unimut on`/`off` markers *are* still present, their meaning **inverts**: an `off`/`on` pair now marks a range to *exclude*, the same way tools like clang-format reuse on/off markers:
70
+
71
+ ```c
72
+ // unimut off
73
+ die("Out of memory"); // no test can reliably trigger this allocator failure
74
+ // unimut on
75
+ ```
76
+
77
+ Excluded text still has to be part of a file that parses as C overall -- exclusion hides a range from *mutation*, not from parsing.
78
+
79
+ ## Running it
80
+
81
+ ```
82
+ unimut --file <path> --run '<shell command>' [--diff REF | --whole-file] [--jobs N]
83
+ ```
84
+
85
+ `unimut` never mutates your real files. It copies the whole repository (via `git rev-parse --show-toplevel`, or the current directory if that isn't a git checkout) into an isolated temp directory per job, and mutates and builds/tests that copy instead -- your working tree is untouched even if `--run` crashes or you hit Ctrl-C.
86
+
87
+ ### Options
88
+
89
+ | Flag | Meaning |
90
+ |---|---|
91
+ | `--file PATH` | source file to mutate (required) |
92
+ | `--run CMD` | shell command to build/test each mutant (required unless `--print-mutant-counts`) |
93
+ | `--lang {c}` | override language detection from `--file`'s extension |
94
+ | `--diff REF` | PR-gate mode (see above) |
95
+ | `--whole-file` | nightly-audit mode (see above) |
96
+ | `--jobs N` | run N mutants at a time, each in its own isolated copy (default: 1) |
97
+ | `--print-mutant-counts` | print how many mutants would be tried, and exit |
98
+ | `--include-killed-mutants` | also print killed mutants, not just survivors |
99
+
100
+ ### A note on "ignored" compile failures
101
+
102
+ A mutation that fails to compile just makes `--run` fail like any other test failure, so it's counted and treated as killed -- there's no separate "ignored" bucket. That's intentional: a mutant that doesn't compile is indistinguishable from one a test caught, and *should* be, since either way nothing survived.
103
+
104
+ ## The C backend (`mutate_c`)
105
+
106
+ The C backend does one kind of mutation so far: **remove a statement**. It walks every `{ ... }` block and generates one mutant per statement/declaration removed, recursing into nested blocks.
107
+
108
+ It's built on `pycparser`, which has no preprocessor and no idea what your project's types are called. Real code (like the LuaJIT recorder functions this was built for) uses unknown types (`TRef`, `jit_State`) and calling-convention macros (`LJ_FASTCALL`) that won't parse as-is, so `mutate_c.py` does a heuristic pre-pass first: strip comments (preserving line numbers), drop bare ALL_CAPS tokens in front of `name(`, and synthesize fake `typedef int Name;` stand-ins for identifiers that look like unknown types, on top of a small `<stdint.h>`-style preamble.
109
+
110
+ This recovers the *statement structure* of typical C -- enough for statement removal -- but it's not a general C frontend, and treats every unknown type as `int`-sized. A region that genuinely can't be parsed this way raises a clear error rather than silently doing the wrong thing.
111
+
112
+ Because `pycparser`'s generator doesn't preserve formatting, applying a mutant regenerates the whole marked (or whole-file) region through `pycparser`'s `CGenerator`; everything outside it is left byte-for-byte identical.
113
+
114
+ Run its own test suite (hardcoded C strings, no fixture files, compiled with whatever of `cc`/`gcc`/`clang` is on `PATH`) with:
115
+
116
+ ```
117
+ python -m unittest unimut.mutate_c -v
118
+ ```
119
+
120
+ ## Adding another language
121
+
122
+ Language backends are plain modules exposing:
123
+
124
+ ```python
125
+ EXTENSIONS: set[str] # e.g. {".c"}
126
+
127
+ def generate_mutants(file_path: str, source: str) -> list[Mutant]:
128
+ ...
129
+ ```
130
+
131
+ where `Mutant` has `.file`, `.line`, `.original`, `.mutated` (`str | None`), and an `.apply(source: str) -> str` method. To support `--diff`/`--whole-file`, also accept `whole_file: bool = False` and `changed_lines: set[int] | None = None` keyword arguments (see `mutate_c.py`); backends that don't will simply refuse those flags. Register the module in `_LANGUAGES` in `unimut.py`, keyed by the `--lang` value.
132
+
133
+ ## Planned
134
+
135
+ * More C mutation kinds beyond statement removal (operator flips, constant tweaks, condition negation) -- these will populate `Mutant.mutated` and print a `+` line.
136
+ * Additional language backends.
137
+
138
+ ## Contributing
139
+
140
+ This project uses Black and Pyright. Run once to install a pre-commit hook that formats/checks staged files on every `git commit`:
141
+
142
+ ```sh
143
+ pip install pre-commit && pre-commit install
144
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "unimut"
7
+ version = "0.1.0"
8
+ description = "Mutation testing for hand-marked regions of source files"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Sander" },
14
+ ]
15
+ keywords = ["mutation testing", "testing", "c", "pycparser"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Software Development :: Testing",
23
+ ]
24
+ dependencies = [
25
+ "pycparser>=2.21",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/grug-lang/unimut"
30
+ Issues = "https://github.com/grug-lang/unimut/issues"
31
+
32
+ [project.scripts]
33
+ unimut = "unimut.unimut:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
unimut-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """unimut: mutation testing for marked regions of source files."""
2
+
3
+ from .unimut import main
4
+
5
+ __all__ = ["main"]
6
+ __version__ = "0.1.0"