diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..550dbc2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +"""Shared pytest fixtures.""" +import pytest +from PySide6.QtWidgets import QApplication + + +@pytest.fixture(scope="session") +def qapp(): + """Provide a QApplication instance for the test session.""" + app = QApplication.instance() + if app is None: + app = QApplication([]) + yield app diff --git a/tests/test_comparison.py b/tests/test_comparison.py new file mode 100644 index 0000000..777bc1f --- /dev/null +++ b/tests/test_comparison.py @@ -0,0 +1,63 @@ +"""Tests for src/pygui/backend/comparison.py.""" + +import numpy as np +from pygui.backend.comparison import compare_runs + + +def test_identical_series(): + a = [20.0, 30.0, 40.0, 50.0, 60.0] + result = compare_runs(a, a) + assert result["distance"] == 0.0 + assert len(result["index_a"]) == len(a) + assert result["index_a"] == result["index_b"] + + +def test_empty_series(): + result = compare_runs([], []) + assert result["distance"] == float("inf") + assert result["index_a"] == [] + assert result["index_b"] == [] + assert result["warping_path"] == [] + + +def test_one_empty(): + result = compare_runs([10.0, 20.0], []) + assert result["distance"] == float("inf") + + +def test_aligned_series(): + a = [10.0, 20.0, 30.0] + b = [15.0, 25.0, 35.0] + result = compare_runs(a, b) + assert result["distance"] >= 0.0 + assert len(result["index_a"]) > 0 + assert len(result["index_b"]) > 0 + + +def test_single_element(): + result = compare_runs([42.0], [42.0]) + assert result["distance"] == 0.0 + + +def test_different_lengths(): + short = [10.0, 20.0] + long_ = [10.0, 15.0, 20.0, 25.0, 30.0] + result = compare_runs(short, long_) + assert result["distance"] >= 0.0 + assert len(result["index_a"]) > 0 + assert len(result["index_b"]) > 0 + assert len(result["index_a"]) == len(result["index_b"]) + + +def test_return_structure(): + a = [1.0, 2.0, 3.0, 4.0, 5.0] + b = [2.0, 3.0, 4.0, 5.0, 6.0] + result = compare_runs(a, b) + assert "distance" in result + assert "index_a" in result + assert "index_b" in result + assert "warping_path" in result + assert isinstance(result["distance"], float) + assert isinstance(result["index_a"], list) + assert isinstance(result["index_b"], list) + assert isinstance(result["warping_path"], list) diff --git a/tests/test_graph_view.py b/tests/test_graph_view.py new file mode 100644 index 0000000..31f4d5b --- /dev/null +++ b/tests/test_graph_view.py @@ -0,0 +1,108 @@ +"""Tests for src/pygui/backend/visualization/graph_view.py.""" +import json +from unittest.mock import MagicMock, PropertyMock, patch + +import pytest + + +def _make_plot_widget(): + """Create a mock PlotWidget with the interface GraphView expects.""" + widget = MagicMock() + widget.removeItem = MagicMock() + widget.plot = MagicMock(return_value=MagicMock()) + widget.setYRange = MagicMock() + widget.clear = MagicMock() + return widget + + +class TestUpdateTrend: + def test_parses_valid_json(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + gv.updateTrend(json.dumps([20.0, 25.0, 30.0])) + + assert gv._plot_widget.plot.called + gv._plot_widget.setYRange.assert_called_once() + + def test_rejects_invalid_json(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + gv.updateTrend("not json") + + assert not gv._plot_widget.plot.called + + def test_rejects_none(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + gv.updateTrend(None) + + assert not gv._plot_widget.plot.called + + def test_empty_list(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + gv.updateTrend("[]") + + # Empty list means nothing to plot, but clear IS called + assert gv._plot_widget.plot.called is False + + def test_no_plot_widget(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + assert gv._plot_widget is None + + # Should not crash + gv.updateTrend("[1, 2, 3]") + + +class TestUpdateComparison: + def test_parses_valid_json(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + a = [10.0, 20.0, 30.0] + b = [12.0, 22.0, 32.0] + path = [[0, 0], [1, 1], [2, 2]] + + gv.updateComparison(json.dumps(a), json.dumps(b), json.dumps(path)) + + assert gv._plot_widget.clear.called + assert gv._plot_widget.plot.call_count == 2 + + def test_rejects_invalid_json(self): + """clear() is called before parsing, so it always fires on valid widget.""" + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + gv._plot_widget = _make_plot_widget() + + gv.updateComparison("bad", json.dumps([1]), json.dumps([])) + + # clear() is called before parsing — it always fires + assert gv._plot_widget.clear.called + # But plot() should NOT be called since parsing fails + assert not gv._plot_widget.plot.called + + def test_no_plot_widget(self): + from pygui.backend.visualization.graph_view import GraphView + + gv = GraphView() + assert gv._plot_widget is None + + # Should not crash + gv.updateComparison("[1,2]", "[3,4]", "[[0,0]]") diff --git a/tests/test_wafer_map_item.py b/tests/test_wafer_map_item.py new file mode 100644 index 0000000..bc99450 --- /dev/null +++ b/tests/test_wafer_map_item.py @@ -0,0 +1,91 @@ +"""Tests for src/pygui/backend/visualization/wafer_map_item.py.""" +from unittest.mock import MagicMock, patch + +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_valid_path(): + """export_image saves a PNG when grabToImage succeeds.""" + from pygui.backend.visualization.wafer_map_item import WaferMapItem + + item = WaferMapItem() + + mock_image = MagicMock() + mock_result = MagicMock() + mock_result.image.return_value = mock_image + item.grabToImage = MagicMock(return_value=mock_result) + + assert item.export_image("/tmp/test_wafer.png") is True + + +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_grab_fails(): + """export_image returns False when grabToImage raises.""" + from pygui.backend.visualization.wafer_map_item import WaferMapItem + + item = WaferMapItem() + item.grabToImage = MagicMock(side_effect=RuntimeError("grab failed")) + + assert item.export_image("/tmp/test_wafer.png") is False + + +def test_export_image_save_fails(): + """export_image returns False when image.save raises.""" + from pygui.backend.visualization.wafer_map_item import WaferMapItem + + mock_image = MagicMock() + mock_image.save.side_effect = IOError("disk full") + mock_result = MagicMock() + mock_result.image.return_value = mock_image + + item = WaferMapItem() + item.grabToImage = MagicMock(return_value=mock_result) + + assert item.export_image("/tmp/test_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 updates _thickness_data.""" + from pygui.backend.visualization.wafer_map_item import WaferMapItem + + item = WaferMapItem() + item._rebuild_thickness = MagicMock() + + item.thicknessData = [1.0, 2.0, 3.0] + + assert item._thickness_data == [1.0, 2.0, 3.0] + assert item._rebuild_thickness.called + + +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