feat(pytest): support pytest-xdist (#105)
This commit is contained in:
committed by
Itai Bohadana
parent
c9b79a42df
commit
7d7cacb91a
+51
-30
@@ -1,18 +1,20 @@
|
|||||||
from io import StringIO
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
|
||||||
import re
|
import re
|
||||||
from typing import Callable, Dict, List, Optional, Union
|
from typing import Callable, Dict, List, Optional, Union
|
||||||
from . import params_getter
|
from . import params_getter
|
||||||
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest.terminal import TerminalReporter
|
|
||||||
from _pytest.fixtures import FixtureLookupErrorRepr
|
from _pytest.fixtures import FixtureLookupErrorRepr
|
||||||
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
from .base import NeotestAdapter, NeotestError, NeotestResult, NeotestResultStatus
|
from .base import NeotestAdapter, NeotestError, NeotestResult, NeotestResultStatus
|
||||||
|
|
||||||
ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||||
|
|
||||||
|
|
||||||
class PytestNeotestAdapter(NeotestAdapter):
|
class PytestNeotestAdapter(NeotestAdapter):
|
||||||
def __init__(self, emit_parameterized_ids: bool):
|
def __init__(self, emit_parameterized_ids: bool):
|
||||||
@@ -72,6 +74,9 @@ class NeotestResultCollector:
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
return buffer.read()
|
return buffer.read()
|
||||||
|
|
||||||
|
def pytest_configure(self, config: "pytest.Config"):
|
||||||
|
self.pytest_config = config
|
||||||
|
|
||||||
def pytest_deselected(self, items: List["pytest.Item"]):
|
def pytest_deselected(self, items: List["pytest.Item"]):
|
||||||
for report in items:
|
for report in items:
|
||||||
file_path, *name_path = report.nodeid.split("::")
|
file_path, *name_path = report.nodeid.split("::")
|
||||||
@@ -89,44 +94,61 @@ class NeotestResultCollector:
|
|||||||
)
|
)
|
||||||
if not params:
|
if not params:
|
||||||
self.stream(pos_id, result)
|
self.stream(pos_id, result)
|
||||||
self.results[pos_id] = result
|
|
||||||
|
|
||||||
def pytest_cmdline_main(self, config: "pytest.Config"):
|
self.results[pos_id] = result
|
||||||
self.pytest_config = config
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_makereport(
|
def pytest_runtest_makereport(
|
||||||
self, item: "pytest.Item", call: "pytest.CallInfo"
|
self, item: "pytest.Item", call: "pytest.CallInfo"
|
||||||
) -> None:
|
) -> Generator:
|
||||||
# pytest generates the report.outcome field in its internal
|
|
||||||
# pytest_runtest_makereport implementation, so call it first. (We don't
|
|
||||||
# implement pytest_runtest_logreport because it doesn't have access to
|
|
||||||
# call.excinfo.)
|
|
||||||
outcome = yield
|
outcome = yield
|
||||||
report = outcome.get_result()
|
report: pytest.TestReport = outcome.get_result()
|
||||||
|
|
||||||
|
if report.when not in {"call", "setup"} or report.outcome != "failed":
|
||||||
|
return
|
||||||
|
|
||||||
|
exc_repr = report.longrepr
|
||||||
|
if not isinstance(exc_repr, ExceptionRepr):
|
||||||
|
return
|
||||||
|
|
||||||
|
file_path, *_ = item.nodeid.split("::")
|
||||||
|
abs_path = str(Path(self.pytest_config.rootdir, file_path))
|
||||||
|
report.error_line = next(
|
||||||
|
(
|
||||||
|
traceback_entry.lineno
|
||||||
|
for traceback_entry in reversed(call.excinfo.traceback)
|
||||||
|
if str(traceback_entry.path) == abs_path
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def pytest_runtest_logreport(self, report: "pytest.TestReport") -> None:
|
||||||
if not (
|
if not (
|
||||||
report.when == "call"
|
report.when == "call"
|
||||||
or (report.when == "setup" and report.outcome in ("skipped", "failed"))
|
or (report.when == "setup" and report.outcome in ("skipped", "failed"))
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
file_path, *name_path = item.nodeid.split("::")
|
file_path, *name_path = report.nodeid.split("::")
|
||||||
abs_path = str(Path(self.pytest_config.rootdir, file_path))
|
abs_path = str(Path(self.pytest_config.rootdir, file_path))
|
||||||
*namespaces, test_name = name_path
|
*namespaces, test_name = name_path
|
||||||
valid_test_name, *params = test_name.split("[") # ]
|
valid_test_name, *params = test_name.split("[") # ]
|
||||||
|
|
||||||
pos_id = "::".join([abs_path, *namespaces, valid_test_name])
|
pos_id = "::".join([abs_path, *namespaces, valid_test_name])
|
||||||
|
|
||||||
errors: List[NeotestError] = []
|
errors: List[NeotestError] = []
|
||||||
short = self._get_short_output(self.pytest_config, report)
|
short = self._get_short_output(self.pytest_config, report)
|
||||||
|
|
||||||
msg_prefix = ""
|
msg_prefix = ""
|
||||||
if getattr(item, "callspec", None) is not None:
|
param_id = None
|
||||||
# Parametrized test
|
if "[" in test_name and test_name.endswith("]"):
|
||||||
|
param_id = test_name[len(valid_test_name) + 1 : -1]
|
||||||
|
if param_id:
|
||||||
if self.emit_parameterized_ids:
|
if self.emit_parameterized_ids:
|
||||||
pos_id += f"[{item.callspec.id}]"
|
pos_id += f"[{param_id}]"
|
||||||
else:
|
else:
|
||||||
msg_prefix = f"[{item.callspec.id}] "
|
msg_prefix = f"[{param_id}] "
|
||||||
|
|
||||||
if report.outcome == "failed":
|
if report.outcome == "failed":
|
||||||
exc_repr = report.longrepr
|
exc_repr = report.longrepr
|
||||||
# Test fails due to condition outside of test e.g. xfail
|
# Test fails due to condition outside of test e.g. xfail
|
||||||
@@ -134,27 +156,27 @@ class NeotestResultCollector:
|
|||||||
errors.append({"message": msg_prefix + exc_repr, "line": None})
|
errors.append({"message": msg_prefix + exc_repr, "line": None})
|
||||||
# Test failed internally
|
# Test failed internally
|
||||||
elif isinstance(exc_repr, ExceptionRepr):
|
elif isinstance(exc_repr, ExceptionRepr):
|
||||||
error_message = ANSI_ESCAPE.sub('', exc_repr.reprcrash.message) # type: ignore
|
# Try to use reprcrash, but ensure the line is 0-based
|
||||||
error_line = None
|
error_message = ANSI_ESCAPE.sub("", exc_repr.reprcrash.message) # type: ignore
|
||||||
for traceback_entry in reversed(call.excinfo.traceback):
|
# error_line = report.error_line
|
||||||
if str(traceback_entry.path) == abs_path:
|
error_line = getattr(report, "error_line", None)
|
||||||
error_line = traceback_entry.lineno
|
|
||||||
errors.append(
|
errors.append(
|
||||||
{"message": msg_prefix + error_message, "line": error_line}
|
{"message": msg_prefix + error_message, "line": error_line}
|
||||||
)
|
)
|
||||||
elif isinstance(exc_repr, FixtureLookupErrorRepr):
|
elif isinstance(exc_repr, FixtureLookupErrorRepr):
|
||||||
|
line0 = getattr(exc_repr, "firstlineno", None)
|
||||||
|
if isinstance(line0, int):
|
||||||
|
line0 = max(0, line0 - 1) # 0-based
|
||||||
errors.append(
|
errors.append(
|
||||||
{
|
{
|
||||||
"message": msg_prefix + exc_repr.errorstring,
|
"message": msg_prefix + exc_repr.errorstring,
|
||||||
"line": exc_repr.firstlineno,
|
"line": line0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# TODO: Figure out how these are returned and how to represent
|
# Preserve compatibility with previous behavior
|
||||||
raise Exception(
|
errors.append({"message": msg_prefix + str(exc_repr), "line": None})
|
||||||
f"Unhandled error type ({type(exc_repr)}), please report to"
|
|
||||||
" neotest-python repo"
|
|
||||||
)
|
|
||||||
result: NeotestResult = self.adapter.update_result(
|
result: NeotestResult = self.adapter.update_result(
|
||||||
self.results.get(pos_id),
|
self.results.get(pos_id),
|
||||||
{
|
{
|
||||||
@@ -199,7 +221,6 @@ class NeotestDebugpyPlugin:
|
|||||||
# Do nothing if not running with a DAP debugger,
|
# Do nothing if not running with a DAP debugger,
|
||||||
# e.g. neotest was invoked with {strategy = dap}
|
# e.g. neotest was invoked with {strategy = dap}
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.current_thread()
|
thread = threading.current_thread()
|
||||||
additional_info = py_db.set_additional_thread_info(thread)
|
additional_info = py_db.set_additional_thread_info(thread)
|
||||||
additional_info.is_tracing += 1
|
additional_info.is_tracing += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user