''' 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)