feat: unified config.yaml with extensible model presets (A-D data-driven, E/F ready)
ci / smoke (push) Canceled after 0s
ci / smoke (push) Canceled after 0s
- config.yaml canonical for stream/models/counting/batch/output/camera - models.modes hold engines+class filters only; conf/iou/min_bbox in detection_params - predict.py derives tracker roles structurally (no per-mode branching) - dashboard mode switch validates + persists atomically to config.yaml - .env keeps secrets/deployment only; zones.json geometry; tracker.yaml hyperparams - batch_mode.json keeps manual/auto batch mode; legacy model_mode ignored w/ warning - src/detection+tracking gain iou param (default 0.7 = no behavior change)
This commit is contained in:
1 parent
630f4bc29e
commit
2d9c66cbb6
14 files changed
+1254
-199
No files matched your search
@@ -0,0 +1,128 @@
|
||||
"""Smoke tests for src/config_loader.py — pure-Python, no cv2/ultralytics needed."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from src.config_loader import (
|
||||
check_legacy_batch_mode,
|
||||
load_config,
|
||||
read_zone_polygons,
|
||||
resolve_active_mode,
|
||||
set_active_mode,
|
||||
validate_config,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def repo_config_path():
|
||||
p = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.yaml")
|
||||
assert os.path.exists(p), "repo config.yaml missing"
|
||||
return p
|
||||
|
||||
|
||||
def test_repo_config_loads_and_validates(repo_config_path):
|
||||
cfg = load_config(repo_config_path)
|
||||
assert cfg.models.active_mode == "C"
|
||||
assert set(cfg.models.modes) >= {"A", "B", "C", "D"}
|
||||
for mode_id, mode in cfg.models.modes.items():
|
||||
assert mode.engines, f"mode {mode_id} has no engines"
|
||||
for e in mode.engines:
|
||||
assert e.path in cfg.models.paths, f"mode {mode_id}: unknown engine {e.path}"
|
||||
assert e.classes, f"mode {mode_id}: engine {e.path} declares no classes"
|
||||
for cls_name in ("truck", "sack", "box"):
|
||||
dp = cfg.detection_params_for(cls_name)
|
||||
assert 0.0 < dp.conf <= 1.0
|
||||
assert 0.0 < dp.iou <= 1.0
|
||||
assert dp.min_bbox_area >= 0
|
||||
|
||||
|
||||
def test_mode_preset_only_filters_and_engines(repo_config_path):
|
||||
"""Modes must not carry per-class conf/iou — those live in detection_params."""
|
||||
with open(repo_config_path) as f:
|
||||
raw = yaml.safe_load(f)
|
||||
for mode_id, mode in raw["models"]["modes"].items():
|
||||
assert "conf" not in mode, f"mode {mode_id}: conf belongs in detection_params"
|
||||
assert "iou" not in mode, f"mode {mode_id}: iou belongs in detection_params"
|
||||
assert "min_bbox_area" not in mode, f"mode {mode_id}"
|
||||
|
||||
|
||||
def test_resolve_active_mode_precedence(repo_config_path, monkeypatch):
|
||||
cfg = load_config(repo_config_path)
|
||||
monkeypatch.delenv("MODEL_MODE", raising=False)
|
||||
assert resolve_active_mode(None, cfg) == "C"
|
||||
assert resolve_active_mode("d", cfg) == "D"
|
||||
with pytest.warns(UserWarning): # unknown -> fallback to active
|
||||
assert resolve_active_mode("Z", cfg) == "C"
|
||||
monkeypatch.setenv("MODEL_MODE", "B")
|
||||
with pytest.warns(DeprecationWarning): # env still honoured, deprecated
|
||||
assert resolve_active_mode(None, cfg) == "B"
|
||||
|
||||
|
||||
def test_missing_file_falls_back_with_warning(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("MODEL_MODE", raising=False)
|
||||
with pytest.warns(UserWarning, match="not found"):
|
||||
cfg = load_config(tmp_path / "nope.yaml")
|
||||
assert cfg.models.active_mode == "C"
|
||||
assert cfg.stream.inference_stride == 2
|
||||
|
||||
|
||||
def test_set_active_mode_roundtrip_and_validation(tmp_path, repo_config_path):
|
||||
import shutil
|
||||
|
||||
dst = tmp_path / "config.yaml"
|
||||
shutil.copy(repo_config_path, dst)
|
||||
assert set_active_mode(dst, "d") == "D"
|
||||
assert load_config(dst).models.active_mode == "D"
|
||||
text = dst.read_text()
|
||||
assert 'active_mode: "D"' in text
|
||||
assert "# config.yaml" in text # comments preserved (no yaml.dump reformat)
|
||||
with pytest.raises(ValueError, match="Invalid model mode"):
|
||||
set_active_mode(dst, "Z")
|
||||
assert load_config(dst).models.active_mode == "D" # untouched on failure
|
||||
|
||||
|
||||
def test_future_mode_extensible_without_code(tmp_path, repo_config_path):
|
||||
"""Adding mode E is a YAML-only change: loader accepts it, no code edits."""
|
||||
import shutil
|
||||
|
||||
dst = tmp_path / "config.yaml"
|
||||
shutil.copy(repo_config_path, dst)
|
||||
with open(dst) as f:
|
||||
raw = yaml.safe_load(f)
|
||||
raw["models"]["modes"]["E"] = {
|
||||
"description": "hypothetical future preset",
|
||||
"engines": [{"path": "combined", "classes": ["truck", "sack"]}],
|
||||
"class_filters": {"truck": ["truck"], "sack": ["sack"], "box": []},
|
||||
}
|
||||
with open(dst, "w") as f:
|
||||
yaml.safe_dump(raw, f)
|
||||
cfg = load_config(dst)
|
||||
assert resolve_active_mode("E", cfg) == "E"
|
||||
assert set_active_mode(dst, "E") == "E"
|
||||
|
||||
|
||||
def test_legacy_batch_mode_check(tmp_path, repo_config_path):
|
||||
cfg = load_config(repo_config_path)
|
||||
legacy = tmp_path / "batch_mode.json"
|
||||
legacy.write_text(json.dumps({"mode": "auto", "model_mode": "B"}))
|
||||
msg = check_legacy_batch_mode(cfg, legacy)
|
||||
assert msg is not None and "'B'" in msg and "'C'" in msg
|
||||
legacy.write_text(json.dumps({"mode": "auto", "model_mode": "C"}))
|
||||
assert check_legacy_batch_mode(cfg, legacy) is None
|
||||
assert check_legacy_batch_mode(cfg, tmp_path / "missing.json") is None
|
||||
|
||||
|
||||
def test_zone_polygons_and_legacy_knob_warning(tmp_path):
|
||||
zf = tmp_path / "zones.json"
|
||||
zf.write_text(json.dumps({
|
||||
"palet": [[0, 0]], "truck": [[1, 1]], "counting": [[2, 2]],
|
||||
"left_limit": 0.27, "right_limit": 0.72,
|
||||
"duplicate_circle_radius": 30, # legacy knob -> ignored + warned
|
||||
}))
|
||||
with pytest.warns(UserWarning, match="legacy knob"):
|
||||
zones = read_zone_polygons(zf)
|
||||
assert zones["palet"] == [[0, 0]]
|
||||
assert zones["left_limit"] == 0.27 # geometry stays in zones.json
|
||||
Reference in new issue
Block a user