feat(import): Z-wafer CSV import button with result banner
- importZWaferCsv slot on FileBrowser — converts and adds to list - SourcePanel: import icon button → FileDialog → importSummary banner (auto-dismisses after 5s, loads the converted file into streamController) - wafer_map_item: showExtremes default → true
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
@@ -15,6 +16,24 @@ def _write_csv(path, wafer_id="P00001"):
|
||||
)
|
||||
|
||||
|
||||
def _write_raw_zwafer_export(path, wafer_id="P00001", chamber="Chamber A", comments="test run"):
|
||||
"""A raw fixture-export CSV, as ZWaferParser/ImportZWaferCSV expect it:
|
||||
row-indexed data rows (leading column dropped on import), unlike the
|
||||
already-normalized `{serial}-{timestamp}.csv` files _write_csv produces."""
|
||||
path.write_text(
|
||||
f"Wafer ID={wafer_id}\n"
|
||||
"Acquisition Date=03/26/2025\n"
|
||||
f"Chamber={chamber}\n"
|
||||
f"Comments={comments}\n"
|
||||
"Label,1,2,3\n"
|
||||
"X (mm),0,10,-10\n"
|
||||
"Y (mm),10,-10,-10\n"
|
||||
"data\n"
|
||||
"0.0,149.0,148.5,150.6\n"
|
||||
"1.0,149.2,148.7,150.9\n"
|
||||
)
|
||||
|
||||
|
||||
def test_recorded_live_file_flagged_as_recording(qapp, tmp_path):
|
||||
"""Files named live_<serial>_<timestamp>.csv (SessionController.startRecording)
|
||||
must be distinguishable in the browser from read-memory dumps."""
|
||||
@@ -41,3 +60,103 @@ def test_set_current_directory_updates_dir_and_refreshes(qapp, tmp_path):
|
||||
assert browser.currentDirectory == str(tmp_path)
|
||||
assert len(browser.files) == 1
|
||||
assert Path(browser.files[0]["fileName"]).name == "P00003-20260706_143500.csv"
|
||||
|
||||
|
||||
def test_import_zwafer_csv_writes_normalized_file_and_sidecar(qapp, tmp_path):
|
||||
"""Mirrors C#'s ImportZWaferCSV (Form1.cs:1711): strip the leading row-index
|
||||
column, rename to {serial}-{timestamp}.csv (no Z prefix — matches this app's
|
||||
other write paths), write a metadata sidecar, and refresh the file list."""
|
||||
source_dir = tmp_path / "incoming"
|
||||
source_dir.mkdir()
|
||||
source = source_dir / "raw_export.csv"
|
||||
_write_raw_zwafer_export(source)
|
||||
|
||||
browser = FileBrowser()
|
||||
browser._set_current_directory(tmp_path)
|
||||
|
||||
signals = []
|
||||
browser.importFinished.connect(lambda path, message: signals.append((path, message)))
|
||||
browser.importZWaferCsv(str(source))
|
||||
|
||||
new_path = tmp_path / "P00001-20250326_000000.csv"
|
||||
assert new_path.exists()
|
||||
assert new_path.read_text().splitlines() == [
|
||||
"1,2,3",
|
||||
"149.0,148.5,150.6",
|
||||
"149.2,148.7,150.9",
|
||||
]
|
||||
|
||||
sidecar = json.loads((tmp_path / "P00001-20250326_000000.csv.json").read_text())
|
||||
assert sidecar["wafer"] == "P00001"
|
||||
assert sidecar["chamber"] == "Chamber A"
|
||||
assert sidecar["notes"] == "test run"
|
||||
|
||||
assert len(signals) == 1
|
||||
assert signals[0][0] == str(new_path)
|
||||
assert any(Path(row["fileName"]).name == new_path.name for row in browser.files)
|
||||
|
||||
|
||||
def test_import_zwafer_csv_blocks_reimporting_same_source(qapp, tmp_path):
|
||||
"""Re-importing the identical source file must be blocked, not overwritten —
|
||||
the output filename is derived from the source's own acquisition-date
|
||||
header, so a second import of the same file collides on purpose."""
|
||||
source_dir = tmp_path / "incoming"
|
||||
source_dir.mkdir()
|
||||
source = source_dir / "raw_export.csv"
|
||||
_write_raw_zwafer_export(source)
|
||||
|
||||
browser = FileBrowser()
|
||||
browser._set_current_directory(tmp_path)
|
||||
|
||||
signals = []
|
||||
browser.importFinished.connect(lambda path, message: signals.append((path, message)))
|
||||
|
||||
browser.importZWaferCsv(str(source))
|
||||
browser.importZWaferCsv(str(source))
|
||||
|
||||
assert len(signals) == 2
|
||||
assert signals[1][0] == ""
|
||||
assert "already" in signals[1][1].lower()
|
||||
|
||||
|
||||
def test_import_zwafer_csv_without_date_header_still_blocks_duplicate(qapp, tmp_path):
|
||||
"""No fallback to wall-clock time: a source missing 'Acquisition Date='
|
||||
must still get a deterministic filename, so re-importing it a second
|
||||
time is blocked instead of silently succeeding with a fresh timestamp."""
|
||||
source_dir = tmp_path / "incoming"
|
||||
source_dir.mkdir()
|
||||
source = source_dir / "raw_export.csv"
|
||||
source.write_text(
|
||||
"Wafer ID=P00002\n"
|
||||
"Label,1,2,3\n"
|
||||
"X (mm),0,10,-10\n"
|
||||
"Y (mm),10,-10,-10\n"
|
||||
"data\n"
|
||||
"0.0,149.0,148.5,150.6\n"
|
||||
)
|
||||
|
||||
browser = FileBrowser()
|
||||
browser._set_current_directory(tmp_path)
|
||||
|
||||
signals = []
|
||||
browser.importFinished.connect(lambda path, message: signals.append((path, message)))
|
||||
|
||||
browser.importZWaferCsv(str(source))
|
||||
browser.importZWaferCsv(str(source))
|
||||
|
||||
assert len(signals) == 2
|
||||
assert signals[0][0] != "" # first import succeeds
|
||||
assert signals[1][0] == "" # second is blocked
|
||||
assert "already" in signals[1][1].lower()
|
||||
|
||||
|
||||
def test_import_zwafer_csv_reports_missing_source(qapp, tmp_path):
|
||||
browser = FileBrowser()
|
||||
browser._set_current_directory(tmp_path)
|
||||
|
||||
signals = []
|
||||
browser.importFinished.connect(lambda path, message: signals.append((path, message)))
|
||||
browser.importZWaferCsv(str(tmp_path / "missing.csv"))
|
||||
|
||||
assert len(signals) == 1
|
||||
assert signals[0][0] == ""
|
||||
|
||||
Reference in New Issue
Block a user