from pathlib import Path import socket from contextlib import contextmanager, ExitStack, closing import json from http.server import SimpleHTTPRequestHandler, HTTPServer from typing import NamedTuple, Any, Iterator, Callable from concurrent.futures import ThreadPoolExecutor from functools import partial import tempfile import sys import subprocess import pytest from _pytest.fixtures import FixtureRequest from _pytest.tmpdir import TempPathFactory from dds_ci.dds import DDSWrapper def _unused_tcp_port() -> int: """Find an unused localhost TCP port from 1024-65535 and return it.""" with closing(socket.socket()) as sock: sock.bind(('127.0.0.1', 0)) return sock.getsockname()[1] class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): """ A simple HTTP request handler that simply serves files from a directory given to the constructor. """ def __init__(self, *args: Any, **kwargs: Any) -> None: self.dir = kwargs.pop('dir') super().__init__(*args, **kwargs) def translate_path(self, path: str) -> str: # Convert the given URL path to a path relative to the directory we are serving abspath = Path(super().translate_path(path)) # type: ignore relpath = abspath.relative_to(Path.cwd()) return str(self.dir / relpath) class ServerInfo(NamedTuple): """ Information about an HTTP server fixture """ base_url: str root: Path @contextmanager def run_http_server(dirpath: Path, port: int) -> Iterator[ServerInfo]: """ Context manager that spawns an HTTP server that serves thegiven directory on the given TCP port. """ handler = partial(DirectoryServingHTTPRequestHandler, dir=dirpath) addr = ('127.0.0.1', port) pool = ThreadPoolExecutor() with HTTPServer(addr, handler) as httpd: pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) try: print('Serving at', addr) yield ServerInfo(f'http://127.0.0.1:{port}', dirpath) finally: httpd.shutdown() HTTPServerFactory = Callable[[Path], ServerInfo] @pytest.fixture(scope='session') def http_server_factory(request: FixtureRequest) -> HTTPServerFactory: """ Spawn an HTTP server that serves the content of a directory. """ def _make(p: Path) -> ServerInfo: st = ExitStack() server = st.enter_context(run_http_server(p, _unused_tcp_port())) request.addfinalizer(st.pop_all) return server return _make class RepoServer: """ A fixture handle to a dds HTTP repository, including a path and URL. """ def __init__(self, dds_exe: Path, info: ServerInfo, repo_name: str) -> None: self.repo_name = repo_name self.server = info self.url = info.base_url self.dds_exe = dds_exe def import_json_data(self, data: Any) -> None: """ Import some packages into the repo for the given JSON data. Uses mkrepo.py """ with tempfile.NamedTemporaryFile(delete=False) as f: f.write(json.dumps(data).encode()) f.close() self.import_json_file(Path(f.name)) Path(f.name).unlink() def import_json_file(self, fpath: Path) -> None: """ Import some package into the repo for the given JSON file. Uses mkrepo.py """ subprocess.check_call([ sys.executable, str(Path.cwd() / 'tools/mkrepo.py'), f'--dds-exe={self.dds_exe}', f'--dir={self.server.root}', f'--spec={fpath}', ]) RepoFactory = Callable[[str], Path] @pytest.fixture(scope='session') def repo_factory(tmp_path_factory: TempPathFactory, dds: DDSWrapper) -> RepoFactory: def _make(name: str) -> Path: tmpdir = tmp_path_factory.mktemp('test-repo-') dds.run(['repoman', 'init', tmpdir, f'--name={name}']) return tmpdir return _make HTTPRepoServerFactory = Callable[[str], RepoServer] @pytest.fixture(scope='session') def http_repo_factory(dds_exe: Path, repo_factory: RepoFactory, http_server_factory: HTTPServerFactory) -> HTTPRepoServerFactory: """ Fixture factory that creates new repositories with an HTTP server for them. """ def _make(name: str) -> RepoServer: repo_dir = repo_factory(name) server = http_server_factory(repo_dir) return RepoServer(dds_exe, server, name) return _make @pytest.fixture() def http_repo(http_repo_factory: HTTPRepoServerFactory, request: FixtureRequest) -> RepoServer: """ Fixture that creates a new empty dds repository and an HTTP server to serve it. """ return http_repo_factory(f'test-repo-{request.function.__name__}')