958fde9050
- Removed outdated comments and documentation from CsvSessionSource and DemoSource classes. - Introduced new unit tests for CsvSource and DataSource functionalities, ensuring robust validation of CSV loading and session handling. - Added UI smoke tests to verify window instantiation and menu actions without hardware dependencies. - Implemented VisionEngine tests to validate demo snapshots and alignment state logic.
135 lines
5.7 KiB
Python
135 lines
5.7 KiB
Python
"""Tests for CsvSource CSV loader."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
import sys
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from wafer_edge.Calibration import BaselineCalibration
|
|
from wafer_edge.CsvSource import find_session_files, load_camera_records_from_csv, parse_csv_filename
|
|
|
|
|
|
BASELINE = BaselineCalibration()
|
|
MASTER_SD = Path(__file__).resolve().parents[1] / "MASTER_SD"
|
|
DATA_DIR = Path(__file__).resolve().parents[1] / "DATA" / "0610"
|
|
|
|
|
|
class CsvSourceTests(unittest.TestCase):
|
|
"""Test CSV loader correctness and robustness."""
|
|
|
|
def test_point_count_matches_header(self) -> None:
|
|
"""Test 1: load S10519P0.CSV, assert parser reads all data rows."""
|
|
# Read raw point count from file header (before filtering)
|
|
with open(MASTER_SD / "S10519P0.CSV") as f:
|
|
for line in f:
|
|
if line.startswith("# points,"):
|
|
raw_count = int(line.split(",")[1].strip())
|
|
break
|
|
# After filter_ring_outliers, non-wafer edges are stripped
|
|
# but a significant fraction should remain for a good capture
|
|
paths = {"cam-1": MASTER_SD / "S10519P0.CSV"}
|
|
records = load_camera_records_from_csv(paths, BASELINE)
|
|
self.assertEqual(len(records), 1)
|
|
self.assertEqual(records[0].camera_id, "cam-1")
|
|
filtered = len(records[0].points)
|
|
self.assertGreater(filtered, 0, "No points loaded from CSV")
|
|
self.assertGreaterEqual(raw_count, 100, f"Expected >= 100 raw points, got {raw_count}")
|
|
# Filtered should be a reasonable fraction (at least 5% of raw)
|
|
self.assertGreaterEqual(filtered, raw_count * 0.05,
|
|
f"Filtered {filtered} << raw {raw_count}")
|
|
|
|
def test_mm_values_in_reasonable_range(self) -> None:
|
|
"""Test 2: assert all mm values are within ±100 mm (physically reasonable)."""
|
|
paths = {"cam-1": MASTER_SD / "S10519P0.CSV"}
|
|
records = load_camera_records_from_csv(paths, BASELINE)
|
|
pts = np.array(records[0].points)
|
|
# All coordinates should be within ±100 mm for a 75mm radius wafer
|
|
self.assertTrue(np.all(np.abs(pts) < 100.0),
|
|
f"Out-of-range values: min={pts.min():.1f}, max={pts.max():.1f}")
|
|
|
|
def test_four_cameras_returns_four_records(self) -> None:
|
|
"""Test 3: 4 paths → 4 records with correct camera IDs."""
|
|
paths = {
|
|
"cam-1": MASTER_SD / "S10540P0.CSV",
|
|
"cam-2": MASTER_SD / "S20540P0.CSV",
|
|
"cam-3": MASTER_SD / "R10540P0.CSV",
|
|
"cam-4": MASTER_SD / "R20539P0.CSV",
|
|
}
|
|
records = load_camera_records_from_csv(paths, BASELINE)
|
|
self.assertEqual(len(records), 4)
|
|
record_ids = {r.camera_id for r in records}
|
|
self.assertEqual(record_ids, {"cam-1", "cam-2", "cam-3", "cam-4"})
|
|
# All should have points and ok status
|
|
for r in records:
|
|
self.assertGreater(len(r.points), 0, f"{r.camera_id} has no points")
|
|
self.assertEqual(r.status, "ok")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# find_session_files tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_find_session_files_complete(self) -> None:
|
|
"""All 4 cameras present → dict with 4 entries, correct labels."""
|
|
result = find_session_files(DATA_DIR)
|
|
self.assertEqual(len(result), 4)
|
|
self.assertIn("cam-1 (S1)", result)
|
|
self.assertIn("cam-2 (S2)", result)
|
|
self.assertIn("cam-3 (R1)", result)
|
|
self.assertIn("cam-4 (R2)", result)
|
|
for label, path in result.items():
|
|
self.assertTrue(path.exists(), f"{label} → {path}")
|
|
|
|
def test_find_session_files_partial(self) -> None:
|
|
"""Only 2 cameras present → returns dict with 2 entries, no error."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmpdir = Path(tmp)
|
|
for name in ("S10610P0.CSV", "R10610P0.CSV"):
|
|
(tmpdir / name).write_text(
|
|
(DATA_DIR / name).read_text()
|
|
)
|
|
result = find_session_files(tmpdir)
|
|
self.assertEqual(len(result), 2)
|
|
self.assertIn("cam-1 (S1)", result)
|
|
self.assertIn("cam-3 (R1)", result)
|
|
self.assertNotIn("cam-2 (S2)", result)
|
|
self.assertNotIn("cam-4 (R2)", result)
|
|
|
|
def test_find_session_files_empty_folder(self) -> None:
|
|
"""No CSVs at all → empty dict, no exception."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = find_session_files(Path(tmp))
|
|
self.assertEqual(result, {})
|
|
|
|
def test_find_session_files_bad_filename(self) -> None:
|
|
"""Non-NanoBerry file is silently skipped."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmpdir = Path(tmp)
|
|
(tmpdir / "README.TXT").write_text("hello")
|
|
(tmpdir / "S10610P0.CSV").write_text(
|
|
(DATA_DIR / "S10610P0.CSV").read_text()
|
|
)
|
|
result = find_session_files(tmpdir)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertIn("cam-1 (S1)", result)
|
|
|
|
def test_find_session_files_new_format(self) -> None:
|
|
"""Underscore-style filenames (R1_0540_P0.CSV) are also discovered."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmpdir = Path(tmp)
|
|
(tmpdir / "S1_0610_P0.CSV").write_text(
|
|
(DATA_DIR / "S10610P0.CSV").read_text()
|
|
)
|
|
result = find_session_files(tmpdir)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertIn("cam-1 (S1)", result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|