diff --git a/tests/conftest.py b/tests/conftest.py index a0b815dc..ca175a05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,21 +6,22 @@ from __future__ import annotations import sys import subprocess import os -import random import signal import platform import time import pytest -import tractor from tractor._testing import ( examples_dir as examples_dir, tractor_test as tractor_test, expect_ctxc as expect_ctxc, ) -# TODO: include wtv plugin(s) we build in `._testing.pytest`? -pytest_plugins = ['pytester'] +pytest_plugins: list[str] = [ + 'pytester', + 'tractor._testing.pytest', +] + # Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives if platform.system() == 'Windows': @@ -48,6 +49,9 @@ no_windows = pytest.mark.skipif( def pytest_addoption( parser: pytest.Parser, ): + # ?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? parser.addoption( "--ll", action="store", @@ -55,54 +59,10 @@ def pytest_addoption( default='ERROR', help="logging level to set when testing" ) - parser.addoption( - "--spawn-backend", - action="store", - dest='spawn_backend', - default='trio', - help="Processing spawning backend to use for test run", - ) - - parser.addoption( - "--tpdb", - "--debug-mode", - action="store_true", - dest='tractor_debug_mode', - # default=False, - help=( - 'Enable a flag that can be used by tests to to set the ' - '`debug_mode: bool` for engaging the internal ' - 'multi-proc debugger sys.' - ), - ) - - # provide which IPC transport protocols opting-in test suites - # should accumulatively run against. - parser.addoption( - "--tpt-proto", - nargs='+', # accumulate-multiple-args - action="store", - dest='tpt_protos', - default=['tcp'], - help="Transport protocol to use under the `tractor.ipc.Channel`", - ) - - -def pytest_configure(config): - backend = config.option.spawn_backend - tractor._spawn.try_set_start_method(backend) - - -@pytest.fixture(scope='session') -def debug_mode(request) -> bool: - debug_mode: bool = request.config.option.tractor_debug_mode - # if debug_mode: - # breakpoint() - return debug_mode - @pytest.fixture(scope='session', autouse=True) def loglevel(request): + import tractor orig = tractor.log._default_loglevel level = tractor.log._default_loglevel = request.config.option.loglevel tractor.log.get_console_log(level) @@ -110,49 +70,6 @@ def loglevel(request): tractor.log._default_loglevel = orig -@pytest.fixture(scope='session') -def spawn_backend(request) -> str: - return request.config.option.spawn_backend - - -@pytest.fixture(scope='session') -def tpt_protos(request) -> list[str]: - - # allow quoting on CLI - proto_keys: list[str] = [ - proto_key.replace('"', '').replace("'", "") - for proto_key in request.config.option.tpt_protos - ] - - # ?TODO, eventually support multiple protos per test-sesh? - if len(proto_keys) > 1: - pytest.fail( - 'We only support one `--tpt-proto ` atm!\n' - ) - - # XXX ensure we support the protocol by name via lookup! - for proto_key in proto_keys: - addr_type = tractor._addr._address_types[proto_key] - assert addr_type.proto_key == proto_key - - yield proto_keys - - -@pytest.fixture( - scope='session', - autouse=True, -) -def tpt_proto( - tpt_protos: list[str], -) -> str: - proto_key: str = tpt_protos[0] - from tractor import _state - if _state._def_tpt_proto != proto_key: - _state._def_tpt_proto = proto_key - # breakpoint() - yield proto_key - - _ci_env: bool = os.environ.get('CI', False) @@ -165,80 +82,6 @@ def ci_env() -> bool: return _ci_env -# TODO: also move this to `._testing` for now? -# -[ ] possibly generalize and re-use for multi-tree spawning -# along with the new stuff for multi-addrs? -# -# choose random port at import time -_rando_port: str = random.randint(1000, 9999) - - -@pytest.fixture(scope='session') -def reg_addr( - tpt_proto: str, -) -> tuple[str, int|str]: - - # globally override the runtime to the per-test-session-dynamic - # addr so that all tests never conflict with any other actor - # tree using the default. - from tractor import ( - _addr, - ) - addr_type = _addr._address_types[tpt_proto] - def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto] - - testrun_reg_addr: tuple[str, int] - match tpt_proto: - case 'tcp': - testrun_reg_addr = ( - addr_type.def_bindspace, - _rando_port, - ) - - # NOTE, file-name uniqueness (no-collisions) will be based on - # the runtime-directory and root (pytest-proc's) pid. - case 'uds': - testrun_reg_addr = addr_type.get_random().unwrap() - - assert def_reg_addr != testrun_reg_addr - return testrun_reg_addr - - -def pytest_generate_tests(metafunc): - spawn_backend: str = metafunc.config.option.spawn_backend - - if not spawn_backend: - # XXX some weird windows bug with `pytest`? - spawn_backend = 'trio' - - # TODO: maybe just use the literal `._spawn.SpawnMethodKey`? - assert spawn_backend in ( - 'mp_spawn', - 'mp_forkserver', - 'trio', - ) - - # NOTE: used-to-be-used-to dyanmically parametrize tests for when - # 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 - # something like this again in the future? - if 'start_method' in metafunc.fixturenames: - metafunc.parametrize( - "start_method", - [spawn_backend], - scope='module', - ) - - # TODO, parametrize any `tpt_proto: str` declaring tests! - # proto_tpts: list[str] = metafunc.config.option.proto_tpts - # if 'tpt_proto' in metafunc.fixturenames: - # metafunc.parametrize( - # 'tpt_proto', - # proto_tpts, # TODO, double check this list usage! - # scope='module', - # ) - - def sig_prog( proc: subprocess.Popen, sig: int, diff --git a/tractor/_testing/addr.py b/tractor/_testing/addr.py new file mode 100644 index 00000000..1b066336 --- /dev/null +++ b/tractor/_testing/addr.py @@ -0,0 +1,70 @@ +# tractor: structured concurrent "actors". +# Copyright 2018-eternity Tyler Goodlet. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +''' +Random IPC addr generation for isolating +the discovery space between test sessions. + +Might be eventually useful to expose as a util set from +our `tractor.discovery` subsys? + +''' +import random +from typing import ( + Type, +) +from tractor import ( + _addr, +) + + +def get_rando_addr( + tpt_proto: str, + *, + + # choose random port at import time + _rando_port: str = random.randint(1000, 9999) + +) -> tuple[str, str|int]: + ''' + Used to globally override the runtime to the + per-test-session-dynamic addr so that all tests never conflict + with any other actor tree using the default. + + ''' + addr_type: Type[_addr.Addres] = _addr._address_types[tpt_proto] + def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto] + + # this is the "unwrapped" form expected to be passed to + # `.open_root_actor()` by test body. + testrun_reg_addr: tuple[str, int|str] + match tpt_proto: + case 'tcp': + testrun_reg_addr = ( + addr_type.def_bindspace, + _rando_port, + ) + + # NOTE, file-name uniqueness (no-collisions) will be based on + # the runtime-directory and root (pytest-proc's) pid. + case 'uds': + testrun_reg_addr = addr_type.get_random().unwrap() + + # XXX, as sanity it should never the same as the default for the + # host-singleton registry actor. + assert def_reg_addr != testrun_reg_addr + + return testrun_reg_addr diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index 93eeaf72..1a2f63ab 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -26,29 +26,46 @@ from functools import ( import inspect import platform +import pytest import tractor import trio def tractor_test(fn): ''' - Decorator for async test funcs to present them as "native" - looking sync funcs runnable by `pytest` using `trio.run()`. + Decorator for async test fns to decorator-wrap them as "native" + looking sync funcs runnable by `pytest` and auto invoked with + `trio.run()` (much like the `pytest-trio` plugin's approach). - Use: + Further the test fn body will be invoked AFTER booting the actor + runtime, i.e. from inside a `tractor.open_root_actor()` block AND + with various runtime and tooling parameters implicitly passed as + requested by by the test session's config; see immediately below. - @tractor_test - async def test_whatever(): - await ... + Basic deco use: + --------------- - If fixtures: + @tractor_test + async def test_whatever(): + await ... - - ``reg_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. + Runtime config via special fixtures: + ------------------------------------ + If any of the following fixture are requested by the wrapped test + fn (via normal func-args declaration), + + - `reg_addr` (a socket addr tuple where arbiter is listening) + - `loglevel` (logging level passed to tractor internals) + - `start_method` (subprocess spawning backend) + + (TODO support) + - `tpt_proto` (IPC transport protocol key) + + they will be automatically injected to each test as normally + expected as well as passed to the initial + `tractor.open_root_actor()` funcargs. + ''' @wraps(fn) def wrapper( @@ -111,3 +128,164 @@ def tractor_test(fn): return trio.run(main) return wrapper + + +def pytest_addoption( + parser: pytest.Parser, +): + # parser.addoption( + # "--ll", + # action="store", + # dest='loglevel', + # default='ERROR', help="logging level to set when testing" + # ) + + parser.addoption( + "--spawn-backend", + action="store", + dest='spawn_backend', + default='trio', + help="Processing spawning backend to use for test run", + ) + + parser.addoption( + "--tpdb", + "--debug-mode", + action="store_true", + dest='tractor_debug_mode', + # default=False, + help=( + 'Enable a flag that can be used by tests to to set the ' + '`debug_mode: bool` for engaging the internal ' + 'multi-proc debugger sys.' + ), + ) + + # provide which IPC transport protocols opting-in test suites + # should accumulatively run against. + parser.addoption( + "--tpt-proto", + nargs='+', # accumulate-multiple-args + action="store", + dest='tpt_protos', + default=['tcp'], + help="Transport protocol to use under the `tractor.ipc.Channel`", + ) + + +def pytest_configure(config): + backend = config.option.spawn_backend + tractor._spawn.try_set_start_method(backend) + + +@pytest.fixture(scope='session') +def debug_mode(request) -> bool: + ''' + Flag state for whether `--tpdb` (for `tractor`-py-debugger) + was passed to the test run. + + Normally tests should pass this directly to `.open_root_actor()` + to allow the user to opt into suite-wide crash handling. + + ''' + debug_mode: bool = request.config.option.tractor_debug_mode + return debug_mode + + +@pytest.fixture(scope='session') +def spawn_backend(request) -> str: + return request.config.option.spawn_backend + + +@pytest.fixture(scope='session') +def tpt_protos(request) -> list[str]: + + # allow quoting on CLI + proto_keys: list[str] = [ + proto_key.replace('"', '').replace("'", "") + for proto_key in request.config.option.tpt_protos + ] + + # ?TODO, eventually support multiple protos per test-sesh? + if len(proto_keys) > 1: + pytest.fail( + 'We only support one `--tpt-proto ` atm!\n' + ) + + # XXX ensure we support the protocol by name via lookup! + for proto_key in proto_keys: + addr_type = tractor._addr._address_types[proto_key] + assert addr_type.proto_key == proto_key + + yield proto_keys + + +@pytest.fixture( + scope='session', + autouse=True, +) +def tpt_proto( + tpt_protos: list[str], +) -> str: + proto_key: str = tpt_protos[0] + + from tractor import _state + if _state._def_tpt_proto != proto_key: + _state._def_tpt_proto = proto_key + + yield proto_key + + +@pytest.fixture(scope='session') +def reg_addr( + tpt_proto: str, +) -> tuple[str, int|str]: + ''' + Deliver a test-sesh unique registry address such + that each run's (tests which use this fixture) will + have no conflicts/cross-talk when running simultaneously + nor will interfere with other live `tractor` apps active + on the same network-host (namespace). + + ''' + from tractor._testing.addr import get_rando_addr + return get_rando_addr( + tpt_proto=tpt_proto, + ) + + +def pytest_generate_tests( + metafunc: pytest.Metafunc, +): + spawn_backend: str = metafunc.config.option.spawn_backend + + if not spawn_backend: + # XXX some weird windows bug with `pytest`? + spawn_backend = 'trio' + + # TODO: maybe just use the literal `._spawn.SpawnMethodKey`? + assert spawn_backend in ( + 'mp_spawn', + 'mp_forkserver', + 'trio', + ) + + # NOTE: used-to-be-used-to dyanmically parametrize tests for when + # 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 + # something like this again in the future? + if 'start_method' in metafunc.fixturenames: + metafunc.parametrize( + "start_method", + [spawn_backend], + scope='module', + ) + + # TODO, parametrize any `tpt_proto: str` declaring tests! + # proto_tpts: list[str] = metafunc.config.option.proto_tpts + # if 'tpt_proto' in metafunc.fixturenames: + # metafunc.parametrize( + # 'tpt_proto', + # proto_tpts, # TODO, double check this list usage! + # scope='module', + # )