|
| 1 | +import multiprocessing |
| 2 | +import os |
| 3 | +import sys |
| 4 | +import threading |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +import pytest |
| 8 | +import uvicorn |
| 9 | +from testcontainers.core.container import DockerContainer |
| 10 | +from testcontainers.core.wait_strategies import HttpWaitStrategy, LogMessageWaitStrategy |
| 11 | + |
| 12 | +APPLICATION_HOST = "0.0.0.0" |
| 13 | +APPLICATION_PORT = 8000 |
| 14 | +HTTP_STUB_PORT = 8080 |
| 15 | + |
| 16 | + |
| 17 | +class UvicornServer(multiprocessing.Process): |
| 18 | + def __init__(self, config: uvicorn.Config): |
| 19 | + super().__init__() |
| 20 | + self.server = uvicorn.Server(config=config) |
| 21 | + self.config = config |
| 22 | + |
| 23 | + def stop(self): |
| 24 | + self.terminate() |
| 25 | + |
| 26 | + def run(self, *args, **kwargs): |
| 27 | + self.server.run() |
| 28 | + |
| 29 | + |
| 30 | +def stream_container_logs(container: DockerContainer, name=None): |
| 31 | + def _stream(): |
| 32 | + for line in container.get_wrapped_container().logs(stream=True, follow=True): |
| 33 | + text = line.decode(errors="ignore").rstrip() |
| 34 | + prefix = f"[{name}] " if name else "" |
| 35 | + print(f"{prefix}{text}") |
| 36 | + |
| 37 | + thread = threading.Thread(target=_stream, daemon=True) |
| 38 | + thread.start() |
| 39 | + return thread |
| 40 | + |
| 41 | + |
| 42 | +@pytest.fixture(scope="module") |
| 43 | +def api_service(): |
| 44 | + config = uvicorn.Config("app.main:app", host=APPLICATION_HOST, port=APPLICATION_PORT, log_level="info") |
| 45 | + server = UvicornServer(config) |
| 46 | + server.start() |
| 47 | + yield server |
| 48 | + server.stop() |
| 49 | + |
| 50 | + |
| 51 | +@pytest.fixture(scope="module") |
| 52 | +def stub_container(): |
| 53 | + examples_path = Path("test/contract/data").resolve() |
| 54 | + specmatic_yaml_path = Path("specmatic.yaml").resolve() |
| 55 | + build_reports_path = Path("build/reports/specmatic").resolve() |
| 56 | + container = ( |
| 57 | + DockerContainer("specmatic/specmatic") |
| 58 | + .with_command(["virtualize", "--examples=examples", f"--port={HTTP_STUB_PORT}"]) |
| 59 | + .with_bind_ports(HTTP_STUB_PORT, HTTP_STUB_PORT) |
| 60 | + .with_volume_mapping(examples_path, "/usr/src/app/examples", mode="ro") |
| 61 | + .with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro") |
| 62 | + .with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw") |
| 63 | + .waiting_for(HttpWaitStrategy(HTTP_STUB_PORT, path="/actuator/health").with_method("GET").for_status_code(200)) |
| 64 | + ) |
| 65 | + container.start() |
| 66 | + stream_container_logs(container, name="specmatic-stub") |
| 67 | + yield container |
| 68 | + container.stop() |
| 69 | + |
| 70 | + |
| 71 | +@pytest.fixture(scope="module") |
| 72 | +def test_container(): |
| 73 | + specmatic_yaml_path = Path("specmatic.yaml").resolve() |
| 74 | + build_reports_path = Path("build/reports/specmatic").resolve() |
| 75 | + container = ( |
| 76 | + DockerContainer("specmatic/specmatic") |
| 77 | + .with_command(["test", "--host=host.docker.internal", f"--port={APPLICATION_PORT}"]) |
| 78 | + .with_env("SPECMATIC_GENERATIVE_TESTS", "true") |
| 79 | + .with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro") |
| 80 | + .with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw") |
| 81 | + .with_kwargs(extra_hosts={"host.docker.internal": "host-gateway"}) |
| 82 | + .waiting_for(LogMessageWaitStrategy("Tests run:")) |
| 83 | + ) |
| 84 | + container.start() |
| 85 | + stream_container_logs(container, name="specmatic-test") |
| 86 | + yield container |
| 87 | + container.stop() |
| 88 | + |
| 89 | + |
| 90 | +@pytest.mark.skipif( |
| 91 | + os.environ.get("CI") == "true" and not sys.platform.startswith("linux"), |
| 92 | + reason="Run only on Linux CI; all platforms allowed locally", |
| 93 | +) |
| 94 | +def test_contract(api_service, stub_container, test_container): |
| 95 | + stdout, stderr = test_container.get_logs() |
| 96 | + stdout = stdout.decode("utf-8") |
| 97 | + stderr = stderr.decode("utf-8") |
| 98 | + if stderr or "Failures: 0" not in stdout: |
| 99 | + raise AssertionError(f"Contract tests failed; container logs:\n{stdout}\n{stderr}") # noqa: EM102 |
0 commit comments