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.
ansible/test/lib/ansible_test/_util/target/pytest/plugins/ansible_forked.py

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)