mirror of
https://github.com/unclebob/mutator.git
synced 2026-11-06 18:23:50 +00:00
Run a file's mutants at the same time in symlink overlays.
Each worker keeps a private copy of the file under test and links the rest of the project, so the original tree stays put. The default is one worker per core, and --max-workers can set a lower cap.
This commit is contained in:
@@ -7,6 +7,7 @@ alone when no input can tell the operators apart.
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -119,9 +120,11 @@ class _Ok:
|
||||
self.verbose = False
|
||||
self.seconds = seconds
|
||||
self.timeouts = []
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def run(self, command, cwd, timeout):
|
||||
self.timeouts.append(timeout)
|
||||
with self._lock:
|
||||
self.timeouts.append(timeout)
|
||||
code = 0 if timeout is None else 1
|
||||
return CommandResult(code=code, timed_out=False, seconds=self.seconds, output="")
|
||||
|
||||
@@ -369,6 +372,10 @@ def test_options_reject_bad_values_and_keep_flag_defaults(monkeypatch):
|
||||
assert parse_args(["--reuse-coverage"]).use_existing_coverage is True
|
||||
assert parse_args(["--verbose"]).verbose is True
|
||||
assert parse_args(["--changed"]).changed is True
|
||||
assert parse_args(["--max-workers", "2"]).max_workers == 2
|
||||
assert parse_args(["--max-workers", "0"]).exit_code == 1
|
||||
assert parse_args(["--max-workers", "nope"]).exit_code == 1
|
||||
assert parse_args(["--scan", "--max-workers", "2"]).exit_code == 1
|
||||
assert parse_args(["--not-a-flag"]).exit_code == 1
|
||||
monkeypatch.setattr(sys, "argv", ["prog", "--verbose"])
|
||||
options = parse_args(None)
|
||||
@@ -685,9 +692,11 @@ def test_mutate_all_reruns_a_killed_mutant_and_clears_its_backup(tmp_path):
|
||||
def __init__(self):
|
||||
self.verbose = False
|
||||
self.calls = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def run(self, command, cwd, timeout):
|
||||
self.calls += 1
|
||||
with self._lock:
|
||||
self.calls += 1
|
||||
text = (cwd / "src" / "demo.py").read_text(encoding="utf-8")
|
||||
namespace: dict = {}
|
||||
exec(text, namespace)
|
||||
|
||||
+85
-1
@@ -1,16 +1,22 @@
|
||||
import os
|
||||
import threading
|
||||
|
||||
from mutator.engine import mutate_file, restore_backups
|
||||
from mutator.edn import loads
|
||||
from mutator.metrics import snapshot_path
|
||||
from mutator.runner import CommandResult
|
||||
from mutator.workers import create_workers, delete_tree, new_run_dir, worker_count
|
||||
|
||||
|
||||
class FakeRunner:
|
||||
def __init__(self):
|
||||
self.verbose = False
|
||||
self.calls = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def run(self, command, cwd, timeout):
|
||||
self.calls += 1
|
||||
with self._lock:
|
||||
self.calls += 1
|
||||
text = (cwd / "src" / "demo.py").read_text(encoding="utf-8")
|
||||
namespace: dict = {}
|
||||
exec(text, namespace)
|
||||
@@ -133,6 +139,84 @@ def test_a_failed_baseline_does_not_rewrite_metrics(tmp_path):
|
||||
assert not snapshot_path(tmp_path, "demo").exists()
|
||||
|
||||
|
||||
def test_worker_count_follows_sites_cores_and_the_requested_cap():
|
||||
cores = os.cpu_count() or 1
|
||||
assert worker_count(10, None) == min(10, cores)
|
||||
assert worker_count(10, 3) == min(3, cores)
|
||||
assert worker_count(2, 8) == min(2, cores)
|
||||
|
||||
|
||||
def test_workers_keep_a_private_copy_and_link_the_rest(tmp_path):
|
||||
source = tmp_path / "src" / "demo.py"
|
||||
source.parent.mkdir()
|
||||
source.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
|
||||
sibling = tmp_path / "src" / "other.py"
|
||||
sibling.write_text("kept = True\n", encoding="utf-8")
|
||||
marker = tmp_path / "tests" / "keep.txt"
|
||||
marker.parent.mkdir()
|
||||
marker.write_text("keep", encoding="utf-8")
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n", encoding="utf-8")
|
||||
original = source.read_bytes()
|
||||
|
||||
base = new_run_dir(tmp_path)
|
||||
try:
|
||||
workers = create_workers(base, tmp_path, "src/demo.py", original, 2)
|
||||
assert len(workers) == 2
|
||||
for worker in workers:
|
||||
private = worker / "src" / "demo.py"
|
||||
assert private.is_file()
|
||||
assert not private.is_symlink()
|
||||
assert private.read_bytes() == original
|
||||
assert (worker / "src" / "other.py").is_symlink()
|
||||
assert (worker / "tests").is_symlink()
|
||||
assert (worker / "tests" / "keep.txt").read_text(encoding="utf-8") == "keep"
|
||||
assert (worker / "pyproject.toml").is_file()
|
||||
assert not (worker / "pyproject.toml").is_symlink()
|
||||
(workers[0] / "src" / "demo.py").write_text("changed\n", encoding="utf-8")
|
||||
assert workers[1].joinpath("src/demo.py").read_bytes() == original
|
||||
assert source.read_bytes() == original
|
||||
finally:
|
||||
delete_tree(base)
|
||||
|
||||
assert source.read_bytes() == original
|
||||
assert sibling.read_text(encoding="utf-8") == "kept = True\n"
|
||||
assert marker.read_text(encoding="utf-8") == "keep"
|
||||
assert not base.exists()
|
||||
|
||||
|
||||
def test_a_mutation_run_leaves_the_project_tree_unchanged(tmp_path):
|
||||
path = tmp_path / "src" / "demo.py"
|
||||
path.parent.mkdir()
|
||||
path.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
|
||||
original = path.read_text(encoding="utf-8")
|
||||
|
||||
class Record:
|
||||
verbose = False
|
||||
|
||||
def run(self, command, cwd, timeout):
|
||||
if timeout is not None:
|
||||
assert "mutation-workers" in cwd.as_posix()
|
||||
assert (cwd / "src" / "demo.py").read_text(encoding="utf-8") != original
|
||||
return CommandResult(code=0 if timeout is None else 1, timed_out=False, seconds=0.01, output="")
|
||||
|
||||
mutate_file(
|
||||
path,
|
||||
tmp_path,
|
||||
runner=Record(),
|
||||
covered_lines=None,
|
||||
ignore_coverage=True,
|
||||
mutate_all=True,
|
||||
lines=None,
|
||||
test_command="fake",
|
||||
timeout_factor=10,
|
||||
mutation_warning=50,
|
||||
baselines={},
|
||||
max_workers=2,
|
||||
)
|
||||
assert path.read_text(encoding="utf-8") == original
|
||||
assert [item for item in (tmp_path / "target").rglob("*") if item.is_file()] == []
|
||||
|
||||
|
||||
def test_restore_backups_puts_an_interrupted_mutant_back(tmp_path):
|
||||
source = tmp_path / "src" / "demo.py"
|
||||
source.parent.mkdir()
|
||||
|
||||
+13
-1
@@ -1,4 +1,6 @@
|
||||
from mutator.runner import _clojure_command, _python_command, nearest
|
||||
import sys
|
||||
|
||||
from mutator.runner import CommandRunner, _clojure_command, _python_command, nearest
|
||||
|
||||
|
||||
def test_nearest_walks_up_to_the_marker(tmp_path):
|
||||
@@ -66,3 +68,13 @@ def test_python_commands(tmp_path):
|
||||
conf.mkdir()
|
||||
(conf / "conftest.py").write_text("", encoding="utf-8")
|
||||
assert _python_command(conf).endswith("-m pytest")
|
||||
|
||||
|
||||
def test_a_worker_overlay_is_imported_ahead_of_the_environment(tmp_path):
|
||||
worker = tmp_path / "target" / "mutation-workers" / "run-1" / "worker-0"
|
||||
(worker / "src").mkdir(parents=True)
|
||||
(worker / "src" / "demo.py").write_text("VALUE = 'worker'\n", encoding="utf-8")
|
||||
command = f"{sys.executable} -c 'import demo; print(demo.VALUE)'"
|
||||
result = CommandRunner().run(command, worker, 5)
|
||||
assert result.code == 0
|
||||
assert result.output.strip() == "worker"
|
||||
|
||||
Reference in New Issue
Block a user