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
+15
View File
@@ -10,6 +10,21 @@ second opinion computed in the UI.
identity and its files instead of leaving both to be inferred, which is what lets the bundle keep
its meaning when the folder is moved or renamed.
## Picker rows
A configuration row in the sensor dialog is **the bundle's name and nothing else**: the
descriptor's `display_name` when there is one, otherwise the folder name (`box`, `mini`, `finger`,
`finger-mems`, and whatever the operator called their own folder). Everything else — the origin,
the resolved root, the `profile_id`, the bundle shape, the SDK's verdict with what it means, the
calibration selection and every finding — is written in the details surface directly under the
selector, where the sentences fit.
One exception: two entries offered together that would otherwise read alike each get a suffix.
Across roots that is the origin (`Box (shipped)` beside `Box (user)`); when one root supplies both
(for example one `display_name` declared by two folders) the origin cannot separate them and the
folder is used instead. Nothing is collapsed or shadowed, and the same tree always produces the
same rows.
## The finger descriptor
| Field | Value | Why |
@@ -22,6 +22,7 @@ from orisys_app_shared.sensors import (
SHAPE_COMPATIBILITY,
SHAPE_DESCRIPTOR,
SHAPE_UNREADABLE,
STATE_UNREADABLE,
)
from PySide6.QtWidgets import (
QDialog,
@@ -42,6 +43,16 @@ _SHAPE_KEYS: dict[str, str] = {
SHAPE_UNREADABLE: "sensor.catalog_shape_unreadable",
}
#: The SDK's verdicts, and the catalog's own "nothing could be read here", as the sentence that
#: says what the verdict means for the operator. Same rule as the shapes: a literal key per state,
#: and an unknown state falls back to words rather than to the identifier the SDK happened to use.
_STATE_KEYS: dict[str, str] = {
"ok": "sensor.catalog_state_ok",
"degraded": "sensor.catalog_state_degraded",
"failed": "sensor.catalog_state_failed",
STATE_UNREADABLE: "sensor.catalog_state_invalid",
}
class SensorDialog(QDialog):
"""Sensor type / config picker plus the calibration facts that follow it."""
@@ -62,6 +73,17 @@ class SensorDialog(QDialog):
self.picker_row.addStretch(1)
root.addLayout(self.picker_row)
#: Everything behind the selected configuration, said in full: the bundle's own name and
#: the folder it lives in, the profile the SDK assigned, how the bundle was read, the
#: root it came from, the verdict with what it means, the calibration selection and every
#: finding — plus, when it must not be started, why. It sits directly under the selector
#: on purpose: the picker row carries the name alone, so this is the surface the rest has
#: to be readable on, without a hover and without hunting for it.
self.profile_label = ShrinkableLabel()
self.profile_label.setObjectName("panelMeta")
self.profile_label.setWordWrap(True)
root.addWidget(self.profile_label)
root.addWidget(self._section_rule())
self.calib_title = QLabel()
self.calib_title.setObjectName("panelTitle")
@@ -72,12 +94,6 @@ class SensorDialog(QDialog):
self.axes_label.setObjectName("panelMeta")
self.config_label = ShrinkableLabel()
self.config_label.setObjectName("panelMeta")
#: What the catalog says the selected configuration *is*: family, profile, origin and
#: the SDK's verdict — plus, when it must not be started, why. The picker can only fit
#: a label on a 30 px rail; the reason needs this room.
self.profile_label = ShrinkableLabel()
self.profile_label.setObjectName("panelMeta")
self.profile_label.setWordWrap(True)
for label in (self.version_label, self.axes_label):
root.addWidget(label)
config_row = QHBoxLayout()
@@ -88,7 +104,6 @@ class SensorDialog(QDialog):
config_row.addWidget(self.btn_reveal)
config_row.addStretch(1)
root.addLayout(config_row)
root.addWidget(self.profile_label)
root.addWidget(self._section_rule())
self.export_title = QLabel()
@@ -203,10 +218,11 @@ class SensorDialog(QDialog):
def _refresh_profile(self) -> None:
"""State the selected catalog entry, and the reason when it cannot be started.
This is the surface that keeps the facts the picker row no longer carries: the identity
the SDK assigned, how the bundle was read and its validation. It is read from the picker
rather than re-derived here, so the window and the dialog can never say two different
things about one bundle.
This is the surface that keeps the facts the picker row no longer carries: the name and
the folder, the identity the SDK assigned, how the bundle was read, the root it was found
in, the verdict with what it means, the calibration selection, and every finding as a
sentence. It is read from the picker rather than re-derived here, so the window and the
dialog can never say two different things about one bundle.
"""
read_entry = getattr(self.picker, "selected_entry", None)
entry = read_entry() if callable(read_entry) else None
@@ -214,19 +230,40 @@ class SensorDialog(QDialog):
self.profile_label.setText("")
self.profile_label.setToolTip("")
return
line = t(
"sensor.catalog_profile",
profile_id=entry.profile_id or entry.folder,
shape=t(_SHAPE_KEYS.get(entry.shape, _SHAPE_KEYS[SHAPE_UNREADABLE])),
origin=entry.origin,
state=entry.state,
)
lines = [
t("sensor.catalog_name", name=entry.name, folder=entry.folder),
t(
"sensor.catalog_profile",
profile_id=entry.profile_id or entry.folder,
shape=t(_SHAPE_KEYS.get(entry.shape, _SHAPE_KEYS[SHAPE_UNREADABLE])),
origin=entry.origin,
),
t("sensor.catalog_root", origin=entry.origin, path=entry.bundle_path),
t(
"sensor.catalog_state",
state=entry.state,
note=t(_STATE_KEYS.get(entry.state, "sensor.catalog_state_unknown")),
),
(
t("sensor.catalog_calibration_selected", name=entry.selected_calibration)
if entry.selected_calibration
else t("sensor.catalog_calibration_none")
),
]
if entry.duplicates:
lines.append(t("sensor.catalog_duplicate", refs=", ".join(entry.duplicates)))
blocked = entry.start_blocked_reason
if blocked:
# In the label, not only the tooltip: a refused configuration has to say why without
# being hovered over, and this is the surface that has room for the sentence.
line = f"{line}\n{t('sensor.catalog_blocked', reason=blocked)}"
self.profile_label.setText(line)
lines.append(t("sensor.catalog_blocked", reason=blocked))
lines.extend(
t("sensor.catalog_error", sentence=text) for text in entry.error_sentences()
)
lines.extend(
t("sensor.catalog_warning", sentence=text) for text in entry.warning_sentences()
)
self.profile_label.setText("\n".join(lines))
self.profile_label.setToolTip(entry.tooltip())
def retranslate(self) -> None:
@@ -1,18 +1,22 @@
"""Sensor type + config picker.
One row, because it lives at the left of the status rail: a badge naming the
selected product, then the product/sensor choice and the config file that
belongs to it. The order matters — picking ``box`` / ``mini`` / ``finger`` is
what decides which configs exist, so the two sit next to each other with their
own labels and tooltips instead of a bare "配置" combo.
selected product, then the product/sensor choice and the configuration bundle
that belongs to it. The order matters — picking ``box`` / ``mini`` / ``finger``
is what decides which configurations exist, so the two sit next to each other
with their own labels and tooltips instead of a bare "配置" combo.
Both combos are filled from the configuration catalog
(:mod:`orisys_app_shared.sensors`), not from a hard-coded product list: the type
combo offers the families bundles actually exist for, and the config combo offers
the catalog's entries for the chosen family — each labelled with what it is, where
it came from and what the calibration selection gives, so the choice is between
products and outcomes rather than between ``profile_id`` strings. An entry the SDK
rejects stays listed, because that is how its reason reaches the operator, and
the catalog's entries for the chosen family. A row is the **bundle's name** — the
descriptor's ``display_name``, else the folder name — and nothing else; the origin,
the calibration selection and the SDK's verdict are read in the dialog, whose
details surface sits directly below this row. The one exception is a collision: two
entries offered together that would otherwise read identically (``shipped/box``
beside ``sensors-user/box``) each carry the suffix that tells them apart, so no
entry is collapsed or shadowed. An entry the SDK rejects stays listed too, because
that is how its reason reaches the operator, and
:meth:`SensorPickerWidget.start_blocked_reason` states why the current selection
must not be started (see also :meth:`selected_entry`).
"""
@@ -186,7 +190,7 @@ class SensorPickerWidget(QWidget):
self.config_combo.blockSignals(True)
self.config_combo.clear()
for entry in entries:
self.config_combo.addItem(_entry_label(entry), entry.key)
self.config_combo.addItem(_entry_label(self._catalog, entry), entry.key)
self.config_combo.setItemData(
self.config_combo.count() - 1,
entry.tooltip(),
@@ -238,12 +242,14 @@ def _family_label(family: str) -> str:
return t("sensor.family_unidentified") if family == FAMILY_UNIDENTIFIED else family
def _entry_label(entry: CatalogEntry) -> str:
"""One combo item: the catalog's operator label, with nothing internal in it.
def _entry_label(catalog: SensorCatalog, entry: CatalogEntry) -> str:
"""One combo item: the catalog's row for an entry, with nothing internal in it.
The catalog names an entry by its display name, so the internal marker for "no product
family" cannot reach the row today; it is still substituted here because a sentinel is
never something an operator should read, and the item text is the one place it could
surface if an entry ever had to fall back to its family.
The row is the bundle's name, and the catalog appends the origin only where two visible
entries would otherwise read alike, so the internal marker for "no product family" cannot
reach it today; it is still substituted here, where translating belongs, because a sentinel
is never something an operator should read and the item text is the one place it could
surface.
"""
return entry.label().replace(FAMILY_UNIDENTIFIED, t("sensor.family_unidentified"))
row = catalog.row_label(entry)
return row.replace(FAMILY_UNIDENTIFIED, t("sensor.family_unidentified"))
+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):