Start protoyping multi-transport testing
Such that we can run (opting-in) tests on both TCP and UDS backends and ensure the `reg_addr` fixture and various timeouts are adjusted accordingly. Impl deats, - add a new `tpc_proto` CLI option and fixture to allow choosing which "transport protocol" will be used in the test suites (either globally or contextually). - rm `_reg_addr` instead opting for a `_rando_port` which will only be used for `reg_addr`s which are net-tpt-protos. - rejig `reg_addr` fixture to set a ideally session-unique `testrun_reg_addr` based on the `tpt_proto` setting making appropriate calls to `._addr` APIs as needed. - refine `daemon` fixture a bit with typing, `tpt_proto` timings, and stderr capture. - in `test_discovery` do a ton of type-annots, add `debug_mode` fixture opt ins, augment `spawn_and_check_registry()` with `psutil.Process` passing for introspection (when things go wrong..).leslies_extra_appendix
parent
10f9b505ee
commit
dc68ea4118
|
@ -1,6 +1,8 @@
|
||||||
"""
|
"""
|
||||||
``tractor`` testing!!
|
Top level of the testing suites!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
@ -30,7 +32,11 @@ else:
|
||||||
_KILL_SIGNAL = signal.SIGKILL
|
_KILL_SIGNAL = signal.SIGKILL
|
||||||
_INT_SIGNAL = signal.SIGINT
|
_INT_SIGNAL = signal.SIGINT
|
||||||
_INT_RETURN_CODE = 1 if sys.version_info < (3, 8) else -signal.SIGINT.value
|
_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
|
_PROC_SPAWN_WAIT = (
|
||||||
|
0.6
|
||||||
|
if sys.version_info < (3, 7)
|
||||||
|
else 0.4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
no_windows = pytest.mark.skipif(
|
no_windows = pytest.mark.skipif(
|
||||||
|
@ -67,6 +73,15 @@ def pytest_addoption(parser):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.addoption(
|
||||||
|
"--tpt-proto",
|
||||||
|
action="store",
|
||||||
|
dest='tpt_proto',
|
||||||
|
# default='tcp', # TODO, mk this default!
|
||||||
|
default='uds',
|
||||||
|
help="Transport protocol to use under the `tractor.ipc.Channel`",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
backend = config.option.spawn_backend
|
backend = config.option.spawn_backend
|
||||||
|
@ -74,7 +89,7 @@ def pytest_configure(config):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def debug_mode(request):
|
def debug_mode(request) -> bool:
|
||||||
debug_mode: bool = request.config.option.tractor_debug_mode
|
debug_mode: bool = request.config.option.tractor_debug_mode
|
||||||
# if debug_mode:
|
# if debug_mode:
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
|
@ -95,6 +110,15 @@ def spawn_backend(request) -> str:
|
||||||
return request.config.option.spawn_backend
|
return request.config.option.spawn_backend
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def tpt_proto(request) -> str:
|
||||||
|
proto_key: str = request.config.option.tpt_proto
|
||||||
|
# XXX ensure we support the protocol by name
|
||||||
|
addr_type = tractor._addr._address_types[proto_key]
|
||||||
|
assert addr_type.proto_key == proto_key
|
||||||
|
yield proto_key
|
||||||
|
|
||||||
|
|
||||||
# @pytest.fixture(scope='function', autouse=True)
|
# @pytest.fixture(scope='function', autouse=True)
|
||||||
# def debug_enabled(request) -> str:
|
# def debug_enabled(request) -> str:
|
||||||
# from tractor import _state
|
# from tractor import _state
|
||||||
|
@ -119,22 +143,38 @@ def ci_env() -> bool:
|
||||||
# branch?
|
# branch?
|
||||||
#
|
#
|
||||||
# choose randomly at import time
|
# choose randomly at import time
|
||||||
_reg_addr: tuple[str, int] = (
|
_rando_port: str = random.randint(1000, 9999)
|
||||||
'127.0.0.1',
|
|
||||||
random.randint(1000, 9999),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def reg_addr() -> tuple[str, int]:
|
def reg_addr(
|
||||||
|
tpt_proto: str,
|
||||||
|
) -> tuple[str, int]:
|
||||||
|
|
||||||
# globally override the runtime to the per-test-session-dynamic
|
# globally override the runtime to the per-test-session-dynamic
|
||||||
# addr so that all tests never conflict with any other actor
|
# addr so that all tests never conflict with any other actor
|
||||||
# tree using the default.
|
# tree using the default.
|
||||||
from tractor import _root
|
from tractor import (
|
||||||
_root._default_lo_addrs = [_reg_addr]
|
_addr,
|
||||||
|
)
|
||||||
|
tpt_proto: str = _addr.preferred_transport
|
||||||
|
addr_type = _addr._address_types[tpt_proto]
|
||||||
|
def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto]
|
||||||
|
|
||||||
return _reg_addr
|
testrun_reg_addr: tuple[str, int]
|
||||||
|
match tpt_proto:
|
||||||
|
case 'tcp':
|
||||||
|
testrun_reg_addr = (
|
||||||
|
addr_type.def_bindspace,
|
||||||
|
_rando_port,
|
||||||
|
)
|
||||||
|
case 'uds':
|
||||||
|
# NOTE, uniqueness will be based on the pid
|
||||||
|
testrun_reg_addr = addr_type.get_random().unwrap()
|
||||||
|
# testrun_reg_addr = def_reg_addr
|
||||||
|
|
||||||
|
assert def_reg_addr != testrun_reg_addr
|
||||||
|
return testrun_reg_addr
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
|
@ -151,13 +191,25 @@ def pytest_generate_tests(metafunc):
|
||||||
'trio',
|
'trio',
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE: used to be used to dyanmically parametrize tests for when
|
# NOTE: used-to-be-used-to dyanmically parametrize tests for when
|
||||||
# you just passed --spawn-backend=`mp` on the cli, but now we expect
|
# you just passed --spawn-backend=`mp` on the cli, but now we expect
|
||||||
# that cli input to be manually specified, BUT, maybe we'll do
|
# that cli input to be manually specified, BUT, maybe we'll do
|
||||||
# something like this again in the future?
|
# something like this again in the future?
|
||||||
if 'start_method' in metafunc.fixturenames:
|
if 'start_method' in metafunc.fixturenames:
|
||||||
metafunc.parametrize("start_method", [spawn_backend], scope='module')
|
metafunc.parametrize(
|
||||||
|
"start_method",
|
||||||
|
[spawn_backend],
|
||||||
|
scope='module',
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO, is this better then parametrizing the fixture above?
|
||||||
|
# spawn_backend = metafunc.config.option.tpt_backend
|
||||||
|
# if 'tpt_proto' in metafunc.fixturenames:
|
||||||
|
# metafunc.parametrize(
|
||||||
|
# 'tpt_proto',
|
||||||
|
# [spawn_backend],
|
||||||
|
# scope='module',
|
||||||
|
# )
|
||||||
|
|
||||||
# TODO: a way to let test scripts (like from `examples/`)
|
# TODO: a way to let test scripts (like from `examples/`)
|
||||||
# guarantee they won't registry addr collide!
|
# guarantee they won't registry addr collide!
|
||||||
|
@ -171,25 +223,32 @@ def pytest_generate_tests(metafunc):
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
def sig_prog(proc, sig):
|
def sig_prog(
|
||||||
|
proc: subprocess.Popen,
|
||||||
|
sig: int,
|
||||||
|
canc_timeout: float = 0.1,
|
||||||
|
) -> int:
|
||||||
"Kill the actor-process with ``sig``."
|
"Kill the actor-process with ``sig``."
|
||||||
proc.send_signal(sig)
|
proc.send_signal(sig)
|
||||||
time.sleep(0.1)
|
time.sleep(canc_timeout)
|
||||||
if not proc.poll():
|
if not proc.poll():
|
||||||
# TODO: why sometimes does SIGINT not work on teardown?
|
# TODO: why sometimes does SIGINT not work on teardown?
|
||||||
# seems to happen only when trace logging enabled?
|
# seems to happen only when trace logging enabled?
|
||||||
proc.send_signal(_KILL_SIGNAL)
|
proc.send_signal(_KILL_SIGNAL)
|
||||||
ret = proc.wait()
|
ret: int = proc.wait()
|
||||||
assert ret
|
assert ret
|
||||||
|
|
||||||
|
|
||||||
# TODO: factor into @cm and move to `._testing`?
|
# TODO: factor into @cm and move to `._testing`?
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def daemon(
|
def daemon(
|
||||||
|
debug_mode: bool,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
testdir,
|
testdir,
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
):
|
tpt_proto: str,
|
||||||
|
|
||||||
|
) -> subprocess.Popen:
|
||||||
'''
|
'''
|
||||||
Run a daemon root actor as a separate actor-process tree and
|
Run a daemon root actor as a separate actor-process tree and
|
||||||
"remote registrar" for discovery-protocol related tests.
|
"remote registrar" for discovery-protocol related tests.
|
||||||
|
@ -200,28 +259,62 @@ def daemon(
|
||||||
loglevel: str = 'info'
|
loglevel: str = 'info'
|
||||||
|
|
||||||
code: str = (
|
code: str = (
|
||||||
"import tractor; "
|
"import tractor; "
|
||||||
"tractor.run_daemon([], registry_addrs={reg_addrs}, loglevel={ll})"
|
"tractor.run_daemon([], "
|
||||||
|
"registry_addrs={reg_addrs}, "
|
||||||
|
"debug_mode={debug_mode}, "
|
||||||
|
"loglevel={ll})"
|
||||||
).format(
|
).format(
|
||||||
reg_addrs=str([reg_addr]),
|
reg_addrs=str([reg_addr]),
|
||||||
ll="'{}'".format(loglevel) if loglevel else None,
|
ll="'{}'".format(loglevel) if loglevel else None,
|
||||||
|
debug_mode=debug_mode,
|
||||||
)
|
)
|
||||||
cmd: list[str] = [
|
cmd: list[str] = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
'-c', code,
|
'-c', code,
|
||||||
]
|
]
|
||||||
|
# breakpoint()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
# without this, tests hang on windows forever
|
# without this, tests hang on windows forever
|
||||||
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
|
||||||
proc = testdir.popen(
|
proc: subprocess.Popen = testdir.popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
assert not proc.returncode
|
|
||||||
|
# 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':
|
||||||
|
_PROC_SPAWN_WAIT: float = 0.6
|
||||||
time.sleep(_PROC_SPAWN_WAIT)
|
time.sleep(_PROC_SPAWN_WAIT)
|
||||||
|
|
||||||
|
assert not proc.returncode
|
||||||
yield proc
|
yield proc
|
||||||
sig_prog(proc, _INT_SIGNAL)
|
sig_prog(proc, _INT_SIGNAL)
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
)
|
||||||
|
|
||||||
|
# @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
|
||||||
|
|
|
@ -7,7 +7,9 @@ import platform
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
import psutil
|
||||||
import pytest
|
import pytest
|
||||||
|
import subprocess
|
||||||
import tractor
|
import tractor
|
||||||
from tractor._testing import tractor_test
|
from tractor._testing import tractor_test
|
||||||
import trio
|
import trio
|
||||||
|
@ -152,13 +154,23 @@ async def unpack_reg(actor_or_portal):
|
||||||
async def spawn_and_check_registry(
|
async def spawn_and_check_registry(
|
||||||
reg_addr: tuple,
|
reg_addr: tuple,
|
||||||
use_signal: bool,
|
use_signal: bool,
|
||||||
|
debug_mode: bool = False,
|
||||||
remote_arbiter: bool = False,
|
remote_arbiter: bool = False,
|
||||||
with_streaming: bool = False,
|
with_streaming: bool = False,
|
||||||
|
maybe_daemon: tuple[
|
||||||
|
subprocess.Popen,
|
||||||
|
psutil.Process,
|
||||||
|
]|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
if maybe_daemon:
|
||||||
|
popen, proc = maybe_daemon
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
async with tractor.open_root_actor(
|
async with tractor.open_root_actor(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
|
debug_mode=debug_mode,
|
||||||
):
|
):
|
||||||
async with tractor.get_registry(reg_addr) as portal:
|
async with tractor.get_registry(reg_addr) as portal:
|
||||||
# runtime needs to be up to call this
|
# runtime needs to be up to call this
|
||||||
|
@ -176,11 +188,11 @@ async def spawn_and_check_registry(
|
||||||
extra = 2 # local root actor + remote arbiter
|
extra = 2 # local root actor + remote arbiter
|
||||||
|
|
||||||
# ensure current actor is registered
|
# ensure current actor is registered
|
||||||
registry = await get_reg()
|
registry: dict = await get_reg()
|
||||||
assert actor.uid in registry
|
assert actor.uid in registry
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
async with trio.open_nursery(
|
async with trio.open_nursery(
|
||||||
strict_exception_groups=False,
|
strict_exception_groups=False,
|
||||||
) as trion:
|
) as trion:
|
||||||
|
@ -189,17 +201,17 @@ async def spawn_and_check_registry(
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
name = f'a{i}'
|
name = f'a{i}'
|
||||||
if with_streaming:
|
if with_streaming:
|
||||||
portals[name] = await n.start_actor(
|
portals[name] = await an.start_actor(
|
||||||
name=name, enable_modules=[__name__])
|
name=name, enable_modules=[__name__])
|
||||||
|
|
||||||
else: # no streaming
|
else: # no streaming
|
||||||
portals[name] = await n.run_in_actor(
|
portals[name] = await an.run_in_actor(
|
||||||
trio.sleep_forever, name=name)
|
trio.sleep_forever, name=name)
|
||||||
|
|
||||||
# wait on last actor to come up
|
# wait on last actor to come up
|
||||||
async with tractor.wait_for_actor(name):
|
async with tractor.wait_for_actor(name):
|
||||||
registry = await get_reg()
|
registry = await get_reg()
|
||||||
for uid in n._children:
|
for uid in an._children:
|
||||||
assert uid in registry
|
assert uid in registry
|
||||||
|
|
||||||
assert len(portals) + extra == len(registry)
|
assert len(portals) + extra == len(registry)
|
||||||
|
@ -232,6 +244,7 @@ async def spawn_and_check_registry(
|
||||||
@pytest.mark.parametrize('use_signal', [False, True])
|
@pytest.mark.parametrize('use_signal', [False, True])
|
||||||
@pytest.mark.parametrize('with_streaming', [False, True])
|
@pytest.mark.parametrize('with_streaming', [False, True])
|
||||||
def test_subactors_unregister_on_cancel(
|
def test_subactors_unregister_on_cancel(
|
||||||
|
debug_mode: bool,
|
||||||
start_method,
|
start_method,
|
||||||
use_signal,
|
use_signal,
|
||||||
reg_addr,
|
reg_addr,
|
||||||
|
@ -248,6 +261,7 @@ def test_subactors_unregister_on_cancel(
|
||||||
spawn_and_check_registry,
|
spawn_and_check_registry,
|
||||||
reg_addr,
|
reg_addr,
|
||||||
use_signal,
|
use_signal,
|
||||||
|
debug_mode=debug_mode,
|
||||||
remote_arbiter=False,
|
remote_arbiter=False,
|
||||||
with_streaming=with_streaming,
|
with_streaming=with_streaming,
|
||||||
),
|
),
|
||||||
|
@ -257,7 +271,8 @@ def test_subactors_unregister_on_cancel(
|
||||||
@pytest.mark.parametrize('use_signal', [False, True])
|
@pytest.mark.parametrize('use_signal', [False, True])
|
||||||
@pytest.mark.parametrize('with_streaming', [False, True])
|
@pytest.mark.parametrize('with_streaming', [False, True])
|
||||||
def test_subactors_unregister_on_cancel_remote_daemon(
|
def test_subactors_unregister_on_cancel_remote_daemon(
|
||||||
daemon,
|
daemon: subprocess.Popen,
|
||||||
|
debug_mode: bool,
|
||||||
start_method,
|
start_method,
|
||||||
use_signal,
|
use_signal,
|
||||||
reg_addr,
|
reg_addr,
|
||||||
|
@ -273,8 +288,13 @@ def test_subactors_unregister_on_cancel_remote_daemon(
|
||||||
spawn_and_check_registry,
|
spawn_and_check_registry,
|
||||||
reg_addr,
|
reg_addr,
|
||||||
use_signal,
|
use_signal,
|
||||||
|
debug_mode=debug_mode,
|
||||||
remote_arbiter=True,
|
remote_arbiter=True,
|
||||||
with_streaming=with_streaming,
|
with_streaming=with_streaming,
|
||||||
|
maybe_daemon=(
|
||||||
|
daemon,
|
||||||
|
psutil.Process(daemon.pid)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -373,7 +393,7 @@ def test_close_channel_explicit(
|
||||||
|
|
||||||
@pytest.mark.parametrize('use_signal', [False, True])
|
@pytest.mark.parametrize('use_signal', [False, True])
|
||||||
def test_close_channel_explicit_remote_arbiter(
|
def test_close_channel_explicit_remote_arbiter(
|
||||||
daemon,
|
daemon: subprocess.Popen,
|
||||||
start_method,
|
start_method,
|
||||||
use_signal,
|
use_signal,
|
||||||
reg_addr,
|
reg_addr,
|
||||||
|
|
Loading…
Reference in New Issue