piker/tests/test_startup.py

215 lines
6.8 KiB
Python

'''
Offline regressions for endpoint configuration and service startup.
'''
from contextlib import asynccontextmanager as acm
from pathlib import Path
import sys
import tomllib
from types import ModuleType
from click.testing import CliRunner
import pytest
import tractor
import trio
from piker import config
from piker.cli import load_trans_eps, pikerd
from piker.service import _daemon, _actor_runtime
@pytest.mark.parametrize('repeat', [False, True])
def test_maddrs_click_collection(repeat):
'''
Click previously produced a string for `--maddrs`, which endpoint
loading split into individual characters. Parse one or repeated
options through Click and then the real Tractor parser, without
entering the indefinitely running daemon callback. Every address
must survive intact and in the declared order.
'''
addresses = ['/ip4/127.0.0.1/tcp/6116']
if repeat:
addresses.append('/ip4/127.0.0.1/tcp/6117')
args = [
arg for address in addresses for arg in ['--maddrs', address]
]
with pikerd.make_context('pikerd', args) as ctx:
endpoints = load_trans_eps(maddrs=ctx.params['maddrs'])
assert [addr.unwrap() for addr in endpoints['pikerd']] == [
('tcp', '127.0.0.1', 6116 + index)
for index in range(len(addresses))
]
def test_single_string_multiaddr():
'''
Direct endpoint callers may provide one string, independently of
Click's collection conversion. That input must denote a single
endpoint rather than a sequence of invalid character endpoints.
'''
endpoints = load_trans_eps(maddrs='/ip4/127.0.0.1/tcp/6116')
assert endpoints['pikerd'][0].unwrap() == (
'tcp', '127.0.0.1', 6116,
)
def test_template_single_transport():
'''
The shipped template enabled TCP and UDS together although the
locked Tractor runtime permits only one transport per actor.
Parse the real template and ensure both daemon and chart defaults
use the TCP transport selected by their startup paths.
'''
path = Path(__file__).parents[1] / 'config' / 'conf.toml'
conf = tomllib.loads(path.read_text())
endpoints = load_trans_eps(conf['network'])
for name in ['pikerd', 'chart']:
assert endpoints[name]
assert {addr.proto_key for addr in endpoints[name]} == {'tcp'}
@pytest.mark.parametrize('network', [None, {}])
def test_chart_without_network(monkeypatch, network):
'''
Existing configs without a populated network table left chart
bind endpoints unbound, crashing before the UI entrypoint. Invoke
the actual Click callback with absent and empty tables, replacing
only config IO and the UI entrypoint. Automatic binding must be
passed as None while preserving the configured registrar address.
'''
from piker.ui.cli import chart
conf = {} if network is None else {'network': network}
monkeypatch.setattr(config, 'load', lambda **_: (conf, None))
app = ModuleType('piker.ui._app')
calls = []
app._main = lambda **kwargs: calls.append(kwargs)
monkeypatch.setitem(sys.modules, 'piker.ui._app', app)
result = CliRunner().invoke(
chart,
['btcusdt.binance'],
obj={
'pdb': False,
'brokermods': [object()],
'tractorloglevel': 'warning',
'loglevel': 'warning',
'registry_addrs': [('127.0.0.1', 6116)],
},
)
assert result.exit_code == 0, result.exception
runtime = calls[0]['tractor_kwargs']
assert runtime['tpt_bind_addrs'] is None
assert runtime['registry_addrs'] == [('127.0.0.1', 6116)]
@pytest.mark.parametrize('explicit_registry', [False, True])
def test_daemon_options_resolve_registrars(
monkeypatch,
explicit_registry,
):
'''
Any nonempty pikerd kwargs previously skipped registrar address
initialization. Exercise the full discovery and spawn path with
runtime options, using protocol-boundary fakes instead of remote
actors. An explicit override must reach discovery and startup
exactly once; otherwise the inherited registrar list is used.
The service lock must be released after leaving the context.
'''
inherited = [('tcp', '127.0.0.1', 6116)]
override = [('tcp', '127.0.0.1', 6117)]
expected = override if explicit_registry else inherited
calls = []
portal = object()
@acm
async def find(name, registry_addrs):
calls.append(('find', registry_addrs))
yield None
@acm
async def open_root(**kwargs):
calls.append(('open', kwargs))
yield None
@acm
async def wait(name):
yield portal
async def start(**kwargs):
return True
monkeypatch.setattr(_daemon, 'find_service', find)
monkeypatch.setattr(_daemon, 'maybe_open_pikerd', open_root)
monkeypatch.setattr(tractor, 'wait_for_actor', wait)
monkeypatch.setattr(
tractor,
'get_runtime_vars',
lambda: {'_registry_addrs': inherited},
)
async def main():
lock = trio.Lock()
monkeypatch.setitem(_daemon.Services.locks, 'probe', lock)
kwargs = {'debug_mode': False}
if explicit_registry:
kwargs['registry_addrs'] = override
async with _daemon.maybe_spawn_daemon(
'probe', start, {}, **kwargs,
) as found:
assert found is portal
assert not lock.locked()
trio.run(main)
assert calls[0] == ('find', expected)
assert calls[1][1]['registry_addrs'] == expected
assert calls[1][1]['debug_mode'] is False
async def _check_distinct_daemon_bind():
'''
Open Piker's service context on a non-registrar child actor.
'''
actor = tractor.current_actor()
assert not actor.is_registrar
assert actor.accept_addrs != actor.reg_addrs
async with _actor_runtime.open_pikerd(
registry_addrs=actor.reg_addrs,
tpt_bind_addrs=[('127.0.0.1', 0)],
):
return actor.accept_addrs
def test_daemon_bind_distinct_from_registrar(reg_addr):
'''
Separate daemon and registrar endpoints were rejected because
startup checked registrar addresses against daemon listeners.
Run a real TCP registrar and a distinct child daemon with an
ephemeral listener. Its service context must start and unwind
without taking ownership of the registrar's listening address.
'''
async def main():
async with (
tractor.open_root_actor(
registry_addrs=[reg_addr],
enable_transports=['tcp'],
),
tractor.open_nursery() as nursery,
):
portal = await nursery.start_actor(
'daemon_probe',
enable_modules=[__name__],
bind_addrs=[('127.0.0.1', 0)],
)
addresses = await portal.run(_check_distinct_daemon_bind)
assert addresses
await nursery.cancel()
trio.run(main)