"""Smoke tests for Sensor Bench SDK acquisition and UI construction.""" from __future__ import annotations import os import sys import time from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock, patch import numpy as np ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") def test_load_fixed_mode() -> None: from sensor_bench.acquisition import load_fixed_mode mode = load_fixed_mode(ROOT / "tests" / "fixtures" / "ddjx01_min.json") assert mode.width == 1920 assert mode.height == 1080 assert mode.fps == 30.0 assert mode.stitch_width == 365 assert "1920x1080" in mode.label() def test_translator_chinese_default() -> None: from sensor_bench.i18n import DEFAULT_LOCALE, Translator from sensor_bench.paths import default_locales_dir assert DEFAULT_LOCALE == "zh_CN" tr = Translator(default_locales_dir()) assert tr.locale == "zh_CN" assert "传感器" in tr.t("app.title") or "展示" in tr.t("exhibition.title") tr.set_locale("en_US") assert tr.t("app.title") == "Sensor Bench" assert "Exhibition" in tr.t("exhibition.title") # Public locales must not ship internal inspection workflow copy. assert "workflow" not in (tr._catalogs.get("en_US") or {}) assert tr._lookup("en_US", "workflow.title") is None def test_read_forces_from_sensor() -> None: from sensor_bench.acquisition import read_forces_from_sensor, sensor_force_calibrated class RawSensor: info = SimpleNamespace( FNORMAL=1, FSHEARX=2, FSHEARY=3, FNORMAL_RAW=4, FSHEARX_RAW=5, FSHEARY_RAW=6, VRAW=7, ) _force_cal_sensor_name = "box" def read_info(self, *args): return 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, None forces = read_forces_from_sensor(RawSensor()) assert sensor_force_calibrated(RawSensor()) assert forces.force_calibrated is True assert forces.force_unit == "N" assert forces.normal_force == 1.1 assert forces.normal_force_raw == 4.4 class LegacySensor: info = SimpleNamespace(FNORMAL=1, FSHEARX=2, FSHEARY=3, VRAW=4) def read_info(self, *args): if len(args) == 4: return 10.0, 20.0, 30.0, None return 10.0, 20.0, 30.0 legacy = read_forces_from_sensor(LegacySensor()) assert legacy.force_calibrated is False assert legacy.force_unit == "raw" assert legacy.normal_force_raw == 10.0 def test_sdk_worker_packet_mapping() -> None: from sensor_bench.acquisition import SdkAcquisitionWorker raw = np.zeros((1080, 1920, 3), dtype=np.uint8) processed = np.full((365, 365, 3), 40, dtype=np.uint8) class FakeSensor: def __init__(self, *args, **kwargs): self.frame = raw.copy() self.info = SimpleNamespace(FNORMAL=1, FSHEARX=2, FSHEARY=3) def get_img(self): self.frame = raw.copy() return processed.copy() def compute_deformation(self, **kwargs): return True def compute_contact(self): return True def read_info(self, *args): return 1.0, 2.0, 3.0 def configure(self, **kwargs): return self def disconnect(self): pass fake_orisys = MagicMock() fake_orisys.Sensor = FakeSensor fake_orisys.__version__ = "test" worker = SdkAcquisitionWorker() with patch.dict(sys.modules, {"orisys": fake_orisys}): with patch("sensor_bench.acquisition.orisys_importable", return_value=(True, "test")): mode = worker.start( 0, config_path=ROOT / "tests" / "fixtures" / "ddjx01_min.json", backend="cpu", ) assert mode.width == 1920 deadline = time.time() + 2.0 packet = None while time.time() < deadline: packet = worker.latest() if packet is not None: break time.sleep(0.02) worker.stop() assert packet is not None assert packet.width == 1920 assert packet.height == 1080 assert packet.processed_width == 365 assert packet.processed_height == 365 assert packet.normal_force == 1.0 assert packet.shear_x == 2.0 assert packet.shear_y == 3.0 assert packet.normal_force_raw == 1.0 assert packet.force_unit == "raw" assert packet.force_calibrated is False assert packet.raw_frame.shape[:2] == (1080, 1920) assert packet.processed_frame.shape[:2] == (365, 365) def test_force_calibration_and_plot_min_span() -> None: from sensor_bench.force_calibration import ( NORMAL_FORCE_CAL_OFFSET, Y_AXIS_MIN_SPAN, calibrate_normal_force, ) from sensor_bench.plot_history import MetricHistory assert abs(calibrate_normal_force(0.0) - NORMAL_FORCE_CAL_OFFSET) < 1e-12 hist = MetricHistory( npoints=8, channels=3, window_sec=1.0, sample_rate_hz=30.0, lowpass_cutoff_hz=0.0, y_axis_min_span=Y_AXIS_MIN_SPAN, y_range_alpha=0.5, ) for i in range(5): hist.push(float(i) / 30.0, 100.0, 100.0, 100.0) state = hist.state() assert state is not None for ymin, ymax in state.y_ranges: assert (ymax - ymin) >= Y_AXIS_MIN_SPAN * 0.99 def test_list_orisys_configs_excludes_options() -> None: from sensor_bench.paths import ( config_display_name, list_local_sensor_bundle_names, list_local_sensor_config_files, list_orisys_configs, local_sensor_configs_dir, ) entries = list_orisys_configs() labels = [config_display_name(e).lower() for e in entries] assert "options" not in labels assert all(Path(e).stem.lower() != "options" for e in entries if Path(e).suffix.lower() == ".json") bundles = list_local_sensor_bundle_names() if bundles: assert labels[0] in bundles local_ddjx = local_sensor_configs_dir() / "ddjx01.json" if local_ddjx.exists(): local_files = list_local_sensor_config_files() assert any(p.name == "ddjx01.json" for p in local_files) if "box" not in bundles: assert any(config_display_name(e).lower() == "ddjx01" for e in entries) else: assert not any(config_display_name(e).lower() == "ddjx01" for e in entries) def test_default_config_prefers_box_bundle() -> None: from sensor_bench.paths import ( config_display_name, default_config_name, default_ddjx01_config, list_local_sensor_bundle_names, ) bundles = list_local_sensor_bundle_names() if not bundles: return assert default_config_name("orisys") == "box" default = default_ddjx01_config() assert config_display_name(default).lower() == "box" def test_legacy_config_force_calibration_fallback() -> None: from sensor_bench.acquisition import sensor_force_calibrated from sensor_bench.paths import ( bundle_has_force_calibration, local_sensor_configs_dir, resolve_force_bundle_from_config, ) if not bundle_has_force_calibration("box"): return class LegacySensor: pass legacy_path = local_sensor_configs_dir() / "ddjx01.json" if legacy_path.exists(): assert resolve_force_bundle_from_config(legacy_path) == "box" assert sensor_force_calibrated(LegacySensor(), config_ref=legacy_path) is True assert resolve_force_bundle_from_config("ddjx01") == "box" assert resolve_force_bundle_from_config("prototype") == "box" assert sensor_force_calibrated(LegacySensor(), config_ref="ddjx01") is True def test_normalize_sensor_config_ref_prefers_bundle() -> None: from sensor_bench.paths import ( bundle_has_force_calibration, local_sensor_bundle_sensor_json, normalize_sensor_config_ref, ) if not bundle_has_force_calibration("box"): return local_json = local_sensor_bundle_sensor_json("box") assert local_json is not None normalized = normalize_sensor_config_ref("ddjx01") assert Path(normalized).resolve() == local_json.resolve() assert normalize_sensor_config_ref("box") == str(local_json.resolve()) def test_roi_geometry_helpers() -> None: from sensor_bench.roi_geometry import build_roi_warp, order_quad_points, warp_roi_to_rectangle import numpy as np points = np.array([[10, 10], [100, 12], [98, 90], [12, 88]], dtype=np.float32) ordered = order_quad_points(points) assert ordered.shape == (4, 2) matrix, src, size = build_roi_warp(points, (120, 100), (120, 100), (64, 64)) assert size == (64, 64) assert src.shape == (4, 2) frame = np.zeros((100, 120, 3), dtype=np.uint8) out = warp_roi_to_rectangle(frame, matrix, size) assert out.shape == (64, 64, 3) def test_exhibition_window_offscreen() -> None: from PyQt5.QtWidgets import QApplication from sensor_bench.exhibition_app import ExhibitionWindow from sensor_bench.i18n import DEFAULT_LOCALE app = QApplication.instance() or QApplication([]) win = ExhibitionWindow() assert win.tr.locale == DEFAULT_LOCALE == "zh_CN" assert "展示" in win.windowTitle() or "Sensor Bench" in win.windowTitle() assert win.btn_start is not None assert win.btn_stop is not None assert win.btn_reset is not None assert win.btn_record is not None assert win.btn_search is not None assert win.btn_camera is not None assert not hasattr(win, "btn_cal") assert not hasattr(win, "cal_edit") assert win.btn_reset.text() == "重置" assert win.image_label is not None assert win.log_widget is not None assert not win.show_log_check.isChecked() assert win.log_widget.isHidden() assert win.show_log_check.text() == "显示日志面板" win.show_log_check.setChecked(True) assert win._log_panel_visible assert not win.log_widget.isHidden() win.show_log_check.setChecked(False) assert win.log_widget.isHidden() assert not win._log_panel_visible assert len(win.plots) == 3 assert len(win.curves) == 3 assert win.btn_start.text() == "启动" assert not win.btn_stop.isEnabled() assert win._view_mode == "pure_color" assert win.view_combo.currentData() == "pure_color" assert win.about_action is not None assert win.help_menu is not None assert win.sensor_combo.count() == 2 assert win.config_combo is not None assert win.config_combo.count() >= 1 from sensor_bench.paths import config_display_name, default_ddjx01_config, list_orisys_configs installed = list_orisys_configs() default = default_ddjx01_config() if config_display_name(default).lower() == "box": assert config_display_name(win._selected_config()).lower() == "box" elif "ddjx01" in installed: assert win.config_combo.currentData() == "ddjx01" or win._selected_config() == "ddjx01" win.language_combo.setCurrentIndex(1) assert win.tr.locale == "en_US" assert "Exhibition" in win.windowTitle() assert win.btn_start.text() == "Start" assert win.btn_reset.text() == "Reset" assert win.show_log_check.text() == "Show log panel" assert "About" in win.about_action.text() title0 = win.plots[0].titleLabel.text assert "FN" in title0 assert "raw" in title0 from sensor_bench.force_calibration import ( CALIBRATED_NORMAL_FORCE_Y_MAX, CALIBRATED_NORMAL_FORCE_Y_MIN, CALIBRATED_SHEAR_FORCE_Y_MAX, CALIBRATED_SHEAR_FORCE_Y_MIN, ) win._force_calibrated = True win.force_history.push(time.time(), 1.0, -2.0, 3.0) win._update_plots() fn_ymin, fn_ymax = win.plots[0].viewRange()[1] assert abs(fn_ymin - CALIBRATED_NORMAL_FORCE_Y_MIN) < 1e-6 assert abs(fn_ymax - CALIBRATED_NORMAL_FORCE_Y_MAX) < 1e-6 fx_ymin, fx_ymax = win.plots[1].viewRange()[1] assert abs(fx_ymin - CALIBRATED_SHEAR_FORCE_Y_MIN) < 1e-6 assert abs(fx_ymax - CALIBRATED_SHEAR_FORCE_Y_MAX) < 1e-6 axis = win.plots[0].getAxis("left") assert "20" in axis.tickStrings([20.0], 1.0, 20.0) win.view_combo.setCurrentIndex(1) assert win._view_mode == "captured" win.sensor_combo.setCurrentIndex(1) assert win._sensor_type == "dd06" selected = str(win._selected_config()).lower() assert "dd06" in selected or selected.endswith(".json") win.close() if __name__ == "__main__": test_load_fixed_mode() test_translator_chinese_default() test_read_forces_from_sensor() test_sdk_worker_packet_mapping() test_force_calibration_and_plot_min_span() test_list_orisys_configs_excludes_options() test_default_config_prefers_box_bundle() test_legacy_config_force_calibration_fallback() test_normalize_sensor_config_ref_prefers_bundle() test_roi_geometry_helpers() test_exhibition_window_offscreen() print("ok")