Source code for chaise.testing

  1"""
  2Provides utilities for testing
  3"""
  4
  5import contextlib
  6import functools
  7from pathlib import Path
  8import socket
  9import sys
 10import time
 11import typing
 12
 13import anyio
 14import docker
 15import docker.utils
 16import httpx
 17
 18
[docs] 19class ContextNotExistError(ValueError): 20 """ 21 The given Docker context does not exist. 22 23 This should only happen if your Docker client is misconfigured. 24 """
25 26 27def _docker_3190_workaround(): 28 """ 29 Work around for https://github.com/docker/docker-py/issues/3190 30 """ 31 if docker.utils.config.find_config_file() is None: 32 # TODO: Prefer .config_path_from_environment() over .home_dir() 33 config_path = ( 34 Path(docker.utils.config.home_dir()) 35 / docker.utils.config.DOCKER_CONFIG_FILENAME 36 ) 37 38 if config_path.parent.exists(): 39 # If .docker doesn't exist, it doesn't contain contexts 40 config_path.touch() 41 42 43@functools.cache 44def _get_docker_client(use: str | None = None) -> docker.DockerClient: 45 """ 46 Get a docker client for the given docker context. 47 48 Unlike docker.from_env(), this considers the user's configured context. 49 """ 50 _docker_3190_workaround() 51 52 context = docker.ContextAPI.get_context(use) 53 if context is None: 54 raise ContextNotExistError(f"Docker context {use!r} not found") 55 return docker.DockerClient( 56 base_url=context.endpoints["docker"]["Host"], tls=context.TLSConfig 57 ) 58 59 60# https://stackoverflow.com/a/45690594 61def _find_free_port(): 62 with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 63 s.bind(("", 0)) 64 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 65 return s.getsockname()[1] 66 67
[docs] 68@contextlib.contextmanager 69def spawn_docker_couchdb() -> typing.Iterator[str]: 70 """ 71 Creates a tempory CouchDB instance using Docker, and automatically cleans it up. 72 73 Returns the URL by which it's accessible. 74 """ 75 client = _get_docker_client() 76 77 couch_container = client.containers.run( 78 detach=True, 79 image="ghcr.io/teahouse-hosting/quick-and-dirty-couch:latest", 80 auto_remove=True, 81 environment={ 82 "COUCHDB_USER": "admin", 83 "COUCHDB_PASSWORD": "admin", 84 }, 85 ports={"5984/tcp": None}, 86 ) 87 # TODO: Stream container stdout 88 89 port_config = None 90 while port_config is None: 91 couch_container.reload() 92 try: 93 # Dig out the connected port 94 port_config = couch_container.attrs["NetworkSettings"]["Ports"]["5984/tcp"][ 95 0 96 ] 97 except IndexError: 98 time.sleep(0.1) 99 100 couch_ip = port_config["HostIp"] 101 if couch_ip == "0.0.0.0": 102 couch_ip = "127.0.0.1" 103 elif couch_ip == "::": 104 couch_ip = "::1" 105 couch_port = port_config["HostPort"] 106 107 try: 108 yield f"http://admin:admin@{couch_ip}:{couch_port}/" 109 finally: 110 couch_container.stop()
111 112
[docs] 113async def wait_for_readiness(couch_url: str, *, timeout: float = 60) -> None: 114 """ 115 Waits for a CouchDB instance to actually initialize and be ready to accept 116 requests. 117 118 Raises TimeoutError on failure. 119 """ 120 timeout_increment = 0.1 121 url = httpx.URL(couch_url).join("_up") 122 client = httpx.AsyncClient() 123 for _ in range(int(timeout / timeout_increment)): 124 try: 125 resp = await client.get(url) 126 except httpx.RequestError: 127 await anyio.sleep(timeout_increment) 128 else: 129 if resp.is_success: 130 return 131 else: 132 await anyio.sleep(timeout_increment) 133 else: 134 raise TimeoutError(f"Timeout waiting for CouchDB to initialize ({couch_url})")
135 136 137async def _call_cli(couch_url, *argv): 138 oldargv = sys.argv 139 try: 140 sys.argv = ["chaise", "--verbose", "--server", couch_url, *argv] 141 import chaise.cli 142 143 await chaise.cli.main() 144 finally: 145 sys.argv = oldargv 146 147
[docs] 148@contextlib.asynccontextmanager 149async def run_cli_apply(couch_url: str, dbs_module: str): 150 """ 151 Context manager that runs the apply CLI command, and then cleans up 152 databases afterwards. 153 """ 154 await _call_cli(couch_url, "apply", dbs_module) 155 yield 156 # TODO: Look up what databases were actually defined 157 from chaise.cli.client import ConstantPool 158 159 session = await ConstantPool(couch_url).session() 160 dbs = {db async for db in session.iter_dbs() if not db.startswith("_")} 161 for db in dbs: 162 await session.delete_db(db)