diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml new file mode 100644 index 00000000..c8b904cc --- /dev/null +++ b/.github/workflows/test_linux.yml @@ -0,0 +1,46 @@ +name: Test Linux + +on: [push, pull_request] + +jobs: + test-linux: + name: Test Linux + runs-on: ubuntu-22.04 + steps: + - name: "Check out the repo" + uses: actions/checkout@v3 + with: + submodules: true + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt update && sudo apt install -y \ + xvfb xauth \ + libgl1 libglx-mesa0 libgl1-mesa-dri libglu1-mesa libegl1-mesa mesa-utils libopengl0 \ + libx11-6 libx11-xcb1 libxext6 libxrender1 libsm6 libice6 libxxf86vm1 libxdamage1 libxfixes3 \ + libxcb-glx0 libxcb-dri2-0 libxcb-dri3-0 libxcb-present0 libxcb-shape0 libxcb-shm0 libxcb-sync1 \ + libxcb-xfixes0 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ + libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 \ + fontconfig fonts-dejavu-core + sudo fc-cache -f + + - name: Install packages + run: | + pip install -e . + pip install .[test] + pip list + + - name: Run tests + env: + LIBGL_ALWAYS_SOFTWARE: "1" + MESA_LOADER_DRIVER_OVERRIDE: "swrast" + GALLIUM_DRIVER: "llvmpipe" + LIBGL_DRIVERS_PATH: "/usr/lib/x86_64-linux-gnu/dri" + QT_OPENGL: "software" + QT_QPA_PLATFORM: "xcb" + uses: coactions/setup-xvfb@v1.0.1 + with: + run: pytest diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4797723a..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,56 +0,0 @@ -shallow_clone: false - -image: - - Ubuntu2204 - - Visual Studio 2019 - -environment: - matrix: - - PYTEST_QT_API: pyqt5 - CODECOV_TOKEN: - secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD - MINICONDA_DIRNAME: C:\FreshMiniconda - -install: - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/download/25.3.1-0/Miniforge3-Linux-x86_64.sh; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Darwin-x86_64.sh; fi - - sh: bash miniconda.sh -b -p $HOME/miniconda - - sh: source $HOME/miniconda/bin/activate - - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/download/24.11.3-2/Miniforge3-Windows-x86_64.exe - - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - - cmd: activate - - conda info - - conda env create -y --name cqgui -f cqgui_env.yml - - sh: source activate cqgui - - cmd: activate cqgui - - conda list - - conda install -y pytest pluggy pytest-qt - - conda install -y pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay - -build: false - -before_test: - - sh: ulimit -c unlimited -S - - sh: sudo rm -f /cores/core.* - -test_script: - - sh: export PYTHONPATH=$(pwd) - - cmd: set PYTHONPATH=%cd% - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi - - cmd: pytest -v --cov=cq_editor - -on_success: - - codecov - -#on_failure: -# - qtdiag -# - ls /cores/core.* -# - lldb --core `ls /cores/core.*` --batch --one-line "bt" - -on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# - sh: export APPVEYOR_SSH_BLOCK=true -# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py index 038f7d31..622bd9ef 100644 --- a/cq_editor/cqe_run.py +++ b/cq_editor/cqe_run.py @@ -8,6 +8,5 @@ from cq_editor.__main__ import main - if __name__ == "__main__": main() diff --git a/cq_editor/widgets/code_editor.py b/cq_editor/widgets/code_editor.py index 98af9cb9..b7946407 100644 --- a/cq_editor/widgets/code_editor.py +++ b/cq_editor/widgets/code_editor.py @@ -4,7 +4,6 @@ from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtGui import QPalette, QColor - DARK_BLUE = QtGui.QColor(118, 150, 185) diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index f28cfbfe..75cda2a7 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -49,8 +49,9 @@ def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) - kernel_manager.kernel.gui = "qt" + kernel_manager.kernel.shell.display_banner = False kernel_manager.kernel.shell.banner1 = "" + kernel_manager.kernel.gui = "qt" self.kernel_client = kernel_client = self._kernel_manager.client() kernel_client.start_channels() @@ -66,6 +67,17 @@ def stop(): self.push_vars(namespace) + def _append_plain_text(self, text, *args, **kwargs): + """ + Overrides the super's method to filter out IPython tips. + """ + # Drop IPython startup tips (including the unicode completion tip) + # Done because turning the banner off does not work + if isinstance(text, str) and text.lstrip().startswith("Tip:"): + return + + return super(ConsoleWidget, self)._append_plain_text(text, *args, **kwargs) + @pyqtSlot(dict) def push_vars(self, variableDict): """ diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index 91855820..1e9dd911 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -38,9 +38,32 @@ def __init__(self, log_widget, *args, **kwargs): self._qobject = _QtLogHandlerQObject() self._qobject.sigRecordEmit.connect(log_widget.append) + self._is_closed = False def emit(self, record): - self._qobject.sigRecordEmit.emit(self.format(record) + "\n") + # Skip emit if handler has been closed + if self._is_closed or self._qobject is None: + # No-op when Qt side is no longer valid + return + + # Protect signal emission against Qt object lifetime race at shutdown + try: + self._qobject.sigRecordEmit.emit(self.format(record) + "\n") + # Catch wrapped C/C++ deletion error during teardown + except RuntimeError: + # Mark handler closed so future emits are ignored + self._is_closed = True + # Drop QObject reference to avoid further access + self._qobject = None + + # Explicit close hook to safely disable handler at teardown + def close(self): + # Mark handler as closed + self._is_closed = True + # Release QObject reference + self._qobject = None + # Preserve base class close behavior + super(QtLogHandler, self).close() class LogViewer(QPlainTextEdit, ComponentMixin): @@ -64,6 +87,9 @@ def __init__(self, *args, **kwargs): self.handler = QtLogHandler(self) + # Ensure handler is closed when widget is destroyed + self.destroyed.connect(lambda *_: self.handler.close()) + def append(self, msg): """Append text to the panel with ANSI escape sequences stipped.""" self.moveCursor(QtGui.QTextCursor.End) diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index 58aac382..b2bebfd3 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -13,7 +13,6 @@ from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode from OCP.Quantity import Quantity_Color - ZOOM_STEP = 0.9 diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index ed42ae27..1e9cf343 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -29,7 +29,6 @@ from pyqtgraph.parametertree import Parameter import qtawesome as qta - DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) DEFAULT_EDGE_WIDTH = 2 diff --git a/pyproject.toml b/pyproject.toml index a64af0d8..1afe10cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ name = "CQ-editor" version = "0.6" dependencies = [ "cadquery", + "PyQt5", "pyqtgraph", "qtawesome==1.4.0", "path", diff --git a/run.py b/run.py index 606ac60f..7401a916 100644 --- a/run.py +++ b/run.py @@ -11,6 +11,5 @@ from cq_editor.__main__ import main - if __name__ == "__main__": main() diff --git a/tests/test_app.py b/tests/test_app.py index 12894dc4..4c1c49f1 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -982,7 +982,7 @@ def test_viewer(main): assert(dummy())""" -def test_module_import(main): +def test_module_import(main, tmp_path, monkeypatch): qtbot, win = main @@ -990,12 +990,17 @@ def test_module_import(main): debugger = win.components["debugger"] traceback_view = win.components["traceback_viewer"] + # isolate test files in a temp directory + monkeypatch.chdir(tmp_path) + # save the dummy module - with open("module.py", "w") as f: - f.write(code_module) + module_file = tmp_path / "module.py" + module_file.write_text(code_module) # run the code importing this module - editor.set_text(code_import) + script_file = tmp_path / "main.py" + script_file.write_text(code_import) + editor.load_from_file(str(script_file)) debugger._actions["Run"][0].triggered.emit() # verify that no exception was generated