拆分收尾:标定归属规则移入 CalibrationService(app_window 2340→2304 行)

- What: `apps/studio/src/orisys_studio/calibration_service.py` 新增三条**无状态**规则,
  它们原来长在窗口里:`identity_of(path)`(这份系数文件声称属于哪台设备——用 SDK 自己的
  解析器读,与设备将要加载的是同一份文档;读不出来就什么都不声称)、`same_path(a, b)`
  (同一个配置路径的不同写法不算不一致;两个空值也不算)、
  `target_mismatch(path, *, sensor_type, config_ref)`(在确认框之前就说清楚"这份标定
  看起来不属于这台设备"的理由)。服务本来就是这个文件工作的所有者,规则回到了它这里,
  并且不需要窗口就能测。
- What: 窗口保留 `calibration_identity` / `calibration_target_mismatch` 两个转发
  (tests 与 tools/ui_check.py 通过窗口驱动它们),删除 `_same_path`。
  `_on_calibration_apply` 仍留在窗口:它要弹确认框、要把路径交给采集 worker,那是交互
  与会话,不是关于文件的规则;`_calibration_summary`、`_drain_calibration_events`、
  `_drain_reset_events`、`_on_calibration_point/cancelled/exported` 同理(面板与状态栏
  的胶水)。
- What: 新增 apps/studio/tests/test_calibration_service.py:4 个用例——身份读出
  sensor_type/sn/config_ref、读不出来的文件既无身份也无"不匹配"指控(不能把"不知道"
  当成"不一致")、另一台设备/另一份配置会被逐条报出而同一台不会、同一路径的不同写法
  不算不一致而非路径值算。
- Why: 这是 app_window 拆分计划里最后一块「有独立规则可搬」的部分。搬完之后
  "这份标定是谁的"只有一处实现,窗口只剩对话与转发。
- Verification: pytest shared/tests apps/studio/tests(offscreen)636 passed / 0 failed /
  15 skipped(原 632 + 新增 4);apps/studio/tools/ui_check.py 587 项检查 0 失败;
  .repro/locale_check.py 0 缺失。行为等价(同一份实现 + 一层转发)。
- Not in this change: 预览(_start_preview/_stop_preview/_on_preview_tick)与 2D 渲染
  (_render_run_image/_show_image_frame/_depth_range)仍留在窗口——它们是面板与相机的
  胶水,不属于任何一个已拆出的变更驱动;已知缺陷(本次未修,另行处理):worker 的
  on_reset_applied 从未接到 recording.note_reset,真机上按"清零"不会记录确认事件。
This commit is contained in:
ykcheungab
2026-09-15 09:53:47 +08:00
parent 0a42a452dd
commit c62452ec4b
3 changed files with 155 additions and 46 deletions
@@ -0,0 +1,81 @@
"""Acceptance for the calibration service's rules about *whose* a file is.
The window used to own these: reading a coefficient file's claimed identity and deciding
whether it obviously belongs to the device on the bench. They are pure rules about files,
so they now live with the rest of the calibration file work — and can be tested without
building a window. The end-to-end apply path is still covered by ``test_ui_smoke``.
"""
from __future__ import annotations
import json
from pathlib import Path
from orisys_app_shared.force.calibration_io import (
CalSample,
CalSessionStore,
build_export_model,
)
from orisys_studio.calibration_service import CalibrationService
def _calibration_file(directory: Path, *, sensor_type: str, config_ref: str) -> Path:
"""A coefficient document that names the device it was measured on."""
store = CalSessionStore()
store.aggregate("fn", CalSample(weight_n=0.0, raw_mean=0.0, n=10))
store.aggregate("fn", CalSample(weight_n=5.0, raw_mean=500.0, n=10))
model = build_export_model({key: store.channel(key) for key in ("fn", "fx", "fy")})
document = model.to_dict()
document["identity"] = {"sensor_type": sensor_type, "sn": "SN-A"}
document["pipeline"] = {"config_ref": config_ref}
path = directory / f"{sensor_type}_calibration.json"
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_the_identity_names_the_sensor_and_the_configuration(tmp_path):
path = _calibration_file(tmp_path, sensor_type="box", config_ref="D:/lab/a/sensor.json")
identity = CalibrationService.identity_of(path)
assert identity["sensor_type"] == "box"
assert identity["sn"] == "SN-A"
assert identity["config_ref"] == "D:/lab/a/sensor.json"
def test_a_file_that_cannot_be_read_claims_nothing(tmp_path):
"""No identity must mean "no mismatch claim", not "mismatch"."""
broken = tmp_path / "not-a-calibration.json"
broken.write_text("this is not a coefficient document\n", encoding="utf-8")
assert CalibrationService.identity_of(broken) == {}
assert CalibrationService.target_mismatch(
broken, sensor_type="finger", config_ref="D:/lab/b/sensor.json"
) == []
def test_another_devices_calibration_is_reported_with_its_reasons(tmp_path):
path = _calibration_file(tmp_path, sensor_type="box", config_ref="D:/lab/a/sensor.json")
reasons = CalibrationService.target_mismatch(
path, sensor_type="finger", config_ref="D:/lab/b/sensor.json"
)
assert any("sensor_type" in reason for reason in reasons), reasons
assert any("config_ref" in reason for reason in reasons), reasons
# The same device and configuration is not a mismatch.
assert (
CalibrationService.target_mismatch(
path, sensor_type="box", config_ref="D:/lab/a/sensor.json"
)
== []
)
def test_the_same_configuration_named_differently_is_not_a_mismatch(tmp_path):
"""A path is compared as a path: the apply path must not cry wolf over spelling."""
assert CalibrationService.same_path("", "D:/lab/a/sensor.json") is True
assert CalibrationService.same_path("D:/lab/a/sensor.json", "") is True
assert CalibrationService.same_path("D:/lab/a/sensor.json", "D:/lab/./a/sensor.json")
# ...but it does have to be a path: a non-path value simply differs.
assert CalibrationService.same_path("D:/lab/a/sensor.json", "not a path\x00") is False