8Expose the in-house mutation engine through Click as ``oct mutate``.
12- Parse the target, the test path, the per-mutant timeout, and the output mode.
13- Enumerate without running anything under ``--list``.
14- Stream progress to stderr so stdout keeps its JSON-only contract.
15- Convert a named refusal into a clear CLI error rather than a stack trace.
21 L2 - command lifecycle
22 L3 - option and output details
27- ``--tests`` is required for a run; there is no default that would silently
28 run a whole suite once per mutant.
29- Exit code is 0 whenever the run completes; mutation outcomes do not set exit
30 codes in v1, which is report-only and gates nothing.
31- Machine-readable stdout contains JSON only.
32- ``--list`` writes nothing to the target and starts no subprocess.
37- oct.core.git (repository discovery for stable mutant ids)
38- oct.mutation.engine (enumeration)
39- oct.mutation.report (rendering)
40- oct.mutation.runner (execution and classification)
41- oct.mutation.sandbox (named refusals)
47from __future__
import annotations
50from pathlib
import Path
54from oct.core
import git
as core_git
61@click.command(name="mutate")
65 type=click.Path(exists=
True, dir_okay=
False, path_type=Path),
66 help=
"Python file to mutate.",
71 type=click.Path(exists=
True, path_type=Path),
72 help=
"Test file or directory to run once per mutant (required for a run).",
76 type=click.IntRange(min=1),
77 default=DEFAULT_TIMEOUT_SECONDS,
79 help=
"Seconds allowed for one test run before the mutant is timed out.",
85 help=
"Emit the full machine-readable report.",
91 help=
"Enumerate the mutants without running any tests.",
100 """Mutate a file and report which mutants the tests fail to notice."""
106 raise click.UsageError(
107 "--tests is required for a run: mutation runs the given tests once "
108 "per mutant, so there is no default that could be safe to assume."
111 report = run_mutants(
115 on_progress=_progress,
117 except MutationRefusal
as exc:
118 raise click.ClickException(str(exc))
from exc
120 click.echo(json.dumps(report.to_dict(), indent=2, sort_keys=
True))
122 click.echo(render_human(report))
126 """Return the repository root that mutant ids are relative to, if any.
128 ``--list`` and a real run must agree on ids, so both anchor on the same
129 thing. A target outside any repository still enumerates: only running
130 requires a repository, because only running rewrites the file.
132 resolved = Path(target).resolve()
133 if not core_git.is_git_repo(resolved.parent):
136 return core_git.get_repo_root(resolved.parent)
137 except core_git.GitError:
141def _emit_list(target: Path, root: Path |
None, json_mode: bool) ->
None:
142 """Write the enumeration for ``--list`` in the requested output mode."""
143 relpath = relative_id(target, root)
145 mutants = enumerate_file(target, root=root)
146 except SyntaxError
as exc:
147 raise click.ClickException(f
"could not parse {relpath}: {exc}")
from exc
151 "totals": {
"mutants": len(mutants)},
152 "mutants": [mutant.to_dict()
for mutant
in mutants],
154 click.echo(json.dumps(payload, indent=2, sort_keys=
True))
156 click.echo(render_list(mutants, relpath))
160 """Write one progress line to stderr, keeping stdout free for the report."""
161 click.echo(message, err=
True)
None _progress(str message)
None _emit_list(Path target, Path|None root, bool json_mode)
Path|None _id_anchor(Path target)
None mutate(Path target, Path|None tests, int timeout, bool json_mode, bool list_mode)