forked from masonhuang/cluster4npu
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>
296 lines
7.0 KiB
Python
296 lines
7.0 KiB
Python
"""
|
||
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")
|