feat(export): batch wafer-map PNG export from file list
- BatchExport worker + BatchExportController (threaded, busy flag) - SourcePanel: Export Maps button → FolderDialog → batch export with progress/summary banner (dismissible) - StreamControlPanel: name single export after loaded CSV + timestamp
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Tests for src/pygui/backend/wafer/batch_export.py."""
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("qapp")
|
||||
|
||||
|
||||
def _write_single_frame_csv(path, wafer_id="F12"):
|
||||
lines = [
|
||||
f"Wafer ID={wafer_id}",
|
||||
"Acquisition Date=03/26/2025",
|
||||
"Label,1,2,3",
|
||||
"X (mm),0,10,20",
|
||||
"Y (mm),0,10,20",
|
||||
"data",
|
||||
"0.0,149.0,150.0,151.0",
|
||||
]
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def test_export_batch_writes_png_for_valid_file(tmp_path):
|
||||
from pygui.backend.wafer.batch_export import export_batch
|
||||
|
||||
csv_path = tmp_path / "f_run.csv"
|
||||
_write_single_frame_csv(csv_path)
|
||||
out_dir = tmp_path / "out"
|
||||
out_dir.mkdir()
|
||||
|
||||
results = export_batch([str(csv_path)], str(out_dir), timestamp="20260101_000000")
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].success is True
|
||||
expected_png = out_dir / "f_run_20260101_000000.png"
|
||||
assert expected_png.exists()
|
||||
assert results[0].output_path == str(expected_png)
|
||||
|
||||
|
||||
def test_export_batch_skips_bad_file_and_continues(tmp_path):
|
||||
from pygui.backend.wafer.batch_export import export_batch
|
||||
|
||||
bad_csv = tmp_path / "corrupt.csv"
|
||||
bad_csv.write_text("not a valid wafer csv\n", encoding="utf-8")
|
||||
|
||||
good_csv = tmp_path / "f_run.csv"
|
||||
_write_single_frame_csv(good_csv)
|
||||
|
||||
out_dir = tmp_path / "out"
|
||||
out_dir.mkdir()
|
||||
|
||||
results = export_batch(
|
||||
[str(bad_csv), str(good_csv)], str(out_dir), timestamp="20260101_000000"
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0].success is False
|
||||
assert results[0].error != ""
|
||||
assert not (out_dir / "corrupt_20260101_000000.png").exists()
|
||||
|
||||
assert results[1].success is True
|
||||
assert (out_dir / "f_run_20260101_000000.png").exists()
|
||||
|
||||
|
||||
def _write_two_frame_csv(path, wafer_id="F12"):
|
||||
lines = [
|
||||
f"Wafer ID={wafer_id}",
|
||||
"Acquisition Date=03/26/2025",
|
||||
"Label,1,2,3",
|
||||
"X (mm),0,10,20",
|
||||
"Y (mm),0,10,20",
|
||||
"data",
|
||||
"0.0,100.0,101.0,102.0",
|
||||
"1.0,149.0,150.0,151.0",
|
||||
]
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def test_export_batch_renders_last_frame_of_multiframe_file(tmp_path):
|
||||
from pygui.backend.wafer.batch_export import export_batch
|
||||
|
||||
multi_csv = tmp_path / "multi.csv"
|
||||
_write_two_frame_csv(multi_csv)
|
||||
|
||||
single_csv = tmp_path / "single.csv"
|
||||
_write_single_frame_csv(single_csv) # same values as multi's 2nd (last) row
|
||||
|
||||
out_dir = tmp_path / "out"
|
||||
out_dir.mkdir()
|
||||
|
||||
export_batch([str(multi_csv)], str(out_dir), timestamp="20260101_000000")
|
||||
export_batch([str(single_csv)], str(out_dir), timestamp="20260101_000000")
|
||||
|
||||
multi_png = (out_dir / "multi_20260101_000000.png").read_bytes()
|
||||
single_png = (out_dir / "single_20260101_000000.png").read_bytes()
|
||||
assert multi_png == single_png
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Tests for src/pygui/backend/controllers/batch_export_controller.py."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from pygui.backend.controllers.batch_export_controller import BatchExportController
|
||||
from pygui.backend.wafer.batch_export import BatchExportResult
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("qapp")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller():
|
||||
return BatchExportController()
|
||||
|
||||
|
||||
def test_start_spawns_thread_and_sets_busy(controller):
|
||||
with patch("threading.Thread") as mock_thread:
|
||||
controller.start(["a.csv", "b.csv"], "/tmp/out")
|
||||
mock_thread.assert_called_once()
|
||||
assert controller.busy is True
|
||||
|
||||
|
||||
def test_start_ignores_call_while_already_busy(controller):
|
||||
with patch("threading.Thread") as mock_thread:
|
||||
controller.start(["a.csv"], "/tmp/out")
|
||||
controller.start(["b.csv"], "/tmp/out")
|
||||
mock_thread.assert_called_once()
|
||||
|
||||
|
||||
def test_handle_batch_finished_reports_all_succeeded(controller):
|
||||
finished = MagicMock()
|
||||
controller.exportFinished.connect(finished)
|
||||
controller._busy = True
|
||||
|
||||
results = [
|
||||
BatchExportResult("a.csv", "a.png", True),
|
||||
BatchExportResult("b.csv", "b.png", True),
|
||||
BatchExportResult("c.csv", "c.png", True),
|
||||
]
|
||||
controller._handle_batch_finished(results)
|
||||
|
||||
finished.assert_called_once_with("Exported 3/3")
|
||||
assert controller.busy is False
|
||||
|
||||
|
||||
def test_handle_batch_finished_reports_failures(controller):
|
||||
finished = MagicMock()
|
||||
controller.exportFinished.connect(finished)
|
||||
controller._busy = True
|
||||
|
||||
results = [
|
||||
BatchExportResult("a.csv", "a.png", True),
|
||||
BatchExportResult("b.csv", "b.png", False, "parse error"),
|
||||
]
|
||||
controller._handle_batch_finished(results)
|
||||
|
||||
finished.assert_called_once_with("Exported 1/2 — 1 failed")
|
||||
assert controller.busy is False
|
||||
Reference in New Issue
Block a user