diff --git a/src/pytest_ty/plugin.py b/src/pytest_ty/plugin.py index c8354d1..0c9f336 100644 --- a/src/pytest_ty/plugin.py +++ b/src/pytest_ty/plugin.py @@ -1,4 +1,6 @@ import functools +import itertools +import json import subprocess import typing @@ -46,13 +48,13 @@ def _run_ty_once(config: pytest.Config) -> dict[str, list[str]]: if (results := config.stash.get(_TY_RESULTS_STASH_KEY, None)) is not None: return results - command = [_ty_bin(), "check", "--output-format=concise"] + command = [_ty_bin(), "check", "--output-format=gitlab"] results = {} try: subprocess.run(command, check=True, timeout=60, capture_output=True, cwd=config.rootpath) # noqa: S603 except subprocess.CalledProcessError as e: - stdout = e.stdout.decode(errors="replace") if e.stdout else "" + stdout = e.stdout.decode(errors="replace") if e.stdout else "[]" stderr = e.stderr.decode(errors="replace") if e.stderr else "" results = _parse_ty_output(stdout) if not results: @@ -74,15 +76,21 @@ def _run_ty_once(config: pytest.Config) -> dict[str, list[str]]: def _parse_ty_output(output: str) -> dict[str, list[str]]: - results: dict[str, list[str]] = {} + try: + diagnostics = json.loads(output) + except json.JSONDecodeError: + return {} - for line in output.split("\n"): - line = line.strip() # noqa: PLW2901 # loop variable cleanup - parts = line.rsplit(":", 3) - if len(parts) < 4: # noqa: PLR2004 # format is `file_name.py:line:pos:error_message + results: dict[str, list[str]] = {} + for diag in diagnostics: + path = diag.get("location", {}).get("path", "") + if not path: continue - file_path = parts[0] - results.setdefault(file_path, []).append(line) + line = diag["location"]["positions"]["begin"]["line"] + column = diag["location"]["positions"]["begin"]["column"] + description = diag.get("description", "") + message = f"{path}:{line}:{column}: {description}" + results.setdefault(path, []).append(message) return results @@ -130,4 +138,4 @@ def runtest(self) -> None: if _TY_FAILURE_MARKER in results: raise TyError("\n".join(results[_TY_FAILURE_MARKER])) - raise TyError("\n".join(results.keys())) + raise TyError("\n".join(itertools.chain.from_iterable(results.values()))) diff --git a/tests/test_pytest_ty.py b/tests/test_pytest_ty.py index 4cc5812..41c96c8 100644 --- a/tests/test_pytest_ty.py +++ b/tests/test_pytest_ty.py @@ -14,6 +14,16 @@ def test_failure() -> None: ) +@pytest.fixture +def another_failing_test(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + test_another_failing_file=""" + def test_another_failure() -> None: + another_value: int = "2" + """ + ) + + @pytest.fixture def passing_test(pytester: pytest.Pytester) -> None: pytester.makepyfile( @@ -188,3 +198,35 @@ def test_timeout_handling_failing_check(pytester: pytest.Pytester) -> None: result.stdout.fnmatch_lines(["*::ty PASSED*"]) result.stdout.fnmatch_lines(["*::ty::status FAILED*"]) assert result.ret == 1 + + +@pytest.mark.usefixtures("failing_test", "another_failing_test", "passing_test") +def test_status_item_shows_all_failures_with_verbose(pytester: pytest.Pytester) -> None: + result = pytester.runpytest("--ty", "-v") + + result.stdout.fnmatch_lines("*test_passing_file.py::ty PASSED*") + result.stdout.fnmatch_lines(["*test_failing_file.py::ty FAILED*"]) + result.stdout.fnmatch_lines(["*test_another_failing_file.py::ty FAILED*"]) + result.stdout.fnmatch_lines(["*::ty::status FAILED*"]) + result.stdout.fnmatch_lines(["*:2:18:*invalid-assignment*"]) + assert result.ret == 1 + + +def test_type_ignore_comment_parsing(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + test_colon_error=""" + from typing import TypedDict + + class MyDict(TypedDict): + name: str + + def test_case() -> None: + d: MyDict = {"name": "test", "extra": 1} + """ + ) + + result = pytester.runpytest("--ty", "-v") + + result.stdout.fnmatch_lines(["*test_colon_error.py::ty FAILED*"]) + result.stdout.fnmatch_lines(["*::ty::status FAILED*"]) + assert result.ret == 1