chore: apply linting fixes and expand ruff coverage to test suite
This commit is contained in:
@@ -3,14 +3,14 @@
|
|||||||
install:
|
install:
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
test:
|
test: lint
|
||||||
uv run pytest tests/
|
uv run pytest tests/
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
uv run ruff check src/
|
uv run ruff check src/ tests/
|
||||||
|
|
||||||
fix:
|
fix:
|
||||||
uv run ruff check src/ --fix
|
uv run ruff check src/ tests/ --fix
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
uv run mypy src/
|
uv run mypy src/
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ISC
|
import ISC
|
||||||
|
|
||||||
|
// TODO P3.1 + P3.2: license grid, "Load License" picker, version binding
|
||||||
|
// THINKING: C# parity requires the dgvLicense grid (Wafer SN, Mfg Date,
|
||||||
|
// License Level, License Date) fed from AES-128-CBC .bin files under
|
||||||
|
// <app_data>/licenses/ — CryptoHelper (backend/crypto/crypto_helper.py)
|
||||||
|
// already has the primitives; the missing piece is the license file model
|
||||||
|
// (P3.1 backend). "Version 0.1.0" below is hardcoded and will drift from
|
||||||
|
// pyproject.toml — expose importlib.metadata.version("pygui") via a context
|
||||||
|
// property instead (P3.2). See docs/pending/alpha-release-polish-plan.md §3.
|
||||||
Popup {
|
Popup {
|
||||||
id: root
|
id: root
|
||||||
modal: true
|
modal: true
|
||||||
|
|||||||
@@ -603,6 +603,14 @@ class SessionController(QObject):
|
|||||||
return average_clusters(edited, getattr(self, "_active_clusters", []))
|
return average_clusters(edited, getattr(self, "_active_clusters", []))
|
||||||
return edited
|
return edited
|
||||||
|
|
||||||
|
# TODO P2.4: throttle live-frame UI updates to ~30Hz
|
||||||
|
# THINKING: every decoded serial frame triggers frameUpdated/stateChanged,
|
||||||
|
# which repaints WaferMapItem + trend chart; at high stream rates the GUI
|
||||||
|
# thread saturates on paint, not decode. Buffer the latest frame and flush
|
||||||
|
# via a 33ms QTimer (latest-wins, no queue growth). Recording must keep
|
||||||
|
# writing every frame — only the UI emit is throttled, so place the timer
|
||||||
|
# here rather than in StreamReader. Depends on nothing; verify with P4.3.
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §2.4.
|
||||||
def _emit_current(self) -> None:
|
def _emit_current(self) -> None:
|
||||||
frame = self._player.current()
|
frame = self._player.current()
|
||||||
if frame is None:
|
if frame is None:
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ _KERNEL = "thin_plate_spline"
|
|||||||
_SMOOTHING = 0.0
|
_SMOOTHING = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# TODO P2.2: decouple grid resolution from output size
|
||||||
|
# THINKING: callers pass widget pixel size as width/height, so RBF evaluation
|
||||||
|
# count scales quadratically with viewport size (600px → 360k evaluations).
|
||||||
|
# Interpolate on a fixed ~100×100 grid and let the caller scale the QImage;
|
||||||
|
# sensor density (≤244 points) carries no more spatial information than that.
|
||||||
|
# Called from WaferMapItem._rebuild_heatmap (see its P2.2/P2.3 TODO).
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §2.2.
|
||||||
def interpolate_field(
|
def interpolate_field(
|
||||||
xs: np.ndarray,
|
xs: np.ndarray,
|
||||||
ys: np.ndarray,
|
ys: np.ndarray,
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ class WaferMapItem(QQuickPaintedItem):
|
|||||||
return idx
|
return idx
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
# TODO P6.1: build batch export on top of this single-frame export
|
||||||
|
# THINKING: export_image() already does the hard part (grabToImage → PNG);
|
||||||
|
# batch export is just a loop over file_browser.files that loads each CSV
|
||||||
|
# through the existing wafer-map pipeline and calls this per file into a
|
||||||
|
# chosen output dir, plus an optional summary CSV of (file, min/max/mean).
|
||||||
|
# No new rendering code needed — see docs/pending/alpha-release-polish-plan.md §6.1.
|
||||||
@Slot(str, result=bool)
|
@Slot(str, result=bool)
|
||||||
def export_image(self, file_path: str) -> bool:
|
def export_image(self, file_path: str) -> bool:
|
||||||
"""Export the current wafer map rendering to a PNG file
|
"""Export the current wafer map rendering to a PNG file
|
||||||
@@ -377,6 +383,13 @@ class WaferMapItem(QQuickPaintedItem):
|
|||||||
r = max(math.hypot(s.x, s.y) for s in self._sensors)
|
r = max(math.hypot(s.x, s.y) for s in self._sensors)
|
||||||
return r * 1.05
|
return r * 1.05
|
||||||
|
|
||||||
|
# TODO P6.2: reuse this for the edge-to-center delta metric
|
||||||
|
# THINKING: radial_metrics.py (new) needs the same "which sensors are near
|
||||||
|
# the edge vs. near the center" bucketing this function already computes
|
||||||
|
# for ring-line drawing — do NOT re-derive ring boundaries separately, this
|
||||||
|
# already handles round vs. square wafer shapes correctly (see family_spec.py
|
||||||
|
# square-wafer work). Outermost group ≈ edge sensors, innermost ≈ center.
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §6.2.
|
||||||
def _sensor_ring_radii_mm(self) -> list[float]:
|
def _sensor_ring_radii_mm(self) -> list[float]:
|
||||||
"""Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary."""
|
"""Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary."""
|
||||||
if not self._sensors:
|
if not self._sensors:
|
||||||
@@ -413,6 +426,15 @@ class WaferMapItem(QQuickPaintedItem):
|
|||||||
self._markers = {i: self._to_px(s.x, s.y, cx, cy, sc)
|
self._markers = {i: self._to_px(s.x, s.y, cx, cy, sc)
|
||||||
for i, s in enumerate(self._sensors)}
|
for i, s in enumerate(self._sensors)}
|
||||||
|
|
||||||
|
# TODO P2.2 + P2.3: fixed-resolution RBF grid, then offload to QThreadPool
|
||||||
|
# THINKING: width=height=ds couples RBF cost to widget pixel size — a 600px
|
||||||
|
# viewport solves a 360k-point system per frame where a fixed 100×100 grid
|
||||||
|
# (10k points, GPU bilinear-scales the QImage up) is visually identical
|
||||||
|
# through the 0.4-opacity blend. Do P2.2 first: it may make P2.3 (QRunnable
|
||||||
|
# worker emitting heatmapReady(QImage), guarding against stale results when
|
||||||
|
# sensors change mid-flight) unnecessary for alpha. Grid width param lands
|
||||||
|
# in interpolate_field (see matching TODO in rbf_heatmap.py).
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §2.2/§2.3.
|
||||||
def _rebuild_heatmap(self) -> None:
|
def _rebuild_heatmap(self) -> None:
|
||||||
if not self._sensors or not self._values or self._blend == 0.0:
|
if not self._sensors or not self._values or self._blend == 0.0:
|
||||||
self._heatmap = None
|
self._heatmap = None
|
||||||
@@ -612,6 +634,13 @@ class WaferMapItem(QQuickPaintedItem):
|
|||||||
ox = getattr(s, "offset_x", 0.0) * r
|
ox = getattr(s, "offset_x", 0.0) * r
|
||||||
oy = getattr(s, "offset_y", 0.0) * r
|
oy = getattr(s, "offset_y", 0.0) * r
|
||||||
|
|
||||||
|
# TODO P2.1: hoist fontMetrics() out of the sensor loop
|
||||||
|
# THINKING: id_font/temp_font are fixed for the whole paint pass,
|
||||||
|
# but fontMetrics() is re-queried per sensor — 65+ OS font-engine
|
||||||
|
# round-trips per frame on a dense wafer, ~30/s in Live mode.
|
||||||
|
# Compute id_fm/temp_fm once before `for i, s in ...` (they only
|
||||||
|
# change when r/font_scale change). Verified by P4.3 benchmark.
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §2.1.
|
||||||
painter.setFont(id_font)
|
painter.setFont(id_font)
|
||||||
id_fm = painter.fontMetrics()
|
id_fm = painter.fontMetrics()
|
||||||
id_line_h = id_fm.height()
|
id_line_h = id_fm.height()
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ def _load_yaml(path: Path) -> dict:
|
|||||||
return yaml.safe_load(f)
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO P6.3: expose this list to a new LayoutSelector.qml (4-button chooser)
|
||||||
|
# THINKING: this already returns everything the UI needs (family names) —
|
||||||
|
# the missing piece is a context-property exposure (SessionController or a
|
||||||
|
# thin new property) and a setLayout(family) slot that calls load_layout()
|
||||||
|
# below. No parsing work needed; this is a wiring-only task.
|
||||||
|
# See docs/pending/alpha-release-polish-plan.md §6.3.
|
||||||
def available_families() -> list[str]:
|
def available_families() -> list[str]:
|
||||||
return [_family_name(_load_yaml(p)["name"]) for p in _LAYOUTS_DIR.glob("*.yaml")]
|
return [_family_name(_load_yaml(p)["name"]) for p in _LAYOUTS_DIR.glob("*.yaml")]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Tests for src/pygui/backend/cluster_average.py."""
|
"""Tests for src/pygui/backend/cluster_average.py."""
|
||||||
import math
|
import math
|
||||||
import pytest
|
|
||||||
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
||||||
from pygui.backend.wafer.zwafer_models import Sensor
|
from pygui.backend.wafer.zwafer_models import Sensor
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Tests for src/pygui/backend/comparison.py."""
|
"""Tests for src/pygui/backend/comparison.py."""
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from pygui.backend.comparison import compare_runs
|
from pygui.backend.comparison import compare_runs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"""Tests for serialcomm/data_parser.py binary parsing pipeline."""
|
"""Tests for serialcomm/data_parser.py binary parsing pipeline."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pygui.serialcomm.data_parser import (
|
from pygui.serialcomm.data_parser import (
|
||||||
|
_convert_hex_to_temp,
|
||||||
|
convert_to_temperatures,
|
||||||
csv_column_count,
|
csv_column_count,
|
||||||
parse_binary_data,
|
parse_binary_data,
|
||||||
convert_to_temperatures,
|
|
||||||
remove_trailing_zeros,
|
remove_trailing_zeros,
|
||||||
save_to_csv,
|
save_to_csv,
|
||||||
_convert_hex_to_temp,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── csv_column_count ──────────────────────────────────────────────────────────
|
# ── csv_column_count ──────────────────────────────────────────────────────────
|
||||||
@@ -271,7 +272,7 @@ class TestSaveToCsv:
|
|||||||
result = save_to_csv(data, "P", "P00001", str(tmp_path))
|
result = save_to_csv(data, "P", "P00001", str(tmp_path))
|
||||||
assert result is not None
|
assert result is not None
|
||||||
headers = open(result).readline().strip().split(",")
|
headers = open(result).readline().strip().split(",")
|
||||||
assert len(headers) == 60, f"data width 60 should win over display 48"
|
assert len(headers) == 60, "data width 60 should win over display 48"
|
||||||
assert headers[0] == "Sensor1"
|
assert headers[0] == "Sensor1"
|
||||||
assert headers[-1] == "Sensor60"
|
assert headers[-1] == "Sensor60"
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import pytest
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from pygui.backend.controllers.device_controller import DeviceController
|
from pygui.backend.controllers.device_controller import DeviceController
|
||||||
from pygui.backend.data.local_settings import LocalSettings
|
from pygui.backend.data.local_settings import LocalSettings
|
||||||
from pygui.serialcomm.serial_port import WaferInfo
|
from pygui.serialcomm.serial_port import WaferInfo
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_settings():
|
def mock_settings():
|
||||||
s = LocalSettings()
|
s = LocalSettings()
|
||||||
@@ -16,8 +19,8 @@ def mock_settings():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def controller(mock_settings):
|
def controller(mock_settings):
|
||||||
import tempfile
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
yield DeviceController(mock_settings, temp_dir)
|
yield DeviceController(mock_settings, temp_dir)
|
||||||
shutil.rmtree(temp_dir)
|
shutil.rmtree(temp_dir)
|
||||||
@@ -159,8 +162,8 @@ def test_logging_handler(controller):
|
|||||||
assert any("Hello QML Activity Log" in line for line in controller._activity_log)
|
assert any("Hello QML Activity Log" in line for line in controller._activity_log)
|
||||||
|
|
||||||
def test_fresh_session_on_startup(mock_settings):
|
def test_fresh_session_on_startup(mock_settings):
|
||||||
import tempfile
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
# Configure mock settings with some data to simulate previous session
|
# Configure mock settings with some data to simulate previous session
|
||||||
mock_settings.activity_log = ["[12:00:00] Old message"]
|
mock_settings.activity_log = ["[12:00:00] Old message"]
|
||||||
@@ -191,7 +194,7 @@ def test_detect_wafer_clears_old_session(controller):
|
|||||||
# We mock detect_all_ports to not run long or fail
|
# We mock detect_all_ports to not run long or fail
|
||||||
controller._service.detect_all_ports = MagicMock(return_value=(None, None))
|
controller._service.detect_all_ports = MagicMock(return_value=(None, None))
|
||||||
|
|
||||||
with patch("threading.Thread") as mock_thread:
|
with patch("threading.Thread"):
|
||||||
controller.detectWafer()
|
controller.detectWafer()
|
||||||
|
|
||||||
# Verify it cleared all session variables and logs immediately at start of detect
|
# Verify it cleared all session variables and logs immediately at start of detect
|
||||||
@@ -227,6 +230,6 @@ def test_device_service_port_caching(controller):
|
|||||||
|
|
||||||
# Clear cache and scan again should invoke subprocess
|
# Clear cache and scan again should invoke subprocess
|
||||||
service.clear_port_scan_cache()
|
service.clear_port_scan_cache()
|
||||||
ports3 = service.enumerate_ports()
|
service.enumerate_ports()
|
||||||
assert mock_run.call_count == 2
|
assert mock_run.call_count == 2
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from pygui.backend.models.frame import Frame
|
from pygui.backend.models.frame import Frame
|
||||||
from pygui.backend.models.frame_player import FramePlayer
|
from pygui.backend.models.frame_player import FramePlayer
|
||||||
|
|
||||||
|
|
||||||
def frames(n):
|
def frames(n):
|
||||||
return [Frame(seq=i, time=float(i), values=[149.0 + i])for i in range(n)]
|
return [Frame(seq=i, time=float(i), values=[149.0 + i])for i in range(n)]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pygui.backend.models.frame_stats import compute_stats, Stats
|
|
||||||
|
from pygui.backend.models.frame_stats import Stats, compute_stats
|
||||||
|
|
||||||
|
|
||||||
def test_basic_stat():
|
def test_basic_stat():
|
||||||
@@ -20,3 +22,9 @@ def test_empty_values_returns_zeros():
|
|||||||
|
|
||||||
def test_ignores_nan():
|
def test_ignores_nan():
|
||||||
s = compute_stats([149.0, float("nan"), 151.0])
|
s = compute_stats([149.0, float("nan"), 151.0])
|
||||||
|
assert s.min == 149.0 and s.min_index == 0
|
||||||
|
assert s.max == 151.0 and s.max_index == 2
|
||||||
|
assert s.diff == pytest.approx(2.0)
|
||||||
|
assert s.avg == pytest.approx(150.0)
|
||||||
|
assert s.sigma == pytest.approx(1.0)
|
||||||
|
assert s.three_sigma == pytest.approx(3.0)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Tests for src/pygui/backend/visualization/graph_view.py."""
|
"""Tests for src/pygui/backend/visualization/graph_view.py."""
|
||||||
import json
|
import json
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def _make_plot_widget():
|
def _make_plot_widget():
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for src/pygui/backend/rbf_heatmap.py."""
|
"""Tests for src/pygui/backend/rbf_heatmap.py."""
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field, BACKEND
|
|
||||||
|
from pygui.backend.visualization.rbf_heatmap import BACKEND, interpolate_field
|
||||||
|
|
||||||
|
|
||||||
def test_field_shape_and_interpolates_at_sites():
|
def test_field_shape_and_interpolates_at_sites():
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Tests for src/pygui/backend/sensor_editor.py."""
|
"""Tests for src/pygui/backend/sensor_editor.py."""
|
||||||
import pytest
|
|
||||||
from pygui.backend.models.sensor_editor import SensorEditor
|
from pygui.backend.models.sensor_editor import SensorEditor
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
from pygui.backend.controllers.session_controller import SessionController
|
from pygui.backend.controllers.session_controller import SessionController
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def controller():
|
def controller():
|
||||||
return SessionController()
|
return SessionController()
|
||||||
@@ -9,7 +12,7 @@ def controller():
|
|||||||
def test_initial_default(controller):
|
def test_initial_default(controller):
|
||||||
assert controller.mode == "review"
|
assert controller.mode == "review"
|
||||||
assert controller.state == "idle"
|
assert controller.state == "idle"
|
||||||
assert controller.recording == False
|
assert not controller.recording
|
||||||
|
|
||||||
def test_playback_flow(controller):
|
def test_playback_flow(controller):
|
||||||
controller.loadFile("tests/fixtures/sample_stream.csv")
|
controller.loadFile("tests/fixtures/sample_stream.csv")
|
||||||
@@ -223,10 +226,10 @@ Item {
|
|||||||
def test_recording_toggle(controller, tmp_path):
|
def test_recording_toggle(controller, tmp_path):
|
||||||
csv_path = str(tmp_path / "rec" / "live_test.csv")
|
csv_path = str(tmp_path / "rec" / "live_test.csv")
|
||||||
controller.startRecording(csv_path, "SN123")
|
controller.startRecording(csv_path, "SN123")
|
||||||
assert controller.recording == True
|
assert controller.recording
|
||||||
|
|
||||||
controller.stopRecording()
|
controller.stopRecording()
|
||||||
assert controller.recording == False
|
assert not controller.recording
|
||||||
|
|
||||||
# Header written with wafer serial
|
# Header written with wafer serial
|
||||||
content = open(csv_path).read()
|
content = open(csv_path).read()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from pygui.backend.models.frame import Frame
|
from pygui.backend.models.frame import Frame
|
||||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, BAND_HIGH, BAND_IN, BAND_LOW
|
from pygui.backend.models.session_model import SessionModel
|
||||||
from pygui.backend.models.session_model import SessionModel, SessionUpdate
|
from pygui.backend.models.threshold_classifier import BAND_HIGH, BAND_IN, BAND_LOW, ThresholdConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_manual_banding_and_stats():
|
def test_manual_banding_and_stats():
|
||||||
@@ -25,4 +24,3 @@ def test_set_thresholds_reband_on_next_frame():
|
|||||||
m.set_thresholds(ThresholdConfig(set_point=149.0, margin=5.0, auto=False))
|
m.set_thresholds(ThresholdConfig(set_point=149.0, margin=5.0, auto=False))
|
||||||
upd = m.process(Frame(seq=0, time=0.0, values=[151.0]))
|
upd = m.process(Frame(seq=0, time=0.0, values=[151.0]))
|
||||||
assert upd.bands == [BAND_IN]
|
assert upd.bands == [BAND_IN]
|
||||||
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from pygui.backend.models.stability_detector import (
|
from pygui.backend.models.stability_detector import (
|
||||||
StabilityDetector, STATE_IDLE, STATE_RAMP, STATE_SET,
|
STATE_IDLE,
|
||||||
|
STATE_RAMP,
|
||||||
|
STATE_SET,
|
||||||
|
StabilityDetector,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SET_POINT = 149.0
|
SET_POINT = 149.0
|
||||||
|
|
||||||
def make():
|
def make():
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from pygui.backend.models.frame import Frame
|
from pygui.backend.models.frame import Frame
|
||||||
from pygui.serialcomm.stream_reader import StreamReader
|
from pygui.serialcomm.stream_reader import StreamReader
|
||||||
|
|
||||||
|
|
||||||
class FakeTransport:
|
class FakeTransport:
|
||||||
"""Yield canned data then blocks (returns b'')."""
|
"""Yield canned data then blocks (returns b'')."""
|
||||||
def __init__(self, lines):
|
def __init__(self, lines):
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from pygui.backend.models.threshold_classifier import (
|
from pygui.backend.models.threshold_classifier import (
|
||||||
ThresholdConfig, classify, classify_all, resolve_bounds,
|
BAND_HIGH,
|
||||||
BAND_IN, BAND_HIGH, BAND_LOW
|
BAND_IN,
|
||||||
|
BAND_LOW,
|
||||||
|
ThresholdConfig,
|
||||||
|
classify,
|
||||||
|
classify_all,
|
||||||
|
resolve_bounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,4 +31,3 @@ def test_auto_bounds_use_mean_and_sigma():
|
|||||||
def test_classify_all_manual():
|
def test_classify_all_manual():
|
||||||
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
||||||
assert classify_all([149.0, 151.0, 147.0], cfg) == [BAND_IN, BAND_HIGH, BAND_LOW]
|
assert classify_all([149.0, 151.0, 147.0], cfg) == [BAND_IN, BAND_HIGH, BAND_LOW]
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for src/pygui/backend/wafer_layouts.py."""
|
"""Tests for src/pygui/backend/wafer_layouts.py."""
|
||||||
import pytest
|
import pytest
|
||||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families
|
|
||||||
|
from pygui.backend.wafer.wafer_layouts import available_families, load_layout
|
||||||
from pygui.backend.wafer.zwafer_models import Sensor
|
from pygui.backend.wafer.zwafer_models import Sensor
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""Tests for src/pygui/backend/visualization/wafer_map_item.py."""
|
"""Tests for src/pygui/backend/visualization/wafer_map_item.py."""
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
# Use the session-scoped qapp fixture from conftest.py so Qt Property/Signal
|
# Use the session-scoped qapp fixture from conftest.py so Qt Property/Signal
|
||||||
# descriptors work properly.
|
# descriptors work properly.
|
||||||
pytestmark = pytest.mark.usefixtures("qapp")
|
pytestmark = pytest.mark.usefixtures("qapp")
|
||||||
|
|||||||
Reference in New Issue
Block a user