2018-07-11 23:25:30 +00:00
|
|
|
"""
|
2025-04-03 02:40:28 +00:00
|
|
|
Top level of the testing suites!
|
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
"""
|
2025-04-03 02:40:28 +00:00
|
|
|
from __future__ import annotations
|
2020-08-03 18:49:46 +00:00
|
|
|
import sys
|
|
|
|
import subprocess
|
2020-07-26 01:20:34 +00:00
|
|
|
import os
|
2020-08-03 18:49:46 +00:00
|
|
|
import signal
|
2020-01-23 06:16:10 +00:00
|
|
|
import platform
|
2020-08-03 18:49:46 +00:00
|
|
|
import time
|
2018-08-07 18:30:25 +00:00
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
import pytest
|
2024-03-12 19:48:20 +00:00
|
|
|
from tractor._testing import (
|
|
|
|
examples_dir as examples_dir,
|
|
|
|
tractor_test as tractor_test,
|
|
|
|
expect_ctxc as expect_ctxc,
|
|
|
|
)
|
2020-07-25 16:00:04 +00:00
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
pytest_plugins: list[str] = [
|
|
|
|
'pytester',
|
|
|
|
'tractor._testing.pytest',
|
|
|
|
]
|
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
|
2020-08-03 18:49:46 +00:00
|
|
|
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
|
|
|
|
if platform.system() == 'Windows':
|
|
|
|
_KILL_SIGNAL = signal.CTRL_BREAK_EVENT
|
|
|
|
_INT_SIGNAL = signal.CTRL_C_EVENT
|
|
|
|
_INT_RETURN_CODE = 3221225786
|
|
|
|
_PROC_SPAWN_WAIT = 2
|
|
|
|
else:
|
|
|
|
_KILL_SIGNAL = signal.SIGKILL
|
|
|
|
_INT_SIGNAL = signal.SIGINT
|
|
|
|
_INT_RETURN_CODE = 1 if sys.version_info < (3, 8) else -signal.SIGINT.value
|
2025-04-03 02:40:28 +00:00
|
|
|
_PROC_SPAWN_WAIT = (
|
|
|
|
0.6
|
|
|
|
if sys.version_info < (3, 7)
|
|
|
|
else 0.4
|
|
|
|
)
|
2020-08-03 18:49:46 +00:00
|
|
|
|
|
|
|
|
2020-07-25 16:00:04 +00:00
|
|
|
no_windows = pytest.mark.skipif(
|
|
|
|
platform.system() == "Windows",
|
|
|
|
reason="Test is unsupported on windows",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2025-04-04 04:05:55 +00:00
|
|
|
def pytest_addoption(
|
|
|
|
parser: pytest.Parser,
|
|
|
|
):
|
2025-04-17 15:20:49 +00:00
|
|
|
# ?TODO? should this be exposed from our `._testing.pytest`
|
|
|
|
# plugin or should we make it more explicit with `--tl` for
|
|
|
|
# tractor logging like we do in other client projects?
|
2020-01-27 02:36:08 +00:00
|
|
|
parser.addoption(
|
2024-02-20 13:53:37 +00:00
|
|
|
"--ll",
|
|
|
|
action="store",
|
|
|
|
dest='loglevel',
|
2021-11-22 18:27:47 +00:00
|
|
|
default='ERROR', help="logging level to set when testing"
|
2020-01-27 02:36:08 +00:00
|
|
|
)
|
|
|
|
|
2024-02-20 13:53:37 +00:00
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
@pytest.fixture(scope='session', autouse=True)
|
|
|
|
def loglevel(request):
|
2025-04-17 15:20:49 +00:00
|
|
|
import tractor
|
2018-07-14 20:09:05 +00:00
|
|
|
orig = tractor.log._default_loglevel
|
|
|
|
level = tractor.log._default_loglevel = request.config.option.loglevel
|
2021-12-02 13:12:02 +00:00
|
|
|
tractor.log.get_console_log(level)
|
2018-07-11 23:25:30 +00:00
|
|
|
yield level
|
2018-07-14 20:09:05 +00:00
|
|
|
tractor.log._default_loglevel = orig
|
2018-08-07 18:30:25 +00:00
|
|
|
|
|
|
|
|
2022-07-12 17:49:36 +00:00
|
|
|
_ci_env: bool = os.environ.get('CI', False)
|
|
|
|
|
|
|
|
|
2020-07-26 01:20:34 +00:00
|
|
|
@pytest.fixture(scope='session')
|
2020-09-03 12:44:24 +00:00
|
|
|
def ci_env() -> bool:
|
2024-03-12 19:48:20 +00:00
|
|
|
'''
|
2025-04-04 04:05:55 +00:00
|
|
|
Detect CI environment.
|
2024-03-12 19:48:20 +00:00
|
|
|
|
|
|
|
'''
|
2022-07-12 17:49:36 +00:00
|
|
|
return _ci_env
|
2020-07-26 01:20:34 +00:00
|
|
|
|
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
def sig_prog(
|
|
|
|
proc: subprocess.Popen,
|
|
|
|
sig: int,
|
|
|
|
canc_timeout: float = 0.1,
|
|
|
|
) -> int:
|
2020-08-03 18:49:46 +00:00
|
|
|
"Kill the actor-process with ``sig``."
|
|
|
|
proc.send_signal(sig)
|
2025-04-03 02:40:28 +00:00
|
|
|
time.sleep(canc_timeout)
|
2020-08-03 18:49:46 +00:00
|
|
|
if not proc.poll():
|
|
|
|
# TODO: why sometimes does SIGINT not work on teardown?
|
|
|
|
# seems to happen only when trace logging enabled?
|
|
|
|
proc.send_signal(_KILL_SIGNAL)
|
2025-04-03 02:40:28 +00:00
|
|
|
ret: int = proc.wait()
|
2020-08-03 18:49:46 +00:00
|
|
|
assert ret
|
|
|
|
|
|
|
|
|
2024-03-12 19:48:20 +00:00
|
|
|
# TODO: factor into @cm and move to `._testing`?
|
2020-08-03 18:49:46 +00:00
|
|
|
@pytest.fixture
|
2023-01-26 17:43:32 +00:00
|
|
|
def daemon(
|
2025-04-03 02:40:28 +00:00
|
|
|
debug_mode: bool,
|
2023-01-26 17:43:32 +00:00
|
|
|
loglevel: str,
|
|
|
|
testdir,
|
2023-10-18 19:35:35 +00:00
|
|
|
reg_addr: tuple[str, int],
|
2025-04-03 02:40:28 +00:00
|
|
|
tpt_proto: str,
|
|
|
|
|
|
|
|
) -> subprocess.Popen:
|
2023-01-26 17:43:32 +00:00
|
|
|
'''
|
2023-10-18 19:35:35 +00:00
|
|
|
Run a daemon root actor as a separate actor-process tree and
|
|
|
|
"remote registrar" for discovery-protocol related tests.
|
2023-01-26 17:43:32 +00:00
|
|
|
|
|
|
|
'''
|
2020-08-08 19:15:43 +00:00
|
|
|
if loglevel in ('trace', 'debug'):
|
2023-10-18 19:35:35 +00:00
|
|
|
# XXX: too much logging will lock up the subproc (smh)
|
|
|
|
loglevel: str = 'info'
|
|
|
|
|
|
|
|
code: str = (
|
2025-04-03 02:40:28 +00:00
|
|
|
"import tractor; "
|
|
|
|
"tractor.run_daemon([], "
|
|
|
|
"registry_addrs={reg_addrs}, "
|
|
|
|
"debug_mode={debug_mode}, "
|
|
|
|
"loglevel={ll})"
|
2023-10-18 19:35:35 +00:00
|
|
|
).format(
|
|
|
|
reg_addrs=str([reg_addr]),
|
|
|
|
ll="'{}'".format(loglevel) if loglevel else None,
|
2025-04-03 02:40:28 +00:00
|
|
|
debug_mode=debug_mode,
|
2023-10-18 19:35:35 +00:00
|
|
|
)
|
|
|
|
cmd: list[str] = [
|
|
|
|
sys.executable,
|
|
|
|
'-c', code,
|
2020-08-03 18:49:46 +00:00
|
|
|
]
|
2025-04-03 02:40:28 +00:00
|
|
|
# breakpoint()
|
2023-10-18 19:35:35 +00:00
|
|
|
kwargs = {}
|
2020-08-03 18:49:46 +00:00
|
|
|
if platform.system() == 'Windows':
|
|
|
|
# without this, tests hang on windows forever
|
|
|
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
proc: subprocess.Popen = testdir.popen(
|
2023-10-18 19:35:35 +00:00
|
|
|
cmd,
|
2020-08-03 18:49:46 +00:00
|
|
|
**kwargs,
|
|
|
|
)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
# UDS sockets are **really** fast to bind()/listen()/connect()
|
|
|
|
# so it's often required that we delay a bit more starting
|
|
|
|
# the first actor-tree..
|
|
|
|
if tpt_proto == 'uds':
|
2025-04-05 21:52:16 +00:00
|
|
|
global _PROC_SPAWN_WAIT
|
|
|
|
_PROC_SPAWN_WAIT = 0.6
|
|
|
|
|
2020-08-03 18:49:46 +00:00
|
|
|
time.sleep(_PROC_SPAWN_WAIT)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
assert not proc.returncode
|
2020-08-03 18:49:46 +00:00
|
|
|
yield proc
|
|
|
|
sig_prog(proc, _INT_SIGNAL)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
# XXX! yeah.. just be reaaal careful with this bc sometimes it
|
|
|
|
# can lock up on the `_io.BufferedReader` and hang..
|
|
|
|
stderr: str = proc.stderr.read().decode()
|
|
|
|
if stderr:
|
|
|
|
print(
|
|
|
|
f'Daemon actor tree produced STDERR:\n'
|
|
|
|
f'{proc.args}\n'
|
|
|
|
f'\n'
|
|
|
|
f'{stderr}\n'
|
|
|
|
)
|
|
|
|
if proc.returncode != -2:
|
|
|
|
raise RuntimeError(
|
|
|
|
'Daemon actor tree failed !?\n'
|
|
|
|
f'{proc.args}\n'
|
|
|
|
)
|
|
|
|
|
2025-04-04 04:05:55 +00:00
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
# @pytest.fixture(autouse=True)
|
|
|
|
# def shared_last_failed(pytestconfig):
|
|
|
|
# val = pytestconfig.cache.get("example/value", None)
|
|
|
|
# breakpoint()
|
|
|
|
# if val is None:
|
|
|
|
# pytestconfig.cache.set("example/value", val)
|
|
|
|
# return val
|
2025-04-04 04:05:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
# TODO: a way to let test scripts (like from `examples/`)
|
|
|
|
# guarantee they won't `registry_addrs` collide!
|
|
|
|
# -[ ] maybe use some kinda standard `def main()` arg-spec that
|
|
|
|
# we can introspect from a fixture that is called from the test
|
|
|
|
# body?
|
|
|
|
# -[ ] test and figure out typing for below prototype! Bp
|
|
|
|
#
|
|
|
|
# @pytest.fixture
|
|
|
|
# def set_script_runtime_args(
|
|
|
|
# reg_addr: tuple,
|
|
|
|
# ) -> Callable[[...], None]:
|
|
|
|
|
|
|
|
# def import_n_partial_in_args_n_triorun(
|
|
|
|
# script: Path, # under examples?
|
|
|
|
# **runtime_args,
|
|
|
|
# ) -> Callable[[], Any]: # a `partial`-ed equiv of `trio.run()`
|
|
|
|
|
|
|
|
# # NOTE, below is taken from
|
|
|
|
# # `.test_advanced_faults.test_ipc_channel_break_during_stream`
|
|
|
|
# mod: ModuleType = import_path(
|
|
|
|
# examples_dir() / 'advanced_faults'
|
|
|
|
# / 'ipc_failure_during_stream.py',
|
|
|
|
# root=examples_dir(),
|
|
|
|
# consider_namespace_packages=False,
|
|
|
|
# )
|
|
|
|
# return partial(
|
|
|
|
# trio.run,
|
|
|
|
# partial(
|
|
|
|
# mod.main,
|
|
|
|
# **runtime_args,
|
|
|
|
# )
|
|
|
|
# )
|
|
|
|
# return import_n_partial_in_args_n_triorun
|