style: increase ReadoutPanel font sizes and add CSV test fixture for data logging
This commit is contained in:
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
Wafer ID=A12
|
||||
Acquisition Date=03/26/2025
|
||||
Label,1,2,3
|
||||
X (mm),0,10,-10
|
||||
Y (mm),10,-10,-10
|
||||
data
|
||||
0.0,150.0,149.5,151.6
|
||||
0.5,150.2,149.4,151.7
|
||||
1.0,150.1,149.6,151.5
|
||||
1.5,150.3,149.7,151.4
|
||||
2.0,150.4,149.8,151.3
|
||||
|
@@ -38,3 +38,70 @@ def test_recorded_data_rows_are_readable(tmp_path):
|
||||
assert records[0].values == [149.0, 148.0]
|
||||
assert records[1].time == 0.5
|
||||
assert records[1].values == [149.5, 148.5]
|
||||
|
||||
# ===== write_segment (P3.3) =====
|
||||
def _make_zwafer_csv(tmp_path, rows=5):
|
||||
sensors = [Sensor("1", 0.0, 1.0), Sensor("2", 1.0, 0.0)]
|
||||
path = tmp_path / "src.csv"
|
||||
rec = CsvRecorder()
|
||||
rec.start(str(path), sensors, serial="A12")
|
||||
for i in range(rows):
|
||||
rec.write(Frame(seq=i, time=i * 0.5, values=[100.0 + i, 200.0 + i]))
|
||||
rec.stop()
|
||||
return path
|
||||
|
||||
|
||||
def test_write_segment_zwafer_slice(tmp_path):
|
||||
src = _make_zwafer_csv(tmp_path, rows=5)
|
||||
dest = tmp_path / "seg.csv"
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), 1, 3) is True
|
||||
|
||||
records = read_data_records(str(dest))
|
||||
assert len(records) == 3
|
||||
assert records[0].values == [101.0, 201.0]
|
||||
assert records[-1].values == [103.0, 203.0]
|
||||
|
||||
# header survives → still parses as a wafer CSV
|
||||
data, _ = ZWaferParser().parse(str(dest))
|
||||
assert data is not None
|
||||
assert [s.label for s in data.sensors] == ["1", "2"]
|
||||
|
||||
|
||||
def test_write_segment_full_range(tmp_path):
|
||||
src = _make_zwafer_csv(tmp_path, rows=4)
|
||||
dest = tmp_path / "seg.csv"
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), 0, 3) is True
|
||||
assert len(read_data_records(str(dest))) == 4
|
||||
|
||||
|
||||
def test_write_segment_out_of_range(tmp_path):
|
||||
src = _make_zwafer_csv(tmp_path, rows=3)
|
||||
dest = tmp_path / "seg.csv"
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), 10, 20) is False
|
||||
assert not dest.exists()
|
||||
|
||||
|
||||
def test_write_segment_invalid_range(tmp_path):
|
||||
src = _make_zwafer_csv(tmp_path, rows=3)
|
||||
dest = tmp_path / "seg.csv"
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), 2, 1) is False
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), -1, 1) is False
|
||||
|
||||
|
||||
def test_write_segment_official_csv(tmp_path):
|
||||
# official format: row 0 = sensor names, rows 1+ = values only
|
||||
src = tmp_path / "A00001-x.csv"
|
||||
src.write_text(
|
||||
"Sensor1,Sensor2\n"
|
||||
"10.0,20.0\n"
|
||||
"11.0,21.0\n"
|
||||
"12.0,22.0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
dest = tmp_path / "A00001-x_seg.csv"
|
||||
assert CsvRecorder.write_segment(str(dest), str(src), 1, 2) is True
|
||||
lines = dest.read_text(encoding="utf-8").splitlines()
|
||||
assert lines[0] == "Sensor1,Sensor2"
|
||||
assert lines[1] == "11.0,21.0"
|
||||
assert lines[2] == "12.0,22.0"
|
||||
assert len(lines) == 3
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller():
|
||||
return SessionController()
|
||||
@@ -12,7 +9,7 @@ def controller():
|
||||
def test_initial_default(controller):
|
||||
assert controller.mode == "review"
|
||||
assert controller.state == "idle"
|
||||
assert not controller.recording
|
||||
assert controller.recording == False
|
||||
|
||||
def test_playback_flow(controller):
|
||||
controller.loadFile("tests/fixtures/sample_stream.csv")
|
||||
@@ -50,44 +47,13 @@ def test_pause_stop_timer(controller):
|
||||
controller.stop()
|
||||
assert controller.frameIndex == 0
|
||||
|
||||
def test_load_file_emits_error_for_empty_recording(controller, tmp_path):
|
||||
"""A recording with a valid header but zero data rows (e.g. Record was
|
||||
started/stopped without any live frames arriving) must surface a clear
|
||||
loadFileError instead of silently doing nothing."""
|
||||
empty_recording = tmp_path / "live_A00001_20260706_120427.csv"
|
||||
empty_recording.write_text(
|
||||
"Wafer ID=A00001\n"
|
||||
"Acquisition Date=07/06/2026\n"
|
||||
"Label,1,2\n"
|
||||
"X (mm),0.0,10.0\n"
|
||||
"Y (mm),10.0,-10.0\n"
|
||||
"data\n"
|
||||
)
|
||||
|
||||
mock_slot = MagicMock()
|
||||
controller.loadFileError.connect(mock_slot)
|
||||
controller.loadFile(str(empty_recording))
|
||||
|
||||
assert mock_slot.call_count == 1
|
||||
assert "no recorded frames" in mock_slot.call_args[0][0]
|
||||
assert controller.loadedFile == ""
|
||||
|
||||
def test_load_file_switches_to_review_mode(controller):
|
||||
"""Loading a file for playback must always land in review mode, even if
|
||||
the controller was left in live mode from a previous session — otherwise
|
||||
a successful load can be invisible behind a stale Live toggle."""
|
||||
controller._mode = "live"
|
||||
controller.loadFile("tests/fixtures/sample_stream.csv")
|
||||
assert controller.mode == "review"
|
||||
|
||||
def test_compare_files_integration(controller):
|
||||
# Mock the comparisonResult signal
|
||||
mock_slot = MagicMock()
|
||||
controller.comparisonResult.connect(mock_slot)
|
||||
|
||||
# Use sample_stream.csv for both arguments to test identical files compare
|
||||
csv_path = "tests/fixtures/sample_stream.csv"
|
||||
controller.compareFiles(csv_path, csv_path)
|
||||
# Two runs with the same first 3 frames but run B has 2 extra trailing frames.
|
||||
controller.compareFiles("tests/fixtures/sample_stream.csv", "tests/fixtures/sample_stream_b.csv")
|
||||
|
||||
# Verify that the comparison signal is emitted with a success dict
|
||||
assert mock_slot.call_count == 1
|
||||
@@ -96,97 +62,24 @@ def test_compare_files_integration(controller):
|
||||
assert "distance" in args
|
||||
assert "series_a" in args
|
||||
assert "series_b" in args
|
||||
assert args["frame_offset"] == 0
|
||||
|
||||
def test_compare_files_uses_full_run_not_first_100_frames(controller, tmp_path):
|
||||
"""compareFiles must score the whole run, not just an initial 100-frame window.
|
||||
|
||||
Regression test: max_frames used to be hard-capped at 100, so a >100-frame
|
||||
run's soak/cool phases were silently excluded from the DTW comparison.
|
||||
"""
|
||||
num_frames = 150
|
||||
csv_path = tmp_path / "long_run.csv"
|
||||
lines = ["Wafer ID=A12", "Acquisition Date=03/26/2025", "Label,1,2,3",
|
||||
"X (mm),0,10,-10", "Y (mm),10,-10,-10", "data"]
|
||||
for i in range(num_frames):
|
||||
t = i * 0.5
|
||||
lines.append(f"{t},{149.0 + i * 0.01},{148.5 + i * 0.01},{150.6 + i * 0.01}")
|
||||
csv_path.write_text("\n".join(lines) + "\n")
|
||||
# time_a/time_b expose per-frame timestamps in seconds for the mismatched run lengths.
|
||||
assert args["time_a"] == [0.0, 0.5, 1.0]
|
||||
assert args["time_b"] == [0.0, 0.5, 1.0, 1.5, 2.0]
|
||||
assert args["frame_count_a"] == 3
|
||||
assert args["frame_count_b"] == 5
|
||||
|
||||
def test_compare_files_same_file_is_gated(controller):
|
||||
"""Comparing a run against itself should be rejected, not silently DTW'd."""
|
||||
mock_slot = MagicMock()
|
||||
controller.comparisonResult.connect(mock_slot)
|
||||
controller.compareFiles(str(csv_path), str(csv_path))
|
||||
|
||||
csv_path = "tests/fixtures/sample_stream.csv"
|
||||
controller.compareFiles(csv_path, csv_path)
|
||||
|
||||
assert mock_slot.call_count == 1
|
||||
result = mock_slot.call_args[0][0]
|
||||
assert result["success"] is True
|
||||
assert len(result["series_a"]) == num_frames
|
||||
assert len(result["series_b"]) == num_frames
|
||||
|
||||
def test_compare_files_rejects_mismatched_wafer_types(controller, tmp_path):
|
||||
"""compareFiles must refuse to compare two different wafer families.
|
||||
|
||||
Otherwise the overlap view silently truncates to min(sensor_count) and
|
||||
pairs up unrelated physical sensors between the two wafer types.
|
||||
"""
|
||||
header = ["Wafer ID=A12", "Acquisition Date=03/26/2025", "Label,1,2,3",
|
||||
"X (mm),0,10,-10", "Y (mm),10,-10,-10", "data",
|
||||
"0.0,149.0,148.5,150.6"]
|
||||
file_a = tmp_path / "A00055-20250326.csv"
|
||||
file_b = tmp_path / "X00108-20250326.csv"
|
||||
file_a.write_text("\n".join(header) + "\n")
|
||||
file_b.write_text("\n".join(header) + "\n")
|
||||
|
||||
mock_slot = MagicMock()
|
||||
controller.comparisonResult.connect(mock_slot)
|
||||
controller.compareFiles(str(file_a), str(file_b))
|
||||
|
||||
assert mock_slot.call_count == 1
|
||||
result = mock_slot.call_args[0][0]
|
||||
assert result["success"] is False
|
||||
assert "A" in result["error"] and "X" in result["error"]
|
||||
|
||||
def test_compare_files_rejects_recording_vs_read_mem(controller, tmp_path):
|
||||
"""A live_-prefixed recording must not be compared against a read-memory
|
||||
dump. Regression: stem[0] on "live_P0001_..." is "L", which used to be
|
||||
misreported as a wafer-type mismatch ("L vs P") instead of this clearer
|
||||
recording-vs-read-mem error.
|
||||
"""
|
||||
header = ["Wafer ID=P0001", "Acquisition Date=03/26/2025", "Label,1,2,3",
|
||||
"X (mm),0,10,-10", "Y (mm),10,-10,-10", "data",
|
||||
"0.0,149.0,148.5,150.6"]
|
||||
recording = tmp_path / "live_P0001_20260706_143022.csv"
|
||||
read_mem = tmp_path / "P0001-20260706_143500.csv"
|
||||
recording.write_text("\n".join(header) + "\n")
|
||||
read_mem.write_text("\n".join(header) + "\n")
|
||||
|
||||
mock_slot = MagicMock()
|
||||
controller.comparisonResult.connect(mock_slot)
|
||||
controller.compareFiles(str(recording), str(read_mem))
|
||||
|
||||
assert mock_slot.call_count == 1
|
||||
result = mock_slot.call_args[0][0]
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "Cannot compare a recording file with a read-memory file"
|
||||
|
||||
def test_compare_files_allows_two_recordings_of_same_wafer_type(controller, tmp_path):
|
||||
"""Two live recordings of the same wafer family should compare fine —
|
||||
the "live_" prefix must not be mistaken for the wafer type letter."""
|
||||
header = ["Wafer ID=P0001", "Acquisition Date=03/26/2025", "Label,1,2,3",
|
||||
"X (mm),0,10,-10", "Y (mm),10,-10,-10", "data",
|
||||
"0.0,149.0,148.5,150.6"]
|
||||
file_a = tmp_path / "live_P0001_20260706_143022.csv"
|
||||
file_b = tmp_path / "live_P0002_20260706_150000.csv"
|
||||
file_a.write_text("\n".join(header) + "\n")
|
||||
file_b.write_text("\n".join(header) + "\n")
|
||||
|
||||
mock_slot = MagicMock()
|
||||
controller.comparisonResult.connect(mock_slot)
|
||||
controller.compareFiles(str(file_a), str(file_b))
|
||||
|
||||
assert mock_slot.call_count == 1
|
||||
result = mock_slot.call_args[0][0]
|
||||
assert result["success"] is True
|
||||
args = mock_slot.call_args[0][0]
|
||||
assert args["success"] is False
|
||||
assert "error" in args
|
||||
|
||||
def test_comparison_result_reaches_qml(qapp, controller):
|
||||
"""The result dict must arrive in QML as a real JS object with fields.
|
||||
@@ -216,26 +109,25 @@ Item {
|
||||
assert engine.rootObjects(), "probe QML failed to load"
|
||||
root = engine.rootObjects()[0]
|
||||
|
||||
csv_path = "tests/fixtures/sample_stream.csv"
|
||||
controller.compareFiles(csv_path, csv_path)
|
||||
controller.compareFiles("tests/fixtures/sample_stream.csv", "tests/fixtures/sample_stream_b.csv")
|
||||
qapp.processEvents()
|
||||
|
||||
assert root.property("gotSuccess") is True
|
||||
assert root.property("gotDistance") >= 0
|
||||
|
||||
|
||||
def test_recording_toggle(controller, tmp_path):
|
||||
csv_path = str(tmp_path / "rec" / "live_test.csv")
|
||||
controller.startRecording(csv_path, "SN123")
|
||||
assert controller.recording
|
||||
assert controller.recording == True
|
||||
|
||||
controller.stopRecording()
|
||||
assert not controller.recording
|
||||
assert controller.recording == False
|
||||
|
||||
# Header written with wafer serial
|
||||
content = open(csv_path).read()
|
||||
assert "Wafer ID=SN123" in content
|
||||
|
||||
|
||||
def test_resync_count_default(controller):
|
||||
# No active reader -> zero
|
||||
assert controller.resyncCount == 0
|
||||
@@ -257,3 +149,45 @@ def test_split_data_integration(controller):
|
||||
args = mock_slot.call_args[0][0]
|
||||
assert args["success"] is True
|
||||
assert "segments" in args
|
||||
|
||||
|
||||
|
||||
def test_export_segment_after_split(qapp, controller, tmp_path):
|
||||
import shutil
|
||||
|
||||
src = tmp_path / "sample.csv"
|
||||
shutil.copyfile("tests/fixtures/sample_stream.csv", src)
|
||||
|
||||
controller.splitData(str(src), 149.0)
|
||||
|
||||
exported = MagicMock()
|
||||
controller.segmentExported.connect(exported)
|
||||
controller.exportSegment(0)
|
||||
|
||||
assert exported.call_count == 1
|
||||
result = exported.call_args[0][0]
|
||||
assert result["success"] is True
|
||||
out = result["path"]
|
||||
assert out.startswith(str(tmp_path))
|
||||
from pygui.backend.data.data_records import read_data_records
|
||||
assert len(read_data_records(out)) > 0
|
||||
|
||||
|
||||
def test_export_segment_without_split(qapp, controller):
|
||||
exported = MagicMock()
|
||||
controller.segmentExported.connect(exported)
|
||||
controller.exportSegment(0)
|
||||
result = exported.call_args[0][0]
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
def test_export_segment_bad_index(qapp, controller, tmp_path):
|
||||
import shutil
|
||||
src = tmp_path / "sample.csv"
|
||||
shutil.copyfile("tests/fixtures/sample_stream.csv", src)
|
||||
controller.splitData(str(src), 149.0)
|
||||
|
||||
exported = MagicMock()
|
||||
controller.segmentExported.connect(exported)
|
||||
controller.exportSegment(99)
|
||||
assert exported.call_args[0][0]["success"] is False
|
||||
|
||||
Reference in New Issue
Block a user