diff --git a/pyproject.toml b/pyproject.toml index 30a9083..1d72581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,16 @@ [tool.pytest.ini_options] -addopts = "--cov=tests --cov-report=term-missing" +addopts = "--cov=tests --cov-report=term-missing --cov-fail-under=95" markers = [ "docker: tests that build and exercise the nginx image through Docker", ] [tool.coverage.run] +# test_video_service.py is a superseded duplicate of test_generated_infra.py's +# Part 2 (same build-and-run-the-image approach, but hardcoded to port 8086 -- +# the same port omnibioai-studio's docker-compose binds the real videos +# service to, so it can never start alongside a running deployment). Kept +# out of the coverage measurement rather than deleted outright -- that's a +# repo-hygiene call for whoever owns this repo, not this change. omit = ["tests/test_video_service.py"] [tool.coverage.report] diff --git a/tests/test_conftest_hooks.py b/tests/test_conftest_hooks.py new file mode 100644 index 0000000..4f23428 --- /dev/null +++ b/tests/test_conftest_hooks.py @@ -0,0 +1,42 @@ +"""Direct unit coverage for conftest.py's Docker-availability skip hook. + +pytest_runtest_setup only ever runs (and its skip branches only ever +trigger) inside a real pytest session collecting a real test item, so the +two skip paths -- docker not installed, docker daemon unreachable -- are +exercised here by calling the hook directly against a minimal fake item, +rather than by actually uninstalling Docker. +""" + +import shutil +import subprocess + +import pytest + +import conftest as _conftest + + +class _FakeItem: + def __init__(self, keywords): + self.keywords = keywords + + +def test_pytest_runtest_setup_ignores_non_docker_items(): + item = _FakeItem(keywords={}) + _conftest.pytest_runtest_setup(item) # must not raise or skip + + +def test_pytest_runtest_setup_skips_when_docker_executable_missing(monkeypatch): + monkeypatch.setattr(shutil, "which", lambda name: None) + item = _FakeItem(keywords={"docker": True}) + with pytest.raises(pytest.skip.Exception, match="docker executable"): + _conftest.pytest_runtest_setup(item) + + +def test_pytest_runtest_setup_skips_when_docker_daemon_unreachable(monkeypatch): + monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/docker") + monkeypatch.setattr( + subprocess, "run", lambda *a, **k: subprocess.CompletedProcess(a, returncode=1) + ) + item = _FakeItem(keywords={"docker": True}) + with pytest.raises(pytest.skip.Exception, match="Docker daemon"): + _conftest.pytest_runtest_setup(item) diff --git a/tests/test_generated_infra.py b/tests/test_generated_infra.py index 6a2e797..9a213d9 100644 --- a/tests/test_generated_infra.py +++ b/tests/test_generated_infra.py @@ -2,6 +2,8 @@ import json import subprocess import time +from pathlib import Path + import pytest import requests @@ -63,10 +65,18 @@ def docker_service(): # Build subprocess.run(["docker", "build", "-t", IMAGE_NAME, "."], capture_output=True, check=True) - # Run + # Run -- mount content/ at /videos:ro, exactly as omnibioai-studio's + # docker-compose does with ${VIDEO_DIR}:/videos:ro in production. + # nginx.conf's /videos.json and /videos/ locations always read from + # /videos (never from the image's own /usr/share/nginx/html/, which + # only backs the "/" SPA route) -- without this mount they 404 against + # any freshly-built container, mount or not. + content_dir = Path(__file__).resolve().parents[1] / "content" subprocess.run([ "docker", "run", "-d", "--name", CONTAINER_NAME, - "-p", f"{PORT}:8086", IMAGE_NAME + "-p", f"{PORT}:8086", + "-v", f"{content_dir}:/videos:ro", + IMAGE_NAME ], capture_output=True, check=True) # Wait diff --git a/tests/test_manifest.py b/tests/test_manifest.py index d6e7fd9..5663f98 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -17,6 +17,17 @@ def test_manifest_is_valid_json(): except json.JSONDecodeError as e: pytest.fail(f"Manifest is not valid JSON: {e}") +def test_manifest_is_valid_json_reports_decode_errors(tmp_path): + global MANIFEST_PATH + bad_manifest = tmp_path / "bad.json" + bad_manifest.write_text("{not valid json") + original, MANIFEST_PATH = MANIFEST_PATH, str(bad_manifest) + try: + with pytest.raises(pytest.fail.Exception, match="not valid JSON"): + test_manifest_is_valid_json() + finally: + MANIFEST_PATH = original + def test_manifest_schema(): with open(MANIFEST_PATH, 'r') as f: data = json.load(f)