Phase 0: test scaffolding + golden fixtures
- ruff config, offscreen smoke tests for all 7 GUI apps/panels - golden v6 .sras fixtures (complete + 4 truncation variants) generated by the pre-refactor writer, with expected header/frontier JSON - golden geometry fixtures from the pre-refactor _build_scan_params - consistency tests proving current code reproduces the goldens Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
target-version = "py311"
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
# F: pyflakes (unused imports/variables, undefined names)
|
||||||
|
# E7/E9: comparison and runtime-error prone constructs
|
||||||
|
# B: bugbear (mutable defaults, useless expressions)
|
||||||
|
select = ["F", "E7", "E9", "B"]
|
||||||
|
ignore = [
|
||||||
|
"E731", # lambda assignment — used deliberately for short Qt slot glue
|
||||||
|
"E741", # ambiguous single-letter names — used in math-heavy geometry code
|
||||||
|
]
|
||||||
|
|
||||||
|
[lint.per-file-ignores]
|
||||||
|
# Wildcard re-exports are part of this package's (legacy) public surface
|
||||||
|
# until Phase 1 empties it.
|
||||||
|
"hardware/__init__.py" = ["F401", "F403"]
|
||||||
|
"hardware/pybbd202/__init__.py" = ["F401", "F403"]
|
||||||
|
"scanning/__init__.py" = ["F401", "F403"]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Shared test setup: repo-root imports, headless Qt, pyueye stub.
|
||||||
|
|
||||||
|
The IDS uEye SDK (pyueye + libueye) only exists on the Linux rig. On any
|
||||||
|
other machine we stub the module before hardware.uc480_camera is imported;
|
||||||
|
everything in uc480_camera references `ueye.*` at call time, not import
|
||||||
|
time, so an attribute-permissive dummy is sufficient for constructing
|
||||||
|
windows and importing modules.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyueye # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
class _UeyeStub:
|
||||||
|
"""Permissive attribute sink standing in for pyueye.ueye."""
|
||||||
|
IS_SUCCESS = 0
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return _UeyeStub()
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return _UeyeStub()
|
||||||
|
|
||||||
|
def __or__(self, other):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __ror__(self, other):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
_pyueye = types.ModuleType("pyueye")
|
||||||
|
_pyueye.ueye = _UeyeStub()
|
||||||
|
sys.modules["pyueye"] = _pyueye
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"""Generate golden fixtures capturing PRE-REFACTOR behavior (Phase 0).
|
||||||
|
|
||||||
|
Run once against the un-refactored sc3_aui_app.py; the outputs are committed
|
||||||
|
under tests/golden/. After the refactor, tests compare the new
|
||||||
|
implementations against these committed files — do NOT regenerate them
|
||||||
|
against refactored code, that would defeat the purpose.
|
||||||
|
|
||||||
|
Usage: .venv/bin/python tests/gen_goldens.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
import conftest # noqa: F401 (pyueye stub + offscreen)
|
||||||
|
|
||||||
|
import sc3_aui_app as app
|
||||||
|
|
||||||
|
GOLDEN = Path(__file__).parent / "golden"
|
||||||
|
|
||||||
|
# ── Geometry fixtures ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
GEOMETRY_CASES = [
|
||||||
|
# label, XS, YS, XD, YD, num_angles, row_spacing
|
||||||
|
("single_angle_square", "10.0", "10.0", "10.0", "10.0", "1", "0.25"),
|
||||||
|
("single_angle_rect", "10.0", "10.0", "80.0", "50.0", "1", "0.25"),
|
||||||
|
("three_angles", "5.0", "5.0", "20.0", "12.0", "3", "0.1"),
|
||||||
|
("five_angles", "0.0", "0.0", "30.0", "30.0", "5", "0.5"),
|
||||||
|
("seven_angles_asym", "12.5", "7.5", "42.0", "17.0", "7", "0.05"),
|
||||||
|
("two_angles_tiny", "1.0", "1.0", "0.02", "0.02", "2", "0.01"),
|
||||||
|
("flat_line_ydelta_zero", "10.0", "10.0", "5.0", "0.0", "1", "0.25"),
|
||||||
|
("fine_spacing", "3.0", "4.0", "2.5", "1.5", "4", "0.025"),
|
||||||
|
]
|
||||||
|
|
||||||
|
GEOMETRY_ERROR_CASES = [
|
||||||
|
("xd_zero", "10.0", "10.0", "0.0", "10.0", "1", "0.25"),
|
||||||
|
("xd_negative", "10.0", "10.0", "-5.0", "10.0", "1", "0.25"),
|
||||||
|
("spacing_zero", "10.0", "10.0", "10.0", "10.0", "1", "0.0"),
|
||||||
|
("angles_zero", "10.0", "10.0", "10.0", "10.0", "0", "0.25"),
|
||||||
|
("xs_not_number", "abc", "10.0", "10.0", "10.0", "1", "0.25"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _W:
|
||||||
|
"""Stands in for a QLineEdit: _build_scan_params only calls .text()."""
|
||||||
|
def __init__(self, text):
|
||||||
|
self._text = text
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_window(xs, ys, xd, yd, na, rs):
|
||||||
|
fs = type("FakeMainWindow", (), {})()
|
||||||
|
fs.x_start_edit = _W(xs)
|
||||||
|
fs.y_start_edit = _W(ys)
|
||||||
|
fs.x_delta_edit = _W(xd)
|
||||||
|
fs.y_delta_edit = _W(yd)
|
||||||
|
fs.num_angles_edit = _W(na)
|
||||||
|
fs.row_spacing_edit = _W(rs)
|
||||||
|
fs.scan_prefix_edit = _W("golden")
|
||||||
|
fs.scan_save_dir_edit = _W("/tmp/golden-scans")
|
||||||
|
return fs
|
||||||
|
|
||||||
|
|
||||||
|
def build_geometry_fixtures():
|
||||||
|
out = {"constants": {
|
||||||
|
"LASER_FREQ_HZ": app.LASER_FREQ_HZ,
|
||||||
|
"SCAN_VELOCITY_MM_S": app.SCAN_VELOCITY_MM_S,
|
||||||
|
"GR_ROTATION_SIGN": app.GR_ROTATION_SIGN,
|
||||||
|
}, "cases": {}, "error_cases": {}}
|
||||||
|
|
||||||
|
for label, *inputs in GEOMETRY_CASES:
|
||||||
|
params = app.MainWindow._build_scan_params(_fake_window(*inputs))
|
||||||
|
out["cases"][label] = {
|
||||||
|
"inputs": dict(zip(("XS", "YS", "XD", "YD", "num_angles", "row_spacing"), inputs)),
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, *inputs in GEOMETRY_ERROR_CASES:
|
||||||
|
try:
|
||||||
|
app.MainWindow._build_scan_params(_fake_window(*inputs))
|
||||||
|
raise AssertionError(f"error case {label} did not raise")
|
||||||
|
except ValueError as e:
|
||||||
|
out["error_cases"][label] = {
|
||||||
|
"inputs": dict(zip(("XS", "YS", "XD", "YD", "num_angles", "row_spacing"), inputs)),
|
||||||
|
"message": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(GOLDEN / "geometry.json", "w") as f:
|
||||||
|
json.dump(out, f, indent=2)
|
||||||
|
print(f"geometry.json: {len(out['cases'])} cases, {len(out['error_cases'])} error cases")
|
||||||
|
|
||||||
|
|
||||||
|
# ── SRAS v6 file fixtures ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SPF = 8 # samples per frame (synthetic, tiny)
|
||||||
|
SAMPLE_RATE = 6.25e9
|
||||||
|
PREAMBLES = [f"WFMOUTPRE:CH{ch};SYNTHETIC;PT_FMT Y;XINCR 1.6E-10" for ch in app.SCAN_CHANNELS]
|
||||||
|
BACKGROUND = bytes(range(SPF))
|
||||||
|
|
||||||
|
|
||||||
|
def _synthetic_frame(ai, ri, ci, fi):
|
||||||
|
return bytes((ai * 7 + ri * 5 + ci * 3 + fi + s) % 256 for s in range(SPF))
|
||||||
|
|
||||||
|
|
||||||
|
def build_sras_fixtures():
|
||||||
|
# Geometry via the real pre-refactor code path: 2 angles, 3 rows, 4 frames.
|
||||||
|
params = app.MainWindow._build_scan_params(
|
||||||
|
_fake_window("1.0", "1.0", "0.02", "0.02", "2", "0.01"))
|
||||||
|
per_angle = params["per_angle"]
|
||||||
|
angles = [pa["angle"] for pa in per_angle]
|
||||||
|
|
||||||
|
complete = GOLDEN / "complete.sras"
|
||||||
|
f = app._open_scan_file(
|
||||||
|
complete, angles, per_angle,
|
||||||
|
params["x_start_nominal"], params["y_start_nominal"],
|
||||||
|
params["x_delta_nominal"], params["y_delta_nominal"],
|
||||||
|
params["row_spacing"],
|
||||||
|
app.SCAN_VELOCITY_MM_S, app.LASER_FREQ_HZ,
|
||||||
|
SPF, SAMPLE_RATE, PREAMBLES, BACKGROUND,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data_start = f.tell()
|
||||||
|
for ai, pa in enumerate(per_angle):
|
||||||
|
for ri in range(pa["n_rows"]):
|
||||||
|
for ci in range(len(app.SCAN_CHANNELS)):
|
||||||
|
for fi in range(pa["n_frames"]):
|
||||||
|
f.write(_synthetic_frame(ai, ri, ci, fi))
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
info = app._read_sras_header(complete)
|
||||||
|
assert info["data_start_offset"] == data_start
|
||||||
|
row_bytes = info["n_channels"] * per_angle[0]["n_frames"] * SPF * info["bytes_per_sample"]
|
||||||
|
angle_bytes = [row_bytes * pa["n_rows"] for pa in per_angle]
|
||||||
|
|
||||||
|
def _truncate(name, size):
|
||||||
|
dst = GOLDEN / name
|
||||||
|
shutil.copyfile(complete, dst)
|
||||||
|
with open(dst, "r+b") as g:
|
||||||
|
g.truncate(data_start + size)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
variants = {
|
||||||
|
"complete.sras": complete,
|
||||||
|
"trunc_midrow_a1.sras": _truncate(
|
||||||
|
"trunc_midrow_a1.sras", angle_bytes[0] + row_bytes + row_bytes // 2),
|
||||||
|
"trunc_rowboundary_a1.sras": _truncate(
|
||||||
|
"trunc_rowboundary_a1.sras", angle_bytes[0] + 2 * row_bytes),
|
||||||
|
"trunc_angleboundary.sras": _truncate(
|
||||||
|
"trunc_angleboundary.sras", angle_bytes[0]),
|
||||||
|
"trunc_midrow_a0.sras": _truncate(
|
||||||
|
"trunc_midrow_a0.sras", row_bytes + row_bytes // 2),
|
||||||
|
"header_only.sras": _truncate("header_only.sras", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {"header": info, "statuses": {}}
|
||||||
|
for name, path in variants.items():
|
||||||
|
expected["statuses"][name] = app._compute_angle_status(path, info)
|
||||||
|
|
||||||
|
with open(GOLDEN / "sras_expected.json", "w") as f_json:
|
||||||
|
json.dump(expected, f_json, indent=2)
|
||||||
|
print(f"sras fixtures: {len(variants)} files, "
|
||||||
|
f"data block {sum(angle_bytes)} bytes, row_bytes={row_bytes}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
GOLDEN.mkdir(exist_ok=True)
|
||||||
|
build_geometry_fixtures()
|
||||||
|
build_sras_fixtures()
|
||||||
|
print("done")
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"version": 6,
|
||||||
|
"n_angles": 2,
|
||||||
|
"x_start_nominal": 1.0,
|
||||||
|
"y_start_nominal": 1.0,
|
||||||
|
"x_delta_nominal": 0.019999999552965164,
|
||||||
|
"y_delta_nominal": 0.019999999552965164,
|
||||||
|
"row_spacing": 0.009999999776482582,
|
||||||
|
"velocity": 100.0,
|
||||||
|
"laser_freq": 20000.0,
|
||||||
|
"samples_per_frame": 8,
|
||||||
|
"sample_rate": 6250000000.0,
|
||||||
|
"bytes_per_sample": 1,
|
||||||
|
"n_channels": 3,
|
||||||
|
"angles": [
|
||||||
|
0.0,
|
||||||
|
-180.0
|
||||||
|
],
|
||||||
|
"per_angle": [
|
||||||
|
{
|
||||||
|
"angle": 0.0,
|
||||||
|
"x_start": 1.0,
|
||||||
|
"x_delta": 0.019999999552965164,
|
||||||
|
"n_frames": 4,
|
||||||
|
"n_rows": 3,
|
||||||
|
"y_positions": [
|
||||||
|
1.0,
|
||||||
|
1.0099999904632568,
|
||||||
|
1.0199999809265137
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"angle": -180.0,
|
||||||
|
"x_start": 1.0,
|
||||||
|
"x_delta": 0.019999999552965164,
|
||||||
|
"n_frames": 4,
|
||||||
|
"n_rows": 3,
|
||||||
|
"y_positions": [
|
||||||
|
1.0,
|
||||||
|
1.0099999904632568,
|
||||||
|
1.0199999809265137
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data_start_offset": 265
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"complete.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 3,
|
||||||
|
"status": "OK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 553,
|
||||||
|
"n_rows_available": 3,
|
||||||
|
"status": "OK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trunc_midrow_a1.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 3,
|
||||||
|
"status": "OK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 553,
|
||||||
|
"n_rows_available": 1,
|
||||||
|
"status": "TRUNCATED"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trunc_rowboundary_a1.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 3,
|
||||||
|
"status": "OK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 553,
|
||||||
|
"n_rows_available": 2,
|
||||||
|
"status": "TRUNCATED"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trunc_angleboundary.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 3,
|
||||||
|
"status": "OK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 553,
|
||||||
|
"n_rows_available": 0,
|
||||||
|
"status": "MISSING"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trunc_midrow_a0.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 1,
|
||||||
|
"status": "TRUNCATED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 0,
|
||||||
|
"status": "MISSING"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header_only.sras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"angle_deg": 0.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 0,
|
||||||
|
"status": "MISSING"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"angle_deg": -180.0,
|
||||||
|
"n_rows": 3,
|
||||||
|
"row_bytes": 96,
|
||||||
|
"data_offset": 265,
|
||||||
|
"n_rows_available": 0,
|
||||||
|
"status": "MISSING"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,81 @@
|
|||||||
|
"""Golden-fixture consistency tests.
|
||||||
|
|
||||||
|
Phase 0: assert the pre-refactor implementations reproduce the committed
|
||||||
|
fixtures. Phase 2 migrates these assertions onto core.sras_format /
|
||||||
|
core.scan_geometry; the fixtures themselves never change.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import sc3_aui_app as app
|
||||||
|
from gen_goldens import (
|
||||||
|
BACKGROUND, PREAMBLES, SAMPLE_RATE, SPF, _fake_window, _synthetic_frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
GOLDEN = Path(__file__).parent / "golden"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def geometry():
|
||||||
|
with open(GOLDEN / "geometry.json") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def sras_expected():
|
||||||
|
with open(GOLDEN / "sras_expected.json") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_geometry_cases_match(geometry):
|
||||||
|
for label, case in geometry["cases"].items():
|
||||||
|
inputs = case["inputs"]
|
||||||
|
params = app.MainWindow._build_scan_params(_fake_window(*inputs.values()))
|
||||||
|
assert params == case["params"], f"geometry mismatch for case {label}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_geometry_error_cases_raise(geometry):
|
||||||
|
for label, case in geometry["error_cases"].items():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
app.MainWindow._build_scan_params(_fake_window(*case["inputs"].values()))
|
||||||
|
|
||||||
|
|
||||||
|
def test_writer_output_byte_identical(tmp_path, sras_expected):
|
||||||
|
params = app.MainWindow._build_scan_params(
|
||||||
|
_fake_window("1.0", "1.0", "0.02", "0.02", "2", "0.01"))
|
||||||
|
per_angle = params["per_angle"]
|
||||||
|
angles = [pa["angle"] for pa in per_angle]
|
||||||
|
|
||||||
|
out = tmp_path / "rewrite.sras"
|
||||||
|
f = app._open_scan_file(
|
||||||
|
out, angles, per_angle,
|
||||||
|
params["x_start_nominal"], params["y_start_nominal"],
|
||||||
|
params["x_delta_nominal"], params["y_delta_nominal"],
|
||||||
|
params["row_spacing"],
|
||||||
|
app.SCAN_VELOCITY_MM_S, app.LASER_FREQ_HZ,
|
||||||
|
SPF, SAMPLE_RATE, PREAMBLES, BACKGROUND,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
for ai, pa in enumerate(per_angle):
|
||||||
|
for ri in range(pa["n_rows"]):
|
||||||
|
for ci in range(len(app.SCAN_CHANNELS)):
|
||||||
|
for fi in range(pa["n_frames"]):
|
||||||
|
f.write(_synthetic_frame(ai, ri, ci, fi))
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
assert out.read_bytes() == (GOLDEN / "complete.sras").read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_header_parse_matches(sras_expected):
|
||||||
|
info = app._read_sras_header(GOLDEN / "complete.sras")
|
||||||
|
assert info == sras_expected["header"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_angle_status_all_variants(sras_expected):
|
||||||
|
info = app._read_sras_header(GOLDEN / "complete.sras")
|
||||||
|
for name, expected in sras_expected["statuses"].items():
|
||||||
|
statuses = app._compute_angle_status(GOLDEN / name, info)
|
||||||
|
assert statuses == expected, f"status mismatch for {name}"
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""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_sras_viewer_window(qapp):
|
||||||
|
import sras_viewer
|
||||||
|
win = sras_viewer.SrasViewerWindow()
|
||||||
|
_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
|
||||||
Reference in New Issue
Block a user