Source code for pykern.api.unit_util
"""Support for `pykern.api` tests
:copyright: Copyright (c) 2024 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
# Defer as many pykern imports as possible to defer pkconfig runing
from pykern.pkcollections import PKDict
import os
import signal
import time
_AUTH_TOKEN = "http_unit_auth_secret"
[docs]
class Setup:
"""Usage::
async with http_unit.Setup(api_classes=(_class())) as c:
from pykern.pkcollections import PKDict
from pykern import pkunit
e = PKDict(ping="pong")
pkunit.pkeq(e.pkupdate(counter=1), await c.call_api("echo", e))
pkunit.pkeq(e.pkupdate(counter=2), await c.call_api("echo", e))
May be subclassed to start multiple servers.
"""
def __init__(self, **server_config):
# Must be first
self._global_config()
self.http_config = self._http_config()
self.server_config = self._server_config(server_config)
self.server_pid = self._server_process()
time.sleep(2)
self.client = self._client()
[docs]
def destroy(self):
"""Destroy client and kill attributes ``*_pid`` attrs"""
self.client.destroy()
for p in filter(lambda x: x.endswith("_pid"), dir(self)):
try:
os.kill(getattr(self, p), signal.SIGKILL)
except Exception:
pass
def _client(self):
"""Creates a client to be used for requests.
Called in `__init__`.
Returns:
object: http client, set to ``self.client``
"""
from pykern.api import client
return client.Client(self.http_config.copy())
def _client_awaitable(self):
"""How to connect to client
Awaited in `__aenter__`.
Returns:
Awaitable: coroutine to connect to client
"""
return self.client.connect(PKDict(token=_AUTH_TOKEN))
def _global_config(self, **kwargs):
"""Initializes os.environ and pkconfig
Called first.
Args:
kwargs (dict): merged into environ and config (from subclasses)
"""
c = PKDict(
PYKERN_PKDEBUG_WANT_PID_TIME="1",
**kwargs,
)
os.environ.update(**c)
from pykern import pkconfig
pkconfig.reset_state_for_testing(c)
def _http_config(self):
"""Initializes ``self.http_config``
Returns:
PKDict: configuration to be shared with client and server
"""
from pykern import pkconst, util
return PKDict(
# any uri is fine
api_uri="/api_unit",
# just needs to be >= 16 word (required by http) chars; apps should generate this randomly
tcp_ip=pkconst.LOCALHOST_IP,
tcp_port=util.unbound_localhost_tcp_port(),
)
def _server_config(self, init_config):
"""Config to be passed to `pykern.api.server.start` in `server_start`
A simple `pykern.api.auth_api.AuthAPI` implementation is defaulted if not in ``init_config.api_classes``.
Args:
init_config (dict): what was passed to `__init__`
Returns:
PKDict: configuration for `server_start`
"""
def _api_classes(init_classes):
from pykern import pkdebug
from pykern.api import auth_api
class AuthAPI(auth_api.AuthAPI):
def token(self):
return _AUTH_TOKEN
return init_classes + [AuthAPI]
rv = PKDict(init_config) if init_config else PKDict()
rv.pksetdefault(
api_classes=(),
attr_classes=(),
coros=(),
http_config=PKDict,
)
rv.http_config.pksetdefault(**self.http_config)
rv.api_classes = _api_classes(list(rv.api_classes))
return rv
def _server_process(self):
"""Call `server_start` in separate process
Override this method to start multiple servers, saving pids in
attributes that end in ``_pid`` so that `destroy` will kill
them.
Returns:
int: pid of process
"""
from pykern import pkdebug
if rv := os.fork():
return rv
try:
pkdebug.pkdlog("start server")
self._server_start()
except Exception as e:
pkdebug.pkdlog("exception={} stack={}", e, pkdebug.pkdexc())
finally:
os._exit(0)
def _server_start(self):
"""Calls api.server.start with ``self.server_config``"""
from pykern.api import server
server.start(**self.server_config)
async def __aenter__(self):
await self._client_awaitable()
return self.client
async def __aexit__(self, *args, **kwargs):
self.destroy()
return False