2018-07-11 23:25:30 +00:00
|
|
|
"""
|
|
|
|
``tractor`` testing!!
|
|
|
|
"""
|
2020-08-03 18:49:46 +00:00
|
|
|
import sys
|
|
|
|
import subprocess
|
2020-07-26 01:20:34 +00:00
|
|
|
import os
|
2018-08-07 18:30:25 +00:00
|
|
|
import random
|
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
|
2022-10-09 17:40:42 +00:00
|
|
|
import inspect
|
|
|
|
from functools import partial, wraps
|
2018-08-07 18:30:25 +00:00
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
import pytest
|
2022-10-09 17:40:42 +00:00
|
|
|
import trio
|
2018-07-11 23:25:30 +00:00
|
|
|
import tractor
|
2020-07-25 16:00:04 +00:00
|
|
|
|
2022-10-09 17:40:42 +00:00
|
|
|
pytest_plugins = ['pytester']
|
2018-07-11 23:25:30 +00:00
|
|
|
|
|
|
|
|
2022-10-09 17:40:42 +00:00
|
|
|
def tractor_test(fn):
|
|
|
|
"""
|
|
|
|
Use:
|
|
|
|
|
|
|
|
@tractor_test
|
|
|
|
async def test_whatever():
|
|
|
|
await ...
|
|
|
|
|
|
|
|
If fixtures:
|
|
|
|
|
|
|
|
- ``arb_addr`` (a socket addr tuple where arbiter is listening)
|
|
|
|
- ``loglevel`` (logging level passed to tractor internals)
|
|
|
|
- ``start_method`` (subprocess spawning backend)
|
|
|
|
|
|
|
|
are defined in the `pytest` fixture space they will be automatically
|
|
|
|
injected to tests declaring these funcargs.
|
|
|
|
"""
|
|
|
|
@wraps(fn)
|
|
|
|
def wrapper(
|
|
|
|
*args,
|
|
|
|
loglevel=None,
|
|
|
|
arb_addr=None,
|
|
|
|
start_method=None,
|
|
|
|
**kwargs
|
|
|
|
):
|
|
|
|
# __tracebackhide__ = True
|
|
|
|
|
|
|
|
if 'arb_addr' in inspect.signature(fn).parameters:
|
|
|
|
# injects test suite fixture value to test as well
|
|
|
|
# as `run()`
|
|
|
|
kwargs['arb_addr'] = arb_addr
|
|
|
|
|
|
|
|
if 'loglevel' in inspect.signature(fn).parameters:
|
|
|
|
# allows test suites to define a 'loglevel' fixture
|
|
|
|
# that activates the internal logging
|
|
|
|
kwargs['loglevel'] = loglevel
|
|
|
|
|
|
|
|
if start_method is None:
|
|
|
|
if platform.system() == "Windows":
|
|
|
|
start_method = 'spawn'
|
|
|
|
else:
|
|
|
|
start_method = 'trio'
|
|
|
|
|
|
|
|
if 'start_method' in inspect.signature(fn).parameters:
|
|
|
|
# set of subprocess spawning backends
|
|
|
|
kwargs['start_method'] = start_method
|
|
|
|
|
|
|
|
if kwargs:
|
|
|
|
|
|
|
|
# use explicit root actor start
|
|
|
|
|
|
|
|
async def _main():
|
|
|
|
async with tractor.open_root_actor(
|
|
|
|
# **kwargs,
|
|
|
|
arbiter_addr=arb_addr,
|
|
|
|
loglevel=loglevel,
|
|
|
|
start_method=start_method,
|
|
|
|
|
|
|
|
# TODO: only enable when pytest is passed --pdb
|
|
|
|
# debug_mode=True,
|
|
|
|
|
|
|
|
) as actor:
|
|
|
|
await fn(*args, **kwargs)
|
|
|
|
|
|
|
|
main = _main
|
|
|
|
|
|
|
|
else:
|
|
|
|
# use implicit root actor start
|
|
|
|
main = partial(fn, *args, **kwargs)
|
|
|
|
|
|
|
|
return trio.run(main)
|
|
|
|
# arbiter_addr=arb_addr,
|
|
|
|
# loglevel=loglevel,
|
|
|
|
# start_method=start_method,
|
|
|
|
# )
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
2018-08-07 18:30:25 +00:00
|
|
|
_arb_addr = '127.0.0.1', random.randint(1000, 9999)
|
|
|
|
|
|
|
|
|
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
|
|
|
|
_PROC_SPAWN_WAIT = 0.6 if sys.version_info < (3, 7) else 0.4
|
|
|
|
|
|
|
|
|
2020-07-25 16:00:04 +00:00
|
|
|
no_windows = pytest.mark.skipif(
|
|
|
|
platform.system() == "Windows",
|
|
|
|
reason="Test is unsupported on windows",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-10-13 18:49:31 +00:00
|
|
|
def repodir():
|
|
|
|
"""Return the abspath to the repo directory.
|
|
|
|
"""
|
|
|
|
dirname = os.path.dirname
|
|
|
|
dirpath = os.path.abspath(
|
|
|
|
dirname(dirname(os.path.realpath(__file__)))
|
|
|
|
)
|
|
|
|
return dirpath
|
|
|
|
|
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
def pytest_addoption(parser):
|
2020-01-27 02:36:08 +00:00
|
|
|
parser.addoption(
|
|
|
|
"--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
|
|
|
)
|
|
|
|
|
|
|
|
parser.addoption(
|
|
|
|
"--spawn-backend", action="store", dest='spawn_backend',
|
2020-07-20 19:18:38 +00:00
|
|
|
default='trio',
|
2020-01-27 02:36:08 +00:00
|
|
|
help="Processing spawning backend to use for test run",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_configure(config):
|
|
|
|
backend = config.option.spawn_backend
|
2020-01-27 03:09:06 +00:00
|
|
|
|
2020-01-27 02:36:08 +00:00
|
|
|
if backend == 'mp':
|
|
|
|
tractor._spawn.try_set_start_method('spawn')
|
2020-07-20 19:18:38 +00:00
|
|
|
elif backend == 'trio':
|
2020-01-27 02:36:08 +00:00
|
|
|
tractor._spawn.try_set_start_method(backend)
|
2018-07-11 23:25:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session', autouse=True)
|
|
|
|
def loglevel(request):
|
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
|
|
|
|
|
|
|
|
2020-07-26 01:20:34 +00:00
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
def spawn_backend(request):
|
|
|
|
return request.config.option.spawn_backend
|
|
|
|
|
|
|
|
|
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:
|
2020-09-02 14:56:39 +00:00
|
|
|
"""Detect CI envoirment.
|
2020-07-26 01:20:34 +00:00
|
|
|
"""
|
2022-07-12 17:49:36 +00:00
|
|
|
return _ci_env
|
2020-07-26 01:20:34 +00:00
|
|
|
|
|
|
|
|
2018-08-07 18:30:25 +00:00
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
def arb_addr():
|
|
|
|
return _arb_addr
|
2019-03-06 05:36:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
def pytest_generate_tests(metafunc):
|
2020-01-27 03:46:48 +00:00
|
|
|
spawn_backend = metafunc.config.option.spawn_backend
|
2020-01-27 04:16:43 +00:00
|
|
|
if not spawn_backend:
|
|
|
|
# XXX some weird windows bug with `pytest`?
|
|
|
|
spawn_backend = 'mp'
|
2020-07-20 19:18:38 +00:00
|
|
|
assert spawn_backend in ('mp', 'trio')
|
2020-01-23 06:16:10 +00:00
|
|
|
|
2020-01-27 02:36:08 +00:00
|
|
|
if 'start_method' in metafunc.fixturenames:
|
|
|
|
if spawn_backend == 'mp':
|
|
|
|
from multiprocessing import get_all_start_methods
|
|
|
|
methods = get_all_start_methods()
|
2020-01-27 03:09:32 +00:00
|
|
|
if 'fork' in methods:
|
|
|
|
# fork not available on windows, so check before
|
|
|
|
# removing XXX: the fork method is in general
|
|
|
|
# incompatible with trio's global scheduler state
|
2020-01-27 02:36:08 +00:00
|
|
|
methods.remove('fork')
|
2020-07-20 19:18:38 +00:00
|
|
|
elif spawn_backend == 'trio':
|
|
|
|
methods = ['trio']
|
2020-01-23 06:16:10 +00:00
|
|
|
|
2019-03-09 01:06:16 +00:00
|
|
|
metafunc.parametrize("start_method", methods, scope='module')
|
2020-08-03 18:49:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
def sig_prog(proc, sig):
|
|
|
|
"Kill the actor-process with ``sig``."
|
|
|
|
proc.send_signal(sig)
|
|
|
|
time.sleep(0.1)
|
|
|
|
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)
|
|
|
|
ret = proc.wait()
|
|
|
|
assert ret
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def daemon(loglevel, testdir, arb_addr):
|
|
|
|
"""Run a daemon actor as a "remote arbiter".
|
|
|
|
"""
|
2020-08-08 19:15:43 +00:00
|
|
|
if loglevel in ('trace', 'debug'):
|
|
|
|
# too much logging will lock up the subproc (smh)
|
|
|
|
loglevel = 'info'
|
|
|
|
|
2020-08-03 18:49:46 +00:00
|
|
|
cmdargs = [
|
|
|
|
sys.executable, '-c',
|
2020-08-13 15:53:45 +00:00
|
|
|
"import tractor; tractor.run_daemon([], arbiter_addr={}, loglevel={})"
|
2020-08-03 18:49:46 +00:00
|
|
|
.format(
|
|
|
|
arb_addr,
|
|
|
|
"'{}'".format(loglevel) if loglevel else None)
|
|
|
|
]
|
|
|
|
kwargs = dict()
|
|
|
|
if platform.system() == 'Windows':
|
|
|
|
# without this, tests hang on windows forever
|
|
|
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
|
|
|
|
|
|
proc = testdir.popen(
|
|
|
|
cmdargs,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
assert not proc.returncode
|
|
|
|
time.sleep(_PROC_SPAWN_WAIT)
|
|
|
|
yield proc
|
|
|
|
sig_prog(proc, _INT_SIGNAL)
|