cluster4npu/tests/unit/conftest.py
abin 55040733fe feat: implement Phase 1-4 performance visualization and device management
Phase 1 — Performance Benchmarking:
- PerformanceBenchmarker: sequential vs parallel benchmark with injectable runner
- PerformanceHistory: JSON-backed benchmark history with regression support
- PerformanceDashboard: real-time FPS/latency display widget
- BenchmarkDialog: one-click benchmark with 3-phase progress bar

Phase 2 — Device Management:
- DeviceManager: NPU dongle scan, assign/unassign, load balance recommendation
- DeviceManagementPanel: live device status cards with auto-refresh
- BottleneckAlert: dataclass for pipeline bottleneck detection

Phase 3 — Advanced Features:
- OptimizationEngine: 3 optimization rules (rebalance/adjust_queue/add_devices)
- TemplateManager: 3 built-in pipeline templates (YOLOv5, fire detection, dual-model)

Phase 4 — Report Export:
- ReportExporter: PDF (reportlab, optional) and CSV export
- ExportReportDialog: format selection + path picker UI

192 unit tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:32:05 +08:00

296 lines
7.0 KiB
Python
Raw Permalink 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.

"""
pytest conftest.py — 單元測試環境設定。
此測試環境沒有 Kneron NPU 硬體,也沒有 PyQt5 等 GUI 函式庫。
為了能夠測試純 Python 的 core/ 和 ui/ 模組,
在收集測試前預先注入 Mock 模組,避免 import 時觸發硬體/GUI 初始化。
UI 元件測試需要 QWidget 等基底類別可被正常繼承與多次實例化,
因此使用輕量 Stub 取代 MagicMock 作為 PyQt5 Widget 基底。
"""
import sys
from unittest.mock import MagicMock
def _install_mock(name: str) -> None:
"""若模組尚未存在,安裝空 MagicMock 作為替代。"""
if name not in sys.modules:
sys.modules[name] = MagicMock()
# Kneron KP SDK需要硬體驅動程式
_install_mock("kp")
# NumPy可能未安裝
try:
import numpy # noqa: F401
except ImportError:
_install_mock("numpy")
# OpenCV可能未安裝
_install_mock("cv2")
# NodeGraphQt依賴 PyQt5
_install_mock("NodeGraphQt")
_install_mock("NodeGraphQt.constants")
_install_mock("NodeGraphQt.base")
_install_mock("NodeGraphQt.base.node")
# ---------------------------------------------------------------------------
# PyQt5 Stub — 允許 QWidget/QDialog 子類別被正常繼承並多次實例化。
# 使用輕量 Python 類別替代,避免 MagicMock 繼承時的 side_effect 耗盡問題。
# ---------------------------------------------------------------------------
class _StubQObject:
"""所有 Qt 物件的基底 Stub。"""
def __init__(self, *args, **kwargs):
pass
class _StubQWidget(_StubQObject):
"""QWidget Stub可被繼承支援多次實例化。提供常用 QWidget 方法的空實作。"""
def setLayout(self, layout):
pass
def setParent(self, parent):
pass
def show(self):
pass
def hide(self):
pass
def setVisible(self, visible: bool):
pass
def setEnabled(self, enabled: bool):
pass
def isEnabled(self) -> bool:
return True
def setObjectName(self, name: str):
pass
def setStyleSheet(self, style: str):
pass
def setMinimumWidth(self, w: int):
pass
def setMinimumHeight(self, h: int):
pass
def setMaximumWidth(self, w: int):
pass
def setMaximumHeight(self, h: int):
pass
def resize(self, *args):
pass
def setWindowTitle(self, title: str):
pass
def setSizePolicy(self, *args):
pass
def update(self):
pass
def repaint(self):
pass
def close(self):
pass
def font(self):
return MagicMock()
def setFont(self, font):
pass
class _StubQDialog(_StubQWidget):
"""QDialog Stub。"""
Accepted = 1
Rejected = 0
def exec_(self):
return self.Accepted
def accept(self):
pass
def reject(self):
pass
class _StubQLabel(_StubQWidget):
"""QLabel Stub追蹤 setText 呼叫,可在測試中驗證顯示文字。"""
def __init__(self, text: str = "", parent=None):
super().__init__(parent)
self._text = text
self.setText = MagicMock(side_effect=self._set_text)
def _set_text(self, text: str) -> None:
self._text = text
def text(self) -> str:
return self._text
class _StubLayout(_StubQObject):
"""QLayout Stub忽略所有 add* 呼叫。"""
def addWidget(self, *args, **kwargs):
pass
def addLayout(self, *args, **kwargs):
pass
def addStretch(self, *args, **kwargs):
pass
def setSpacing(self, *args, **kwargs):
pass
def setContentsMargins(self, *args, **kwargs):
pass
class _StubQVBoxLayout(_StubLayout):
pass
class _StubQHBoxLayout(_StubLayout):
pass
class _StubQProgressBar(_StubQWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._value = 0
self._maximum = 100
self._minimum = 0
self.setValue = MagicMock(side_effect=self._set_value)
def _set_value(self, v: int) -> None:
self._value = v
def value(self) -> int:
return self._value
def setMaximum(self, v: int) -> None:
self._maximum = v
def setMinimum(self, v: int) -> None:
self._minimum = v
class _StubQTableWidget(_StubQWidget):
def __init__(self, *args, **kwargs):
super().__init__()
self.setItem = MagicMock()
self.setHorizontalHeaderLabels = MagicMock()
class _StubQPushButton(_StubQWidget):
def __init__(self, text: str = "", parent=None):
super().__init__(parent)
self._text = text
self._enabled = True
self.clicked = MagicMock()
self.setEnabled = MagicMock(side_effect=self._set_enabled)
def _set_enabled(self, enabled: bool) -> None:
self._enabled = enabled
def isEnabled(self) -> bool:
return self._enabled
def _make_pyqt_signal(*args, **kwargs):
"""pyqtSignal Stub回傳可 connect/emit 的 MagicMock。"""
signal = MagicMock()
signal.connect = MagicMock()
signal.emit = MagicMock()
return signal
def _make_qthread():
"""QThread Stub。"""
class _StubQThread(_StubQObject):
started = MagicMock()
finished = MagicMock()
def start(self):
pass
def isRunning(self):
return False
def wait(self):
pass
def run(self):
pass
def deleteLater(self):
pass
return _StubQThread
# 建立 PyQt5.QtWidgets Mock 模組(保留 MagicMock 為底,覆蓋關鍵類別)
_qtwidgets_mock = MagicMock()
_qtwidgets_mock.QWidget = _StubQWidget
_qtwidgets_mock.QDialog = _StubQDialog
_qtwidgets_mock.QLabel = _StubQLabel
_qtwidgets_mock.QVBoxLayout = _StubQVBoxLayout
_qtwidgets_mock.QHBoxLayout = _StubQHBoxLayout
_qtwidgets_mock.QProgressBar = _StubQProgressBar
_qtwidgets_mock.QTableWidget = _StubQTableWidget
_qtwidgets_mock.QPushButton = _StubQPushButton
_qtwidgets_mock.QSizePolicy = MagicMock()
_qtwidgets_mock.QTableWidgetItem = MagicMock()
_qtwidgets_mock.QHeaderView = MagicMock()
_qtwidgets_mock.QMessageBox = MagicMock()
_qtwidgets_mock.QApplication = MagicMock()
_qtwidgets_mock.QGroupBox = _StubQWidget
_qtwidgets_mock.QFrame = _StubQWidget
_qtwidgets_mock.QScrollArea = _StubQWidget
_qtwidgets_mock.QSpinBox = _StubQWidget
_qtwidgets_mock.QComboBox = _StubQWidget
_qtwidgets_mock.QCheckBox = _StubQWidget
# 建立 PyQt5.QtCore Mock 模組
_qtcore_mock = MagicMock()
_qtcore_mock.pyqtSignal = _make_pyqt_signal
_qtcore_mock.QThread = _make_qthread()
_qtcore_mock.Qt = MagicMock()
_qtcore_mock.QTimer = MagicMock()
_qtcore_mock.QObject = _StubQObject
# 建立 PyQt5.QtGui Mock 模組
_qtgui_mock = MagicMock()
# 建立頂層 PyQt5 Mock
_pyqt5_mock = MagicMock()
_pyqt5_mock.QtWidgets = _qtwidgets_mock
_pyqt5_mock.QtCore = _qtcore_mock
_pyqt5_mock.QtGui = _qtgui_mock
sys.modules["PyQt5"] = _pyqt5_mock
sys.modules["PyQt5.QtWidgets"] = _qtwidgets_mock
sys.modules["PyQt5.QtCore"] = _qtcore_mock
sys.modules["PyQt5.QtGui"] = _qtgui_mock
sys.modules["PyQt5.QtChart"] = MagicMock()
# pyqtgraph選配
_install_mock("pyqtgraph")