"""Tests for src/pygui/backend/visualization/wafer_map_item.py.""" from unittest.mock import MagicMock import pytest # Use the session-scoped qapp fixture from conftest.py so Qt Property/Signal # descriptors work properly. pytestmark = pytest.mark.usefixtures("qapp") def test_export_image_empty_path(): """export_image returns False for empty/None path.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() assert item.export_image("") is False assert item.export_image(None) is False def test_export_image_zero_size_item_fails(): """export_image returns False when the item has no geometry to render.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() assert item.export_image("/tmp/test_wafer.png") is False def test_export_image_unwritable_path_fails(tmp_path): """export_image returns False when the target directory doesn't exist.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(100) item.setHeight(100) item.values = [10.0, 20.0, 30.0] assert item.export_image(str(tmp_path / "no_such_dir" / "wafer.png")) is False def test_thickness_properties(): """Thickness data properties exist with correct defaults.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() assert item._thickness_data == [] assert item._show_thickness is False def test_thickness_setter_updates_data(): """Setting thicknessData stores [x, y, t2] triples.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item._rebuild_thickness = MagicMock() item.thicknessData = [[0.0, 10.0, 1.5], [5.0, -5.0, 2.0]] assert item._thickness_data == [[0.0, 10.0, 1.5], [5.0, -5.0, 2.0]] assert item._rebuild_thickness.called assert item.hasThickness is True THICKNESS_HEADER = "Lot Start Time,RC,Site#,Slot#,T2,NGOF,Wafer X,Wafer Y\n" def _write_csv(tmp_path, body, header=THICKNESS_HEADER): p = tmp_path / "thickness.csv" p.write_text(header + body) return str(p) def test_parse_thickness_csv_valid(tmp_path): from pygui.backend.visualization.wafer_map_item import parse_thickness_csv path = _write_csv(tmp_path, "t,1,1,7,1.5,0,10.0,-20.0\n" "t,1,2,7,2.5,0,-30.0,40.0\n") points, err = parse_thickness_csv(path) assert err == "" assert points == [[10.0, -20.0, 1.5], [-30.0, 40.0, 2.5]] def test_parse_thickness_csv_first_slot_only(tmp_path): """Rows after the Slot# changes belong to another wafer and are skipped.""" from pygui.backend.visualization.wafer_map_item import parse_thickness_csv path = _write_csv(tmp_path, "t,1,1,7,1.5,0,10.0,-20.0\n" "t,1,1,8,9.9,0,0.0,0.0\n") points, err = parse_thickness_csv(path) assert err == "" assert points == [[10.0, -20.0, 1.5]] def test_parse_thickness_csv_bad_header(tmp_path): from pygui.backend.visualization.wafer_map_item import parse_thickness_csv path = _write_csv(tmp_path, "t,1,1,7,1.5,0,10.0,-20.0\n", header="A,B,C\n") points, err = parse_thickness_csv(path) assert points == [] assert "header" in err def test_parse_thickness_csv_malformed_value(tmp_path): from pygui.backend.visualization.wafer_map_item import parse_thickness_csv path = _write_csv(tmp_path, "t,1,1,7,oops,0,10.0,-20.0\n") points, err = parse_thickness_csv(path) assert points == [] assert "Malformed" in err def test_parse_thickness_csv_missing_file(): from pygui.backend.visualization.wafer_map_item import parse_thickness_csv points, err = parse_thickness_csv("/no/such/file.csv") assert points == [] assert "Unable to read" in err def test_load_thickness_slot(tmp_path): """loadThickness feeds parsed points into thicknessData.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() path = _write_csv(tmp_path, "t,1,1,7,1.5,0,10.0,-20.0\n") assert item.loadThickness(path) == "" assert item.hasThickness is True assert item.loadThickness("/no/such/file.csv") != "" # Failed load keeps the previously loaded data. assert item.hasThickness is True def test_show_thickness_setter(): """Setting showThickness updates _show_thickness.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() assert item._show_thickness is False item.showThickness = True assert item._show_thickness is True def test_export_image_writes_real_png_headless(tmp_path): """export_image must produce an actual decodable image file without a QQuickWindow — the button was silently failing because grabToImage() resolves asynchronously and never completes headless/immediately.""" from PySide6.QtGui import QImage from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(200) item.setHeight(200) item.values = [10.0, 20.0, 30.0] out = tmp_path / "wafer.png" assert item.export_image(str(out)) is True assert out.exists() loaded = QImage(str(out)) assert not loaded.isNull() assert loaded.width() == 200 def test_export_image_includes_metrics_footer(tmp_path): """With sensor values loaded, the export gains a footer strip below the map carrying the metric readouts (sensors / min / max / avg).""" from PySide6.QtGui import QImage from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(200) item.setHeight(200) item.values = [10.0, 20.0, 30.0] out = tmp_path / "wafer.png" assert item.export_image(str(out)) is True loaded = QImage(str(out)) assert loaded.height() > 200 # footer strip appended # footer is painted opaque (dark bg), not transparent assert loaded.pixelColor(5, loaded.height() - 5).alpha() == 255 def test_export_image_extra_line_grows_footer(tmp_path): """Live export passes an extra metrics line; the footer gains a second row for it. Review export (no extra) keeps the single-row footer.""" from PySide6.QtGui import QImage from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(200) item.setHeight(200) item.values = [10.0, 20.0, 30.0] review = tmp_path / "review.png" live = tmp_path / "live.png" assert item.export_image(str(review)) is True assert item.export_image(str(live), "Frames: 42 Errors: 0") is True h_review = QImage(str(review)).height() h_live = QImage(str(live)).height() assert h_review > 200 # readout footer present assert h_live > h_review # extra live row appended def test_export_image_highlights_hottest_and_coldest(tmp_path): """The exported map rings the hottest sensor in the high color and the coldest in the low color. With no bands set every marker fill is the in-range green, so any high/low-colored pixels must come from the rings.""" from PySide6.QtGui import QImage from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(200) item.setHeight(200) item.sensors = [ {"label": "1", "x": -50.0, "y": 0.0}, {"label": "2", "x": 0.0, "y": 0.0}, {"label": "3", "x": 50.0, "y": 0.0}, ] item.values = [10.0, 20.0, 30.0] out = tmp_path / "wafer.png" assert item.export_image(str(out)) is True img = QImage(str(out)) found = {img.pixelColor(x, y).name() for y in range(img.height()) for x in range(img.width())} assert "#ef4444" in found # hottest ring (sensorHigh) assert "#5b9df5" in found # coldest ring (sensorLow) def test_show_extremes_toggle_gates_peak_rings(tmp_path): """showExtremes (Display setting) hides the hot/cold rings when off. Same pixel probe as the highlight test: with rings off, no high/low colored pixels should appear anywhere.""" from PySide6.QtGui import QImage from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() assert item.showExtremes is True # rings on by default item.setWidth(200) item.setHeight(200) item.sensors = [ {"label": "1", "x": -50.0, "y": 0.0}, {"label": "2", "x": 0.0, "y": 0.0}, {"label": "3", "x": 50.0, "y": 0.0}, ] item.values = [10.0, 20.0, 30.0] item.showExtremes = False out = tmp_path / "wafer.png" assert item.export_image(str(out)) is True img = QImage(str(out)) found = {img.pixelColor(x, y).name() for y in range(img.height()) for x in range(img.width())} assert "#ef4444" not in found assert "#5b9df5" not in found def test_readout_text_full_stats(): """Export footer readout carries every READOUT stat with sensor indices. Worked example: [10, 20, 30] → min 10 (#1), max 30 (#3), diff 20, avg 20, σ = sqrt(200/3) ≈ 8.16, 3σ ≈ 24.49. """ from pygui.backend.visualization.wafer_map_item import readout_text assert readout_text([10.0, 20.0, 30.0]) == ( "Sensors: 3 Min: 10.00°C (#1) Max: 30.00°C (#3) " "Diff: 20.00 Avg: 20.00 σ: 8.16 3σ: 24.49" ) def test_readout_text_empty(): from pygui.backend.visualization.wafer_map_item import readout_text assert readout_text([]) == "" def test_export_image_no_data_fails(tmp_path): """No file loaded / no stream played -> nothing meaningful to export.""" from pygui.backend.visualization.wafer_map_item import WaferMapItem item = WaferMapItem() item.setWidth(200) item.setHeight(200) out = tmp_path / "wafer.png" assert item.export_image(str(out)) is False assert not out.exists()