Show only the bundle's name in the sensor configuration row

The row carried the origin, the calibration selection and the SDK's verdict, which a 30 px
line cannot explain; it is now the descriptor's display_name, else the folder name, and the
sensor dialog states the rest under the selector — name and folder, profile, shape, root,
verdict, calibration and every finding — with a suffix only where two rows would otherwise
read alike. Verified with the studio suite, the catalog tests in shared/tests, ui_check and
the lint gate.
This commit is contained in:
ykcheungab
2026-10-05 17:01:42 +08:00
parent 92b2f2fcd7
commit d11441cd79
7 changed files with 457 additions and 135 deletions
+183 -22
View File
@@ -6,19 +6,29 @@ twice with both sides named, and ``start_blocked_reason()`` must be the answer a
refuse to start on. The catalog itself is tested in ``shared/tests``; here it is a fixture tree
pointed at the widget, so the assertions are about the combo contents rather than the
filesystem.
What a row says is part of that contract: the row is the bundle's name and nothing else, so the
tests here also state what a row must *not* contain (the origin, the calibration phrasing, the
SDK's verdict, a ``profile_id``). Everything that left the row is asserted on the surface that
took it — the dialog's details label, rendered beside the same picker — because that is where an
operator reads it.
"""
from __future__ import annotations
import json
import re
from collections.abc import Callable
from pathlib import Path
import pytest
from orisys_app_shared.i18n import t
from orisys_app_shared.sensors import FAMILY_UNIDENTIFIED, scan_sensor_catalog
from orisys_studio.ui import sensor_picker as picker_module
from orisys_studio.ui.sensor_dialog import SensorDialog
from orisys_studio.ui.sensor_picker import SensorPickerWidget
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget
CONFIG = {
"name": "fixture",
@@ -36,8 +46,8 @@ def _write(path: Path, payload: object) -> None:
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
def _descriptor(profile_id: str) -> dict[str, object]:
return {
def _descriptor(profile_id: str, **overrides: object) -> dict[str, object]:
data: dict[str, object] = {
"schema_version": 1,
"profile_id": profile_id,
"display_name": "Mini lab",
@@ -45,6 +55,39 @@ def _descriptor(profile_id: str) -> dict[str, object]:
"processing_backend": "single_camera",
"sensor_json": "sensor.json",
}
data.update(overrides)
return data
class _Host(QWidget):
"""Stand-in for the window the dialog reads its calibration facts from.
``SensorDialog`` reaches the shell through ``getattr`` for everything but the "reveal the
config file" action, which it connects to while it is built — so a bare widget with that one
method is enough to render the details surface a test wants to read.
"""
def __init__(self) -> None:
super().__init__()
self.revealed = 0
def _on_reveal_config(self) -> None:
self.revealed += 1
def _details_dialog(picker) -> SensorDialog:
"""The real dialog, rendering the details surface for whatever the picker has selected.
Not a rendering of the dialog's strings: this is what the operator reads below the row, which
is the surface the row's dropped facts had to move into.
"""
host = _Host()
return SensorDialog(window=host, picker=picker)
def _details(picker) -> str:
"""The details surface as text, for the current pick."""
return _details_dialog(picker).profile_label.text()
def _make_picker(qt_app, tmp_path, monkeypatch, build: Callable[[Path], None]):
@@ -110,9 +153,7 @@ def test_a_shared_profile_id_is_listed_twice_and_can_be_refused(mixed_picker):
assert picker.config_combo.count() == 2
def test_a_single_valid_entry_is_listed_with_what_it_is_and_not_blocked(
qt_app, tmp_path, monkeypatch
):
def test_a_single_valid_entry_is_listed_by_its_name_alone(qt_app, tmp_path, monkeypatch):
def build(root: Path) -> None:
_write(root / "mini-lab" / "sensor.json", CONFIG)
_write(root / "mini-lab" / "manifest.json", _descriptor("orisys.mini.lab-r1"))
@@ -120,21 +161,19 @@ def test_a_single_valid_entry_is_listed_with_what_it_is_and_not_blocked(
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
assert picker.type_combo.findData("mini") >= 0
picker.set_sensor_type("mini")
label = picker.config_combo.itemText(0)
# What it is, where it came from, what it gives — and not the profile_id the operator
# cannot act on, nor the SDK's verdict word, which says nothing about the force.
assert label == "Mini lab · User · No calibration selected — raw force"
# The descriptor's display_name, with no origin, no calibration phrasing and no verdict.
assert picker.config_combo.itemText(0) == "Mini lab"
assert picker.start_blocked_reason() is None
entry = picker.selected_entry()
assert entry is not None and entry.profile_id == "orisys.mini.lab-r1"
# The identity the row dropped is one hover away.
# The identity and the shape the row dropped are on the picker's own item tooltip ...
tooltip = picker.config_combo.itemData(0, Qt.ItemDataRole.ToolTipRole)
assert "orisys.mini.lab-r1" in tooltip
assert "descriptor bundle" in tooltip
picker.deleteLater()
def test_no_valid_row_shows_internal_identity_or_a_severity_word(mixed_picker):
def test_no_row_shows_origin_calibration_identity_or_a_verdict_word(mixed_picker):
"""Every row of every family, not only the one the fixture is built around."""
picker = mixed_picker
seen = 0
@@ -142,20 +181,142 @@ def test_no_valid_row_shows_internal_identity_or_a_severity_word(mixed_picker):
picker.set_sensor_type(family)
for index in range(picker.config_combo.count()):
picker.config_combo.setCurrentIndex(index)
entry = picker.selected_entry()
if entry is None or not entry.valid:
continue
seen += 1
label = picker.config_combo.itemText(index)
assert entry.profile_id not in label, label
for word in ("ok", "degraded", "failed", "legacy"):
assert word not in label, f"{word!r} reached the row: {label}"
assert seen, "the fixture offered no valid entry, so nothing was checked"
row = picker.config_combo.itemText(index)
entry = picker.selected_entry()
assert entry is not None
# The row is the name itself, or the name plus a collision suffix: nothing else can
# have been appended to it.
assert row == entry.name or row.startswith(f"{entry.name} ("), row
if entry.profile_id:
assert entry.profile_id not in row, row
for word in (
"ok",
"degraded",
"failed",
"legacy",
"Shipped",
"Calibration",
"Cannot start",
"Also provided by",
):
assert re.search(rf"\b{re.escape(word)}\b", row) is None, (
f"{word!r} reached the row: {row}"
)
assert seen, "the fixture offered no entry, so nothing was checked"
def test_a_duplicate_row_names_its_twin(mixed_picker):
def test_a_name_two_entries_share_is_told_apart_without_hiding_either(mixed_picker):
"""``mini-a`` and ``mini-b`` declare one ``Mini lab``; one root, so the folder separates."""
picker = mixed_picker
picker.set_sensor_type("mini")
assert picker.config_combo.count() == 2, "one of the two was shadowed"
first, second = (picker.config_combo.itemText(i) for i in (0, 1))
assert first == "Mini lab · User · Also provided by user/mini-b"
assert second == "Mini lab · User · Also provided by user/mini-a"
assert first == "Mini lab (mini-a)"
assert second == "Mini lab (mini-b)"
def test_the_same_name_in_both_roots_is_told_apart_by_origin(qt_app, tmp_path, monkeypatch):
"""The operator's ``box`` beside the shipped one: two rows, each naming its root."""
shipped = tmp_path / "shipped"
user = tmp_path / "user"
config = dict(CONFIG, stitcher={"n": [1, 1]})
for root in (shipped, user):
_write(root / "box" / "sensor.json", config)
catalog = scan_sensor_catalog(shipped_root=shipped, user_root=user)
monkeypatch.setattr(picker_module, "scan_sensor_catalog", lambda: catalog)
picker = SensorPickerWidget()
picker.set_sensor_type("box")
rows = [
picker.config_combo.itemText(index)
for index in range(picker.config_combo.count())
]
assert rows == ["Box (shipped)", "Box (user)"], "an entry was collapsed into the other"
picker.deleteLater()
def test_the_details_carry_the_name_the_folder_the_profile_shape_state_and_root(
qt_app, tmp_path, monkeypatch
):
"""What left the row is readable below it, in sentences, without a hover."""
def build(root: Path) -> None:
_write(root / "mini-lab" / "sensor.json", CONFIG)
_write(root / "mini-lab" / "manifest.json", _descriptor("orisys.mini.lab-r1"))
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
picker.set_sensor_type("mini")
entry = picker.selected_entry()
assert entry is not None
details = _details(picker)
# The declared name and the folder it actually lives in, which the row may not distinguish.
assert t("sensor.catalog_name", name=entry.name, folder=entry.folder) in details
assert entry.folder in details
# The identity, how the bundle was read, and where it was found.
assert entry.profile_id in details
assert t("sensor.catalog_shape_descriptor") in details
assert t("sensor.catalog_root", origin=entry.origin, path=entry.bundle_path) in details
# The verdict with what it means, and the calibration as a selection.
assert t(
"sensor.catalog_state",
state=entry.state,
note=t("sensor.catalog_state_degraded"),
) in details
assert t("sensor.catalog_calibration_none") in details
# A warning is a sentence about the force, not the SDK's issue code.
assert "calibrated force is unavailable; raw force is available" in details
def test_the_details_of_an_unstartable_entry_say_cannot_start_with_the_sdk_reason(
qt_app, tmp_path, monkeypatch
):
def build(root: Path) -> None:
_write(root / "broken" / "sensor.json", CONFIG)
_write(
root / "broken" / "manifest.json",
_descriptor(
"orisys.finger.broken-r1",
display_name="Finger R01 (map missing)",
product_family="finger",
processing_backend="uv_unroll",
computational_assets={"unroll_map": "assets/not-here.npz"},
),
)
picker = _make_picker(qt_app, tmp_path, monkeypatch, build)
picker.set_sensor_type("finger")
entry = picker.selected_entry()
assert entry is not None and not entry.valid, entry.reason
# The row stays selectable and keeps the name; the refusal is stated below it.
assert picker.config_combo.itemText(0) == "Finger R01 (map missing)"
assert picker.config_combo.isEnabled()
assert picker.start_blocked_reason() == entry.start_blocked_reason
details = _details(picker)
assert t("sensor.catalog_blocked", reason=entry.start_blocked_reason) in details
assert "unroll_map" in details, "the SDK's own reason is not quoted"
def test_the_details_sit_directly_below_the_selector(mixed_picker):
"""The warning does not have to be in the row, because it is the next thing read."""
dialog = _details_dialog(mixed_picker)
root = dialog.layout()
assert root.itemAt(0).layout() is dialog.picker_row, "the selector is not the first row"
assert root.itemAt(1).widget() is dialog.profile_label, (
"the details are not directly under the selector"
)
def test_the_details_of_a_duplicate_name_the_twin(mixed_picker):
picker = mixed_picker
picker.set_sensor_type("mini")
for index, other_ref in ((0, "user/mini-b"), (1, "user/mini-a")):
picker.config_combo.setCurrentIndex(index)
entry = picker.selected_entry()
assert entry is not None and entry.duplicates == (other_ref,)
details = _details(picker)
assert t("sensor.catalog_duplicate", refs=other_ref) in details
# The row says nothing about it: the condition is unmissable below the selector.
row = picker.config_combo.itemText(index)
assert "Also provided by" not in row
assert other_ref not in row
+60 -38
View File
@@ -50,15 +50,16 @@ EXPECTED_SHIPPED: dict[str, tuple[str, str, str, bool, str]] = {
"mini": ("mini", "orisys.mini.legacy", "compound_eye", True, "degraded"),
}
#: What an operator reads in the picker, one row per shipped bundle: the product, where it came
#: from, and what the force numbers will be. ``box`` is the only shipped bundle that carries a
#: ``force_calibration.json``, so it is the only row naming a calibration; the other three are
#: stated as "no calibration selected — raw force" rather than as the SDK's ``degraded``.
EXPECTED_LABELS: dict[str, str] = {
"box": "Box · Shipped · Calibration: force_calibration.json",
"finger": "Finger R01 · Shipped · No calibration selected — raw force",
"finger-mems": "Finger (MEMS) · Shipped · No calibration selected — raw force",
"mini": "Mini · Shipped · No calibration selected — raw force",
#: What an operator reads in the picker, one row per shipped bundle: the bundle's name, and
#: nothing else. ``finger`` declares ``Finger R01`` in its descriptor; the other three have no
#: descriptor and are named by the folder they live in, as the SDK's built-in table words it.
#: The origin, the calibration selection and the SDK's ``degraded`` verdict are not rows — they
#: are read in the dialog's details surface, which sits under the selector.
EXPECTED_ROWS: dict[str, str] = {
"box": "Box",
"finger": "Finger R01",
"finger-mems": "Finger (MEMS)",
"mini": "Mini",
}
@@ -102,53 +103,74 @@ def test_the_shipped_families_keep_the_picker_order(shipped):
assert shipped.families() == ["box", "mini", "finger", "finger-mems"]
def test_every_shipped_row_states_what_it_is_where_from_and_what_it_gives(shipped):
"""The four real bundles, exactly as the picker renders them."""
for folder, expected in EXPECTED_LABELS.items():
def test_every_shipped_row_is_exactly_the_bundles_name(shipped):
"""The four real bundles, each as the picker renders it: the name, and nothing else."""
for folder, expected in EXPECTED_ROWS.items():
entry = shipped.by_ref(f"shipped/{folder}")
assert entry is not None, folder
assert entry.label() == expected, folder
assert shipped.row_label(entry) == expected, folder
# The name comes from the descriptor when there is one, else from the folder.
assert expected == (entry.display_name or entry.folder), folder
def test_no_shipped_row_carries_identity_severity_or_the_word_legacy(shipped):
def test_no_shipped_row_carries_origin_calibration_identity_or_a_verdict(shipped):
"""The row is the first thing an operator reads; none of these belong in it.
``legacy`` means "resolved through the compatibility adapter because it has no descriptor",
which is not what the word says to a reader — and the finger bundle's ``profile_id`` is not
even that shape, so the column mixed two kinds of string.
even that shape, so the column mixed two kinds of string. The origin, the calibration
phrasing and the SDK's verdict moved into the details surface under the selector.
"""
for entry in shipped.entries:
label = entry.label()
assert entry.profile_id not in label, label
for word in ("ok", "degraded", "failed", "legacy"):
assert word not in label, f"{word!r} reached the row: {label}"
row = shipped.row_label(entry)
assert entry.profile_id not in row, row
for word in (
"ok",
"degraded",
"failed",
"legacy",
"Shipped",
"User",
"Calibration",
"Cannot start",
"Also provided by",
):
assert word not in row, f"{word!r} reached the row: {row}"
def test_every_shipped_row_says_what_the_calibration_selection_is(shipped):
"""Calibration is a selection, so an absent one reads as raw force, not as a defect."""
def test_the_shipped_details_keep_what_the_rows_dropped(shipped):
"""Every fact that left the row is still readable, in sentences, one hover away."""
for entry in shipped.entries:
label = entry.label()
selected = entry.selected_calibration
if selected:
assert f"Calibration: {selected}" in label, label
else:
assert "No calibration selected — raw force" in label, label
# The SDK calls an absent calibration a warning and the bundle "degraded"; the row
# says what that means for the operator instead of repeating the verdict.
assert entry.state == "degraded", entry.ref
def test_the_shipped_tooltips_keep_the_facts_the_rows_dropped(shipped):
for entry in shipped.entries:
tooltip = entry.tooltip()
assert entry.profile_id in tooltip, entry.ref
assert f"SDK validation: {entry.state}" in tooltip, entry.ref
detail = entry.tooltip()
assert entry.profile_id in detail, entry.ref
assert entry.folder in detail, entry.ref
assert str(entry.bundle_path) in detail, entry.ref
assert f"SDK validation: {entry.state}" in detail, entry.ref
assert "something optional is missing" in detail, entry.ref
assert f"calibration selection: {entry.calibration_selection}" in detail, entry.ref
shape = (
"descriptor bundle"
if entry.profile_id == FINGER_PROFILE_ID
else "compatibility bundle, no descriptor"
)
assert shape in tooltip, entry.ref
assert shape in detail, entry.ref
def test_the_shipped_calibration_selection_is_stated_as_a_selection(shipped):
"""Calibration is a selection, so an absent one reads as raw force, not as a defect."""
for entry in shipped.entries:
selected = entry.selected_calibration
if selected:
assert entry.calibration_selection == selected, entry.ref
else:
assert entry.calibration_selection == "No calibration selected — raw force", (
entry.ref
)
# The SDK calls an absent calibration a warning and the bundle "degraded"; the
# details say what that means for the operator instead of repeating the verdict.
assert entry.state == "degraded", entry.ref
# ``box`` is the only shipped bundle that carries a ``force_calibration.json``.
assert shipped.by_ref("shipped/box").selected_calibration == "force_calibration.json"
def test_box_mini_and_finger_mems_are_legacy_sensor_json_bundles(shipped):