""" 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")