feat: 为每个 Sensor 绑定只读 SensorContext
构造时一次绑定 SN、配置、资产和当前生效的标定:只接受显式给出的 SN,不做猜测;标定路径只取当前生效的标定,清除后为 None;换标定时整体替换。帧身份复用 stream-contract 的 FrameIdentity。load_config 改为复用 resolve_config_source,使记录的配置来源与实际读取的文件一致,并补一致性测试。已发布 Sensor 数值和资产包解析不变。回归 275 项、lint 通过。
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""``resolve_config_source`` and ``load_config`` must agree on what is read.
|
||||
|
||||
``SensorContext.config_source`` promises the JSON file that was actually loaded.
|
||||
That promise only holds while the recorded lookup and the loading lookup are the
|
||||
same implementation, so this module checks it against every installed config
|
||||
name, the alias table, an explicit path and the legacy ``"name"`` field, and
|
||||
pins the preserved error types/messages for a missing or non-JSON input.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys import configs
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SENSORS = REPO_ROOT / "sensors"
|
||||
|
||||
|
||||
def _read(path: str | Path) -> dict:
|
||||
return json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
||||
|
||||
|
||||
def test_every_listed_name_resolves_to_the_file_load_config_reads() -> None:
|
||||
names = configs.list_config()
|
||||
assert names
|
||||
for name in names:
|
||||
source = configs.resolve_config_source(name)
|
||||
assert source is not None, name
|
||||
assert _read(source) == configs.load_config(name), name
|
||||
# The recorded source can be loaded directly with the same result.
|
||||
assert configs.load_config(source) == configs.load_config(name), name
|
||||
|
||||
|
||||
def test_aliases_resolve_like_loading() -> None:
|
||||
aliases = dict(configs.CONFIG_ALIASES)
|
||||
assert aliases
|
||||
for alias, canonical in aliases.items():
|
||||
source = configs.resolve_config_source(alias)
|
||||
assert source is not None, alias
|
||||
assert _read(source) == configs.load_config(alias), alias
|
||||
canonical_source = configs.resolve_config_source(canonical)
|
||||
if canonical_source is not None:
|
||||
assert source == canonical_source, alias
|
||||
|
||||
|
||||
def test_explicit_paths_resolve_to_themselves(tmp_path: Path) -> None:
|
||||
bundle_config = SENSORS / "box" / "sensor.json"
|
||||
assert configs.resolve_config_source(bundle_config) == bundle_config.resolve()
|
||||
assert configs.load_config(bundle_config) == configs.load_config("box")
|
||||
|
||||
copied = tmp_path / "copied-config.json"
|
||||
copied.write_text(bundle_config.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
assert configs.resolve_config_source(copied) == copied.resolve()
|
||||
assert configs.load_config(copied) == configs.load_config(bundle_config)
|
||||
|
||||
|
||||
def test_legacy_name_field_scan_agrees_with_loading() -> None:
|
||||
"""A config's JSON ``"name"`` may differ from its file stem (``prototype``)."""
|
||||
found = False
|
||||
for name in configs.list_config():
|
||||
source = configs.resolve_config_source(name)
|
||||
if source is None:
|
||||
continue
|
||||
field = _read(source).get("name")
|
||||
if (
|
||||
isinstance(field, str)
|
||||
and field
|
||||
and field not in configs.list_config()
|
||||
):
|
||||
resolved = configs.resolve_config_source(field)
|
||||
assert resolved is not None, field
|
||||
assert _read(resolved) == configs.load_config(field), field
|
||||
found = True
|
||||
assert found, "expected at least one legacy name field in the installed configs"
|
||||
|
||||
|
||||
def test_missing_and_non_json_inputs_keep_their_error_contract(tmp_path: Path) -> None:
|
||||
assert configs.resolve_config_source("no-such-config-name") is None
|
||||
with pytest.raises(FileNotFoundError, match="Input config file not found"):
|
||||
configs.load_config("no-such-config-name")
|
||||
|
||||
absolute_missing = tmp_path / "missing.json"
|
||||
assert configs.resolve_config_source(absolute_missing) is None
|
||||
with pytest.raises(FileNotFoundError, match="not found at absolute path"):
|
||||
configs.load_config(absolute_missing)
|
||||
|
||||
not_json = tmp_path / "config.txt"
|
||||
not_json.write_text("{}", encoding="utf-8")
|
||||
# The source lookup names the existing file; loading still refuses a non-JSON
|
||||
# file with the historical ValueError instead of silently reading it.
|
||||
assert configs.resolve_config_source(not_json) == not_json.resolve()
|
||||
with pytest.raises(ValueError, match="must be a JSON file"):
|
||||
configs.load_config(not_json)
|
||||
Reference in New Issue
Block a user