mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: coverage campaign and mutation testing across five packages (#628)
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""Contract tests for the dispatcher security gate and installed-bundle reader.
|
||||
|
||||
_run_script_main's allowlist + feature gate is the sidecar's PRIMARY security
|
||||
boundary (there is no process isolation between scripts). These tests exercise
|
||||
the three reject branches that return BEFORE any script is exec'd, plus the
|
||||
installed-bundle reader and progress emitter. The "docs" profile is selected so
|
||||
importing the dispatcher skips all heavy ML imports."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# Select the lean profile before import so no heavy ML libraries are pulled in.
|
||||
os.environ["DISPATCHER_PROFILE"] = "docs"
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
import dispatcher # noqa: E402
|
||||
|
||||
|
||||
# --- _run_script_main security gate (returns before exec) -------------------
|
||||
|
||||
|
||||
def test_rejects_invalid_script_name_with_path_separator():
|
||||
out, code = dispatcher._run_script_main("../evil", ["x"])
|
||||
assert code == 1
|
||||
payload = json.loads(out)
|
||||
assert payload["success"] is False
|
||||
assert payload["error"] == "invalid_script_name"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["Evil", "has space", "dots.py", "semi;colon", ""])
|
||||
def test_rejects_names_failing_the_strict_regex(bad):
|
||||
out, code = dispatcher._run_script_main(bad, [])
|
||||
assert code == 1
|
||||
assert json.loads(out)["error"] == "invalid_script_name"
|
||||
|
||||
|
||||
def test_rejects_valid_format_name_not_on_allowlist():
|
||||
# Well-formed but not an allowed script -> script_not_allowed (not exec'd).
|
||||
out, code = dispatcher._run_script_main("definitely_not_a_real_script", [])
|
||||
assert code == 1
|
||||
payload = json.loads(out)
|
||||
assert payload["error"] == "script_not_allowed"
|
||||
|
||||
|
||||
def test_rejects_allowed_script_whose_bundle_is_not_installed(monkeypatch):
|
||||
# Allow an AI script that maps to a bundle, but report nothing installed.
|
||||
monkeypatch.setattr(dispatcher, "ALLOWED_SCRIPTS", {"remove_bg"})
|
||||
monkeypatch.setattr(dispatcher, "_get_installed_bundles", lambda: set())
|
||||
out, code = dispatcher._run_script_main("remove_bg", ["in.png"])
|
||||
assert code == 1
|
||||
payload = json.loads(out)
|
||||
assert payload["error"] == "feature_not_installed"
|
||||
assert payload["feature"] == "background-removal"
|
||||
|
||||
|
||||
# --- _get_installed_bundles -------------------------------------------------
|
||||
|
||||
|
||||
def test_installed_bundles_empty_when_file_missing(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(dispatcher, "INSTALLED_PATH", str(tmp_path / "nope.json"))
|
||||
assert dispatcher._get_installed_bundles() == set()
|
||||
|
||||
|
||||
def test_installed_bundles_reads_bundle_keys(monkeypatch, tmp_path):
|
||||
p = tmp_path / "installed.json"
|
||||
p.write_text(json.dumps({"bundles": {"ocr": {}, "transcription": {}}}))
|
||||
monkeypatch.setattr(dispatcher, "INSTALLED_PATH", str(p))
|
||||
assert dispatcher._get_installed_bundles() == {"ocr", "transcription"}
|
||||
|
||||
|
||||
def test_installed_bundles_empty_on_malformed_json(monkeypatch, tmp_path):
|
||||
p = tmp_path / "installed.json"
|
||||
p.write_text("{ not valid json")
|
||||
monkeypatch.setattr(dispatcher, "INSTALLED_PATH", str(p))
|
||||
assert dispatcher._get_installed_bundles() == set()
|
||||
|
||||
|
||||
# --- emit_progress ----------------------------------------------------------
|
||||
|
||||
|
||||
def test_emit_progress_writes_json_to_stderr(capsys):
|
||||
dispatcher.emit_progress(42, "removing background")
|
||||
err = capsys.readouterr().err.strip()
|
||||
payload = json.loads(err)
|
||||
assert payload == {"progress": 42, "stage": "removing background"}
|
||||
|
||||
|
||||
# --- docs profile swaps the allowlist --------------------------------------
|
||||
|
||||
|
||||
def test_docs_profile_allowlist_is_the_docs_script_set():
|
||||
# Import-time selected DISPATCHER_PROFILE=docs, so ALLOWED_SCRIPTS is the
|
||||
# lean document set, not the AI set.
|
||||
assert "doc_health" in dispatcher.ALLOWED_SCRIPTS
|
||||
assert "remove_bg" not in dispatcher.ALLOWED_SCRIPTS
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Contract tests for offline_guard: the strict-offline download gate and the
|
||||
best-effort bundle-weight symlink helpers. Pure (os + filesystem), no models,
|
||||
so this runs on any Python the sidecar supports."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
import offline_guard # noqa: E402
|
||||
|
||||
|
||||
# --- downloads_allowed ------------------------------------------------------
|
||||
|
||||
|
||||
def test_downloads_allowed_default_true(monkeypatch):
|
||||
monkeypatch.delenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", raising=False)
|
||||
assert offline_guard.downloads_allowed() is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["0", "false", "FALSE", "False"])
|
||||
def test_downloads_blocked_by_explicit_off(monkeypatch, value):
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", value)
|
||||
assert offline_guard.downloads_allowed() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["1", "true", "yes", "anything"])
|
||||
def test_downloads_allowed_for_non_off_values(monkeypatch, value):
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", value)
|
||||
assert offline_guard.downloads_allowed() is True
|
||||
|
||||
|
||||
# --- ensure_download_allowed ------------------------------------------------
|
||||
|
||||
|
||||
def test_ensure_noop_when_allowed(monkeypatch):
|
||||
monkeypatch.delenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", raising=False)
|
||||
assert offline_guard.ensure_download_allowed("thing") is None
|
||||
|
||||
|
||||
def test_ensure_raises_actionable_error_when_blocked(monkeypatch):
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "0")
|
||||
with pytest.raises(RuntimeError) as exc:
|
||||
offline_guard.ensure_download_allowed("MyModel weight")
|
||||
msg = str(exc.value)
|
||||
assert "MyModel weight" in msg
|
||||
assert "SNAPOTTER_ALLOW_MODEL_DOWNLOAD" in msg
|
||||
|
||||
|
||||
# --- link_bundled_weight ----------------------------------------------------
|
||||
|
||||
|
||||
def test_link_already_present_returns_true_without_symlink(tmp_path):
|
||||
link = tmp_path / "present.pth"
|
||||
link.write_text("x")
|
||||
target = tmp_path / "target.pth" # intentionally absent
|
||||
assert offline_guard.link_bundled_weight(str(link), str(target)) is True
|
||||
assert not link.is_symlink() # left untouched
|
||||
|
||||
|
||||
def test_link_target_missing_returns_false(tmp_path):
|
||||
link = tmp_path / "missing" / "link.pth"
|
||||
target = tmp_path / "nope.pth" # absent
|
||||
assert offline_guard.link_bundled_weight(str(link), str(target)) is False
|
||||
assert not link.exists()
|
||||
|
||||
|
||||
def test_link_creates_symlink_and_parent_dirs(tmp_path):
|
||||
target = tmp_path / "bundle" / "weight.pth"
|
||||
target.parent.mkdir()
|
||||
target.write_text("weights")
|
||||
link = tmp_path / "nested" / "dir" / "weight.pth"
|
||||
assert offline_guard.link_bundled_weight(str(link), str(target)) is True
|
||||
assert link.is_symlink()
|
||||
assert link.read_text() == "weights" # resolves to the target bytes
|
||||
|
||||
|
||||
def test_link_oserror_returns_false_when_symlink_fails(tmp_path, monkeypatch):
|
||||
target = tmp_path / "t.pth"
|
||||
target.write_text("w")
|
||||
link = tmp_path / "l.pth"
|
||||
|
||||
def raise_oserror(*_a, **_k):
|
||||
raise OSError("read-only fs")
|
||||
|
||||
monkeypatch.setattr(offline_guard.os, "symlink", raise_oserror)
|
||||
# symlink fails and the link never materialised -> False.
|
||||
assert offline_guard.link_bundled_weight(str(link), str(target)) is False
|
||||
|
||||
|
||||
# --- prepare_gfpgan_helper_weights / prepare_codeformer_weights -------------
|
||||
|
||||
|
||||
def test_prepare_gfpgan_links_present_weights(tmp_path, monkeypatch):
|
||||
models = tmp_path / "models"
|
||||
facelib = models / "gfpgan" / "facelib"
|
||||
facelib.mkdir(parents=True)
|
||||
for fname in offline_guard.GFPGAN_HELPER_WEIGHTS:
|
||||
(facelib / fname).write_text("w")
|
||||
monkeypatch.chdir(tmp_path) # link paths are cwd-relative
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "0") # would raise on any miss
|
||||
offline_guard.prepare_gfpgan_helper_weights(str(models)) # must not raise
|
||||
for fname in offline_guard.GFPGAN_HELPER_WEIGHTS:
|
||||
assert (tmp_path / "gfpgan" / "weights" / fname).exists()
|
||||
|
||||
|
||||
def test_prepare_gfpgan_raises_offline_when_weight_missing(tmp_path, monkeypatch):
|
||||
models = tmp_path / "models" # nothing installed
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "0")
|
||||
with pytest.raises(RuntimeError):
|
||||
offline_guard.prepare_gfpgan_helper_weights(str(models))
|
||||
|
||||
|
||||
def test_prepare_codeformer_links_present_weights(tmp_path, monkeypatch):
|
||||
models = tmp_path / "models"
|
||||
layout = [
|
||||
("codeformer", "codeformer.pth"),
|
||||
("gfpgan", "facelib", "detection_Resnet50_Final.pth"),
|
||||
("gfpgan", "facelib", "parsing_parsenet.pth"),
|
||||
("realesrgan", "RealESRGAN_x2plus.pth"),
|
||||
]
|
||||
for parts in layout:
|
||||
p = models.joinpath(*parts)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text("w")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "0")
|
||||
offline_guard.prepare_codeformer_weights(str(models)) # must not raise
|
||||
|
||||
|
||||
def test_prepare_codeformer_raises_offline_when_missing(tmp_path, monkeypatch):
|
||||
models = tmp_path / "models"
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "0")
|
||||
with pytest.raises(RuntimeError):
|
||||
offline_guard.prepare_codeformer_weights(str(models))
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Contract tests for progress_heartbeat.run_with_heartbeat: it advances a
|
||||
rising progress bar from a background thread while an opaque model call runs,
|
||||
caps below ``end``, returns the call's value, and propagates its exception.
|
||||
Pure (threading), deterministic via events rather than sleep races."""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
import progress_heartbeat # noqa: E402
|
||||
|
||||
|
||||
def test_returns_fn_value_without_beats_when_interval_long():
|
||||
emitted = []
|
||||
result = progress_heartbeat.run_with_heartbeat(
|
||||
lambda: 42,
|
||||
lambda pct, stage: emitted.append((pct, stage)),
|
||||
start=0,
|
||||
end=100,
|
||||
stage="work",
|
||||
interval=100,
|
||||
)
|
||||
assert result == 42
|
||||
assert emitted == [] # fn returned long before the first (100s) beat
|
||||
|
||||
|
||||
def test_propagates_fn_exception():
|
||||
emitted = []
|
||||
|
||||
def boom():
|
||||
raise ValueError("nope")
|
||||
|
||||
with pytest.raises(ValueError, match="nope"):
|
||||
progress_heartbeat.run_with_heartbeat(
|
||||
boom,
|
||||
lambda pct, stage: emitted.append((pct, stage)),
|
||||
start=0,
|
||||
end=100,
|
||||
stage="work",
|
||||
interval=100,
|
||||
)
|
||||
assert emitted == []
|
||||
|
||||
|
||||
def test_emits_rising_progress_capped_below_end():
|
||||
emitted = []
|
||||
reached_two = threading.Event()
|
||||
|
||||
def emit(pct, stage):
|
||||
emitted.append((pct, stage))
|
||||
if len(emitted) >= 2:
|
||||
reached_two.set()
|
||||
|
||||
def fn():
|
||||
reached_two.wait(timeout=2.0) # block until two heartbeats fire
|
||||
return "done"
|
||||
|
||||
result = progress_heartbeat.run_with_heartbeat(
|
||||
fn, emit, start=10, end=13, stage="infer", interval=0.01
|
||||
)
|
||||
assert result == "done"
|
||||
pcts = [p for p, _ in emitted]
|
||||
# start+1 .. end-1, monotonically rising, never reaching end.
|
||||
assert pcts == [11, 12]
|
||||
assert all(stage == "infer" for _, stage in emitted)
|
||||
assert max(pcts) <= 13 - 1
|
||||
|
||||
|
||||
def test_no_emit_when_start_at_cap():
|
||||
emitted = []
|
||||
|
||||
def fn():
|
||||
time.sleep(0.05) # give the beat thread room to attempt an emit
|
||||
return "x"
|
||||
|
||||
result = progress_heartbeat.run_with_heartbeat(
|
||||
fn, lambda pct, stage: emitted.append(pct), start=12, end=13, stage="s", interval=0.01
|
||||
)
|
||||
assert result == "x"
|
||||
assert emitted == [] # pct=12 is not < end-1=12, so nothing is emitted
|
||||
|
||||
|
||||
def test_stops_emitting_after_fn_returns():
|
||||
emitted = []
|
||||
reached_one = threading.Event()
|
||||
|
||||
def emit(pct, stage):
|
||||
emitted.append(pct)
|
||||
reached_one.set()
|
||||
|
||||
def fn():
|
||||
reached_one.wait(timeout=2.0)
|
||||
return None
|
||||
|
||||
progress_heartbeat.run_with_heartbeat(
|
||||
fn, emit, start=0, end=100, stage="s", interval=0.01
|
||||
)
|
||||
count = len(emitted)
|
||||
time.sleep(0.05) # a still-alive beat thread would push more emits here
|
||||
assert len(emitted) == count # the heartbeat stopped on return
|
||||
Reference in New Issue
Block a user