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:
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user