Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions multiplex/execute/pytest_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from util.rewrite_method import rewrite_method


def _execute(project_root):
def _execute(project_root, output_path=None, approach=None, label=None):
"""Run the project's tests with pytest.

Returns True if pytest exits 0 (all tests pass) — i.e. the mutant is not
Expand All @@ -31,14 +31,24 @@ def _execute(project_root):
command = [sys.executable, "-m", "pytest", "-q", str(project_root)]
try:
result = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, check=False
)
except FileNotFoundError as exc:
raise SystemExit(
"pytest is not installed for the current interpreter "
f"({sys.executable}). Install pytest to use the 'pytest' runtool."
) from exc

if output_path is not None and approach and label:
test_dir = Path(output_path, approach + "-test")
test_dir.mkdir(parents=True, exist_ok=True)
safe = Path(str(label).replace(os.sep, "_")).stem
(test_dir / f"{safe}_test.txt").write_text(
f"$ {' '.join(command)}\n# exit code: {result.returncode}\n\n"
f"{result.stdout}"
)

return result.returncode == 0


Expand All @@ -59,7 +69,7 @@ def run_mutants(

mutants = [["MUTANT", "EQUIVALENCE", "COMPILABLE", "SURVIVES"]]

if not _execute(project_root):
if not _execute(project_root, output_path, approach, "ORIGINAL"):
raise IOError(
"The original (unmutated) project did not pass pytest, so mutants "
"cannot be evaluated against it. Run it manually to see why: "
Expand All @@ -83,7 +93,7 @@ def run_mutants(

mutant_survives = False
if mutant_compiles:
mutant_survives = _execute(project_root)
mutant_survives = _execute(project_root, output_path, approach, mutant_file)
mutant_output.append(str(mutant_survives))

mutants.append(mutant_output)
Expand Down
39 changes: 36 additions & 3 deletions tests/execute/test_pytest_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ def raise_fnf(*a, **k):
assert "pytest" in str(exc.value).lower()


def _run_reporting(returncode, stdout):
def run(command, *args, **kwargs):
return types.SimpleNamespace(returncode=returncode, stdout=stdout)

return run


def test_execute_writes_test_output_file_when_labelled(tmp_path, monkeypatch):
# Given an output path, approach and label, _execute persists the run's
# captured output to <approach>-test/<label without extension>_test.txt.
monkeypatch.setattr(
pytest_runner.subprocess, "run", _run_reporting(1, "1 failed, 2 passed\n")
)

assert pytest_runner._execute("proj", tmp_path, "basic", "mutant_killed.py") is False

# Same naming as the Defects4J runner: the extension is not carried over.
out_file = tmp_path / "basic-test" / "mutant_killed_test.txt"
assert out_file.exists()
body = out_file.read_text()
assert "1 failed, 2 passed" in body
assert "# exit code: 1" in body


def test_execute_writes_no_output_file_without_a_label(tmp_path, monkeypatch):
monkeypatch.setattr(pytest_runner.subprocess, "run", _run_reporting(0, "2 passed\n"))

assert pytest_runner._execute("proj") is True
assert list(tmp_path.iterdir()) == []


# --------------------------------------------------------------------------- #
# run_mutants: full evaluation loop over a mutants directory #
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -127,7 +158,9 @@ def test_run_mutants_writes_summary_with_correct_classification(project, monkeyp
# Stand in for `pytest`: the suite fails (mutant killed) only when the
# injected source returns n + 1; every other state passes.
monkeypatch.setattr(
pytest_runner, "_execute", lambda project_root: "return n + 1" not in Path(src).read_text()
pytest_runner,
"_execute",
lambda project_root, *args, **kwargs: "return n + 1" not in Path(src).read_text(),
)

_run(project)
Expand All @@ -142,7 +175,7 @@ def test_run_mutants_writes_summary_with_correct_classification(project, monkeyp


def test_run_mutants_raises_when_baseline_fails(project, monkeypatch):
monkeypatch.setattr(pytest_runner, "_execute", lambda project_root: False)
monkeypatch.setattr(pytest_runner, "_execute", lambda project_root, *args, **kwargs: False)
with pytest.raises(IOError):
_run(project)

Expand All @@ -155,7 +188,7 @@ def test_run_mutants_skips_tests_for_noncompilable_mutant(project, monkeypatch):

calls = {"n": 0}

def counting_execute(project_root):
def counting_execute(project_root, *args, **kwargs):
calls["n"] += 1
return True

Expand Down
Loading