Files
scanengine-3/tests/test_smoke_apps.py
T
Thomas Ales 6e8c1cb7a2 Auto-align: level the sample on the DC bias levels from the camera window
The operator frames a good spot, confirms the two DC levels the detector
reads there, and the rig then measures its own tilt: step 1.5 mm either side
on X and then on Y, and tilt the platform until those levels come back.  The
correction that fixes an offset point is the correction that levels the whole
travel — height error and tilt effect are both proportional to the offset —
so the procedure ends by applying it and leaving it applied.

Both directions are measured from the same starting tilt and averaged, which
makes their disagreement a flatness read-out rather than something averaged
away silently.

core/auto_align.py holds the geometry and the search, Qt-free.  The three
T-axes' azimuths are the whole geometry: T1 lies along +X so it alone tilts
along X, and T0/T2 move as an equal-and-opposite pair to tilt along Y without
touching X (tilt_response derives that, and the tests pin it — an axis map
that drifts would still converge, on the wrong axis).  The search is a secant
null on the split-detector difference: probe once to learn what a microstep
is worth, sign included, then step at the null.  It refuses to servo on a
scope that has not re-triggered, escalates a probe that reads as no response
before calling an axis dead, and stops at a per-axis travel limit.

gui/align_bridge.py runs it on a worker thread; stopping is a threading.Event
rather than a queued command, because the worker is inside a long handler for
the whole run.  The camera window carries the button and the progress window,
and locks the scan panel and the jog pads while a run owns the stage.

Adds immediate MEAN measurements and an acquisition count to the scope
driver, and read_bias_mv to core/scope_inspect — the one scalar the
inspection state was missing.

KNOWN_ISSUES.md records what only the rig can settle: the probe step, the
travel limit, the hold current, and whether the piston the X phase applies
alongside its tilt matters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:00:36 -05:00

156 lines
3.9 KiB
Python

"""Offscreen smoke tests: every GUI app must construct without hardware.
These don't exercise behavior — they catch import errors, missing .ui
widgets, and constructor regressions during the refactor.
"""
import pytest
from PyQt6.QtWidgets import QApplication
@pytest.fixture(scope="session")
def qapp():
app = QApplication.instance() or QApplication([])
yield app
def _pump(qapp):
qapp.processEvents()
def test_sc3_aui_main_window(qapp):
import sc3_aui_app
win = sc3_aui_app.MainWindow()
_pump(qapp)
try:
assert win.x_start_edit.text()
assert win.windowTitle() == "Scanengine-3 AUI"
finally:
for worker, thread in (
(win._bbd_worker, win._bbd_thread),
(win._oscope_worker, win._oscope_thread),
(win._helios_worker, win._helios_thread),
):
worker.stop_worker()
thread.quit()
assert thread.wait(2000)
win._camera_win.deleteLater()
win.deleteLater()
_pump(qapp)
def test_camera_window_without_hardware_is_a_plain_viewer(qapp):
"""No stage, no scope, no auto-align button to press."""
import sc3_aui_app
win = sc3_aui_app.CameraWindow()
_pump(qapp)
try:
assert not win.uc480_auto_align_btn.isVisible()
finally:
win.deleteLater()
_pump(qapp)
def test_camera_window_auto_align_needs_every_device(qapp):
"""The button appears once the three devices exist, and says which one is
missing rather than starting and failing on the rig."""
import sc3_aui_app
from gui.qt_t3r import QtT3RAdapter
win = sc3_aui_app.CameraWindow(QtT3RAdapter(), sc3_aui_app.BBD202Worker(),
sc3_aui_app.OscopeWorker())
_pump(qapp)
try:
assert win.uc480_auto_align_btn.isVisibleTo(win)
assert "oscilloscope" in win._align_prerequisite_problem()
finally:
win.deleteLater()
_pump(qapp)
def test_auto_align_window_logs_a_result(qapp):
import sc3_aui_app
from core.auto_align import AlignResult, Reading
win = sc3_aui_app.AutoAlignWindow()
_pump(qapp)
try:
reference = Reading(400.0, 400.0)
win.set_reference(reference)
win.on_reading(Reading(403.0, 397.0))
win.on_finished(AlignResult(reference=reference, final=reference))
assert "Reference" in win.log.toPlainText()
assert win.close_btn.isEnabled()
finally:
win.deleteLater()
_pump(qapp)
def test_sras_viewer_window(qapp):
import sras_viewer
win = sras_viewer.SrasViewerWindow()
_pump(qapp)
try:
assert win.windowTitle()
finally:
win.deleteLater()
_pump(qapp)
def test_saw_check_viewer_window(qapp):
import saw_check_viewer
win = saw_check_viewer.SawCheckWindow()
_pump(qapp)
try:
assert win.windowTitle()
finally:
win.deleteLater()
_pump(qapp)
def test_helios_test_app(qapp):
import helios_test_app
win = helios_test_app.HeliosTestApp()
_pump(qapp)
try:
assert win.windowTitle()
finally:
win.deleteLater()
_pump(qapp)
def test_bbd202_test_app(qapp):
import bbd202_test_app
win = bbd202_test_app.BBD202TestApp()
_pump(qapp)
try:
assert win.windowTitle()
finally:
win.close()
_pump(qapp)
def test_camera_test_app(qapp):
import camera_test_app
win = camera_test_app.CameraTestWindow()
_pump(qapp)
try:
assert win.windowTitle()
finally:
win.deleteLater()
_pump(qapp)
def test_t3r_control_panel(qapp):
from hardware.t3r_driver import T3RDriver
from t3r_control_panel import T3RControlPanel
driver = T3RDriver()
panel = T3RControlPanel(driver)
_pump(qapp)
try:
assert panel.windowTitle()
finally:
panel.deleteLater()
_pump(qapp)
def test_sras_scan_manager_importable():
import sras_scan_manager # noqa: F401