nvim-neotest / neotest-python

MIT License
115 stars 34 forks source link

Line-level diagnostics not supported for `--tb` other than `long`, `short` #28

Closed OddBloke closed 1 year ago

OddBloke commented 1 year ago

From discussion in #27: this is because pytest returns a different internal representation of the traceback based on the --tb setting.

OddBloke commented 1 year ago

The below does set the tbstyle to long unconditionally, overriding a pytest.ini setting, and ensuring I get line-level diagnostics. This does mean, however, that the output displayed by neotest-python will be different to the CLI output if users do have a --tb setting, which is less than ideal.

diff --git a/neotest_python/pytest.py b/neotest_python/pytest.py
index a6010d7..741edfa 100644
--- a/neotest_python/pytest.py
+++ b/neotest_python/pytest.py
@@ -81,6 +81,7 @@ class NeotestResultCollector:
             self.results[pos_id] = result

     def pytest_cmdline_main(self, config: "Config"):
+        config.option.tbstyle = "long"
         self.pytest_config = config

     def pytest_runtest_logreport(self, report: "TestReport"):
OddBloke commented 1 year ago

An alternative approach, which adds code to handle native tracebacks:

--- a/neotest_python/pytest.py
+++ b/neotest_python/pytest.py
@@ -1,3 +1,4 @@
+import re
 from io import StringIO
 from pathlib import Path
 from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union
@@ -9,6 +10,18 @@ if TYPE_CHECKING:
     from _pytest.reports import TestReport

+def _lineno_of_uppermost_frame_in_file(path: str, lines: List[str]) -> Optional[int]:
+    # lines should be ReprEntry.lines or NativeReprEntry.lines, this will find
+    # the top frame in the traceback within the given path and return its line
+    error_line = None
+    for line in reversed(lines):
+        # File "/path/to/file.py", line 2
+        match = re.search(r'File "(?P<path>.*)", line (?P<line>\d+)', line)
+        if match is not None and match.group("path") == path:
+            error_line = int(match.group("line")) - 1
+    return error_line
+
+
 class PytestNeotestAdapter(NeotestAdapter):
     def run(
         self,
@@ -116,6 +129,11 @@ class NeotestResultCollector:
                         and repr.reprfileloc.path == file_path
                     ):
                         error_line = repr.reprfileloc.lineno - 1
+                    if error_line is None:
+                        # Fall back to parsing traceback lines
+                        error_line = _lineno_of_uppermost_frame_in_file(
+                            abs_path, repr.lines
+                        )
                 errors.append({"message": error_message, "line": error_line})
             else:
                 # TODO: Figure out how these are returned and how to represent