mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
3.7 KiB
Python
102 lines
3.7 KiB
Python
"""Run each test in its own fork. PYTEST_DONT_REWRITE"""
|
|
# MIT License (see licenses/MIT-license.txt or https://opensource.org/licenses/MIT)
|
|
# Based on code originally from:
|
|
# https://github.com/pytest-dev/pytest-forked
|
|
# https://github.com/pytest-dev/py
|
|
# TIP: Disable pytest-xdist when debugging internal errors in this plugin.
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import pickle
|
|
import tempfile
|
|
import warnings
|
|
|
|
from pytest import Item, hookimpl
|
|
|
|
try:
|
|
from pytest import TestReport
|
|
except ImportError:
|
|
from _pytest.runner import TestReport # Backwards compatibility with pytest < 7. Remove once Python 2.7 is not supported.
|
|
|
|
from _pytest.runner import runtestprotocol
|
|
|
|
|
|
@hookimpl(tryfirst=True)
|
|
def pytest_runtest_protocol(item, nextitem): # type: (Item, Item | None) -> object | None
|
|
"""Entry point for enabling this plugin."""
|
|
# This is needed because pytest-xdist creates an OS thread (using execnet).
|
|
# See: https://github.com/pytest-dev/execnet/blob/d6aa1a56773c2e887515d63e50b1d08338cb78a7/execnet/gateway_base.py#L51
|
|
warnings.filterwarnings("ignore", "^This process .* is multi-threaded, use of .* may lead to deadlocks in the child.$", DeprecationWarning)
|
|
|
|
item_hook = item.ihook
|
|
item_hook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
|
|
reports = run_item(item, nextitem)
|
|
|
|
for report in reports:
|
|
item_hook.pytest_runtest_logreport(report=report)
|
|
|
|
item_hook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
|
|
|
|
return True
|
|
|
|
|
|
def run_item(item, nextitem): # type: (Item, Item | None) -> list[TestReport]
|
|
"""Run the item in a child process and return a list of reports."""
|
|
with tempfile.NamedTemporaryFile() as temp_file:
|
|
pid = os.fork()
|
|
|
|
if not pid:
|
|
temp_file.delete = False
|
|
run_child(item, nextitem, temp_file.name)
|
|
|
|
return run_parent(item, pid, temp_file.name)
|
|
|
|
|
|
def run_child(item, nextitem, result_path): # type: (Item, Item | None, str) -> None
|
|
"""Run the item, record the result and exit. Called in the child process."""
|
|
with warnings.catch_warnings(record=True) as captured_warnings:
|
|
reports = runtestprotocol(item, nextitem=nextitem, log=False)
|
|
|
|
with open(result_path, "wb") as result_file:
|
|
pickle.dump((reports, captured_warnings), result_file)
|
|
|
|
os._exit(0) # noqa
|
|
|
|
|
|
def run_parent(item, pid, result_path): # type: (Item, int, str) -> list[TestReport]
|
|
"""Wait for the child process to exit and return the test reports. Called in the parent process."""
|
|
exit_code = waitstatus_to_exitcode(os.waitpid(pid, 0)[1])
|
|
|
|
if exit_code:
|
|
reason = "Test CRASHED with exit code {}.".format(exit_code)
|
|
report = TestReport(item.nodeid, item.location, {x: 1 for x in item.keywords}, "failed", reason, "call", user_properties=item.user_properties)
|
|
|
|
if item.get_closest_marker("xfail"):
|
|
report.outcome = "skipped"
|
|
report.wasxfail = reason
|
|
|
|
reports = [report]
|
|
else:
|
|
with open(result_path, "rb") as result_file:
|
|
reports, captured_warnings = pickle.load(result_file) # type: list[TestReport], list[warnings.WarningMessage]
|
|
|
|
for warning in captured_warnings:
|
|
warnings.warn_explicit(warning.message, warning.category, warning.filename, warning.lineno)
|
|
|
|
return reports
|
|
|
|
|
|
def waitstatus_to_exitcode(status): # type: (int) -> int
|
|
"""Convert a wait status to an exit code."""
|
|
# This function was added in Python 3.9.
|
|
# See: https://docs.python.org/3/library/os.html#os.waitstatus_to_exitcode
|
|
|
|
if os.WIFEXITED(status):
|
|
return os.WEXITSTATUS(status)
|
|
|
|
if os.WIFSIGNALED(status):
|
|
return -os.WTERMSIG(status)
|
|
|
|
raise ValueError(status)
|