Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/mutmut/type_checking.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def run_type_checker(type_check_command: list[str]) -> list[TypeCheckingError]:

try:
if "mypy" in type_check_command:
report = [json.loads(line) for line in completed_process.stdout.splitlines()]
# mypy prints a lone newline (not an empty string) when it found no errors
report = [json.loads(line) for line in completed_process.stdout.splitlines() if line.strip()]
else:
report = json.loads(completed_process.stdout)
except json.JSONDecodeError:
Expand Down
50 changes: 50 additions & 0 deletions tests/test_type_checking.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json
import subprocess
from pathlib import Path

import pytest
Expand All @@ -7,6 +9,54 @@
from mutmut.type_checking import TypeCheckingError
from mutmut.type_checking import parse_mypy_report
from mutmut.type_checking import parse_pyrefly_report
from mutmut.type_checking import run_type_checker

MYPY_COMMAND = ["mypy", "src", "--output", "json"]


def _fake_completed_process(monkeypatch, stdout: str, returncode: int = 0, stderr: str = "") -> None:
def fake_run(command, **kwargs):
return subprocess.CompletedProcess(command, returncode, stdout=stdout, stderr=stderr)

monkeypatch.setattr("mutmut.type_checking.subprocess.run", fake_run)


def test_run_type_checker_mypy_without_errors(monkeypatch):
# mypy prints a lone newline (and exits with 0) when it found no type errors at all
_fake_completed_process(monkeypatch, stdout="\n", returncode=0)

assert run_type_checker(MYPY_COMMAND) == []


def test_run_type_checker_mypy_with_errors(monkeypatch):
error = {
"file": "src/mod.py",
"line": 2,
"column": 11,
"message": 'Incompatible return value type (got "int", expected "str")',
"hint": None,
"code": "return-value",
"severity": "error",
}
# mypy prints one JSON object per line and exits with 1 when it found type errors
_fake_completed_process(monkeypatch, stdout=f"{json.dumps(error)}\n{json.dumps(error)}\n", returncode=1)

errors = run_type_checker(MYPY_COMMAND)

assert [(e.file_path.name, e.line_number) for e in errors] == [("mod.py", 2), ("mod.py", 2)]


def test_run_type_checker_mypy_still_raises_on_non_json_output(monkeypatch):
# a fatal mypy failure (exit code 2) must stay loud and not be mistaken for "no errors"
_fake_completed_process(
monkeypatch,
stdout="Found 1 error in 1 file (errors prevented further checking)\n",
returncode=2,
stderr="mypy: error: Cannot read file 'src': No such file or directory",
)

with pytest.raises(Exception, match="type check command did not return JSON"):
run_type_checker(MYPY_COMMAND)


def test_mypy_parsing():
Expand Down