Files
pyGUI/tests/test_wafer_map_item.py
T
jack 94f917b116 feat(map): add edge-center delta stats, thickness overlay, and layout fix
- Add edge-center sensor pair tables and delta calculations per wafer family.
- Parse and interpolate wafer thickness CSV data for offscreen visualization.
- Refactor ReadoutPanel layout using Item + anchors to fix right-margin clipping.
- Compact E-C Delta text format to fit standard sidebar width.
- Add pytest suite covering delta calculations, layout pairs, and thickness CSV.
2026-07-10 17:32:12 -07:00

298 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()