diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 358ab04a..2d3888cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,75 @@ +# Run the project's test suite name: Tests on: - # This avoids having duplicate builds for a pull request push: branches: - master + - main + - '*.x' pull_request: branches: - master + - main + - '*.x' jobs: - linux: - name: Linux Py${{ matrix.PYTHON_VERSION }} conda=${{ matrix.USE_CONDA }} - runs-on: ubuntu-latest + test: + name: Test ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }} + runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.special-invocation }}bash -l {0} env: - CI: True - PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} - USE_CONDA: ${{ matrix.USE_CONDA }} + CI: 'True' + PYTHON_VERSION: ${{ matrix.python-version }} + USE_CONDA: ${{ matrix.use-conda }} + PYQT5_VERSION: ${{ matrix.pyqt5-version }} + PYQT6_VERSION: ${{ matrix.pyqt6-version }} + PYSIDE2_VERSION: ${{ matrix.pyside2-version }} + PYSIDE6_VERSION: ${{ matrix.pyside6-version }} + PYQT5_QT_VERSION: ${{ matrix.pyqt5-qt-version }} + PYQT6_QT_VERSION: ${{ matrix.pyqt6-qt-version }} + PYSIDE2_QT_VERSION: ${{ matrix.pyside2-qt-version }} + PYSIDE6_QT_VERSION: ${{ matrix.pyside6-qt-version }} strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.6', '3.8'] - USE_CONDA: ['Yes', 'No'] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.6', '3.9'] + use-conda: ['Yes', 'No'] + include: + - os: ubuntu-latest + special-invocation: 'xvfb-run --auto-servernum ' + - python-version: '3.6' + use-conda: 'No' + pyside2-version: 5.12.0 # 5.12.1-5.12.6 fails on collection/segfaults on patch test + - os: ubuntu-latest + python-version: '3.9' + use-conda: 'Yes' + coverage: 'True' # Collect coverage only from this run, currently + - os: ubuntu-latest + python-version: '3.6' + use-conda: 'No' + skip-pyqt6: true # No wheels on Py 3.6 Linux CIs + - os: windows-latest + python-version: '3.9' + use-conda: 'No' + pyside2-version: 5.15 # No 5.12 wheel on Windows and Python 3.9 + - os: windows-latest + python-version: '3.6' + use-conda: 'Yes' + pyqt5-qt-version: '5.9' # 5.12 is apparently unreliable here + - os: macos-latest + python-version: '3.6' + use-conda: 'No' + skip-pyqt6: true # No wheels on Py 3.6 macOS CIs steps: - name: Checkout branch uses: actions/checkout@v2 - - name: Install System Packages + - name: Install Linux system packages + if: contains(matrix.os, 'ubuntu') + shell: bash run: | sudo apt update sudo apt install libpulse-dev libegl1-mesa libopengl0 @@ -33,82 +77,31 @@ jobs: uses: conda-incubator/setup-miniconda@v2 with: activate-environment: '' + auto-activate-base: true auto-update-conda: true - auto-activate-base: false - - name: Test PyQt5 + channels: conda-forge + - name: Print Conda info shell: bash -l {0} run: | - eval "$(conda shell.bash hook)" - xvfb-run --auto-servernum bash -l ./.github/workflows/test-pyqt5.sh - - name: Test PySide2 - shell: bash -l {0} - run: xvfb-run --auto-servernum bash -l ./.github/workflows/test-pyside2.sh - - name: Test PySide6 - shell: bash -l {0} - run: xvfb-run --auto-servernum bash -l ./.github/workflows/test-pyside6.sh - - name: Upload coverage - if: matrix.PYTHON_VERSION == '3.8' - shell: bash -l {0} - run: bash -l ./.github/workflows/coverage.sh - - macos: - name: Mac Py${{ matrix.PYTHON_VERSION }} conda=${{ matrix.USE_CONDA }} - runs-on: macos-latest - env: - CI: True - PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} - USE_CONDA: ${{ matrix.USE_CONDA }} - strategy: - fail-fast: false - matrix: - PYTHON_VERSION: ['3.6', '3.8'] - USE_CONDA: ['Yes', 'No'] - steps: - - name: Checkout branch - uses: actions/checkout@v2 - - name: Install Conda - uses: conda-incubator/setup-miniconda@v2 - with: - activate-environment: '' - auto-update-conda: true - auto-activate-base: false - - name: Test PyQt5 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyqt5.sh - - name: Test PySide2 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyside2.sh - - name: Test PySide6 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyside6.sh - - windows: - name: Windows Py${{ matrix.PYTHON_VERSION }} conda=${{ matrix.USE_CONDA }} - runs-on: windows-latest - env: - CI: True - PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} - USE_CONDA: ${{ matrix.USE_CONDA }} - strategy: - fail-fast: false - matrix: - PYTHON_VERSION: ['3.6', '3.8'] - USE_CONDA: ['Yes', 'No'] - steps: - - name: Checkout branch - uses: actions/checkout@v2 - - name: Install Conda - uses: conda-incubator/setup-miniconda@v2 - with: - activate-environment: '' - auto-update-conda: true - auto-activate-base: true + conda info + conda list - name: Test PyQt5 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyqt5.sh + if: (! matrix.skip-pyqt5) + run: ./.github/workflows/test.sh pyqt5 + - name: Test PyQt6 + if: always() && (! ((matrix.skip-pyqt6) || (matrix.use-conda == 'Yes'))) # No conda packages yet for Qt6/PyQt6 + run: ./.github/workflows/test.sh pyqt6 - name: Test PySide2 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyside2.sh + if: always() && (! (matrix.skip-pyside2)) + run: ./.github/workflows/test.sh pyside2 - name: Test PySide6 - shell: bash -l {0} - run: bash -l ./.github/workflows/test-pyside6.sh + if: always() && (! ((matrix.skip-pyside6) || (matrix.use-conda == 'Yes'))) # No conda packages yet for Qt6/Pyside6 + run: ./.github/workflows/test.sh pyside6 + - name: Upload coverage data to coveralls.io + if: matrix.coverage + shell: bash + env: + COVERALLS_REPO_TOKEN: 'xh75EzxFFMoTEyNPo3wXxXv8OVkul3eE5' + run: | + python3 -m pip install --upgrade coveralls + python3 -m coveralls --service=github-actions || true diff --git a/.github/workflows/coverage.sh b/.github/workflows/coverage.sh deleted file mode 100755 index f68ecb3e..00000000 --- a/.github/workflows/coverage.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -eval "$(conda shell.bash hook)" -conda deactivate -conda activate test-pyqt5 - -export COVERALLS_REPO_TOKEN="xh75EzxFFMoTEyNPo3wXxXv8OVkul3eE5" -coveralls - -# Don't fail at this step -exit 0 diff --git a/.github/workflows/test-pyqt5.sh b/.github/workflows/test-pyqt5.sh deleted file mode 100755 index cdd68b26..00000000 --- a/.github/workflows/test-pyqt5.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -ex - -# Create conda environment for this test -conda create -n test-pyqt5 -conda activate test-pyqt5 - -# Select build with QtMultimedia -if [ "$(uname)" == "Darwin" ]; then - - if [ "$PYTHON_VERSION" = "3.6" ]; then - export QT_VER=5.12 - else - export QT_VER=5.* - fi - -elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then - - if [ "$PYTHON_VERSION" = "3.6" ]; then - export QT_VER=5.12 - else - export QT_VER=5.* - fi - -else - - if [ "$PYTHON_VERSION" = "3.6" ]; then - export QT_VER=5.9 - else - export QT_VER=5.* - fi - -fi - -if [ "$USE_CONDA" = "Yes" ]; then - conda install -q coveralls pytest pytest-cov python="$PYTHON_VERSION" -c conda-forge - conda install -q qt=$QT_VER pyqt=$QT_VER -c conda-forge -else - # We are getting segfaults in 5.10 - conda install -q coveralls pytest pytest-cov python="$PYTHON_VERSION" -c anaconda - pip install -q pyqt5 PyQtWebEngine -fi - -# Install package -python -m pip install -e . - -# Run tests -python qtpy/tests/runtests.py diff --git a/.github/workflows/test-pyside2.sh b/.github/workflows/test-pyside2.sh deleted file mode 100755 index 46198351..00000000 --- a/.github/workflows/test-pyside2.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -ex - -# Create conda environment for this test -conda create -n test-pyside2 -conda activate test-pyside2 - -if [ "$USE_CONDA" = "Yes" ]; then - # There are no conda packages for PySide2 - exit 0 -elif [ "$PYTHON_VERSION" != "3.6" ] && [ "$RUNNER_OS" = "Windows" ]; then - # There is no wheel for PySide 5.12 on Windows and Python 3.8 - exit 0 -else - # Simple solution to avoid failures with the Qt3D modules - conda install -q coveralls pytest pytest-cov python="$PYTHON_VERSION" -c conda-forge - pip install -q pyside2==5.12 -fi - -# Install package -python -m pip install -e . - -# Run tests -python qtpy/tests/runtests.py diff --git a/.github/workflows/test-pyside6.sh b/.github/workflows/test-pyside6.sh deleted file mode 100755 index eb927888..00000000 --- a/.github/workflows/test-pyside6.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -ex - -# Create conda environment for this test -conda create -n test-pyside6 -conda activate test-pyside6 - -if [ "$USE_CONDA" = "Yes" ]; then - # There are no conda packages for PySide6 - exit 0 -else - # Simple solution to avoid failures with the Qt3D modules - conda install -q coveralls pytest pytest-cov python="$PYTHON_VERSION" -c conda-forge - pip install -q pyside6==6.2.0 -fi - -# Install package -python -m pip install -e . - -# Run tests -python qtpy/tests/runtests.py diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh new file mode 100755 index 00000000..295304f0 --- /dev/null +++ b/.github/workflows/test.sh @@ -0,0 +1,54 @@ +#!/bin/bash -ex + +# Activate conda properly +eval "$(conda shell.bash hook)" + +# Set conda channel +if [ "$USE_CONDA" = "No" ]; then + CONDA_CHANNEL_ARG="-c anaconda" +fi + +# Create and activate conda environment for this test +conda create -q -n test-${1} ${CONDA_CHANNEL_ARG} python=${PYTHON_VERSION} pytest pytest-cov +conda activate test-${1} + +if [ "$USE_CONDA" = "Yes" ]; then + + if [ "${1}" = "pyqt5" ]; then + conda install -q qt=${PYQT5_QT_VERSION:-"5.12"} pyqt=${PYQT5_VERSION:-"5"} + elif [ "${1}" = "pyside2" ]; then + conda install -q qt=${PYSIDE2_QT_VERSION:-"5.12"} pyside2=${PYSIDE2_VERSION:-"5"} + else + exit 1 + fi + +else + + if [ "${1}" = "pyqt5" ]; then + pip install pyqt5==${PYQT5_VERSION:-"5.15"}.* PyQtWebEngine==${PYQT5_VERSION:-"5.15"}.* + elif [ "${1}" = "pyqt6" ]; then + pip install pyqt6==${PYQT6_VERSION:-"6.2"}.* PyQt6-WebEngine==${PYQT6_VERSION:-"6.2"}.* + elif [ "${1}" = "pyside2" ]; then + pip install pyside2==${PYSIDE2_VERSION:-"5.12"}.* + elif [ "${1}" = "pyside6" ]; then + pip install pyside6==${PYSIDE6_VERSION:-"6.2"}.* + else + exit 1 + fi + +fi + +# Build wheel of package +git clean -xdf +python -bb -X dev -W error setup.py sdist bdist_wheel + +# Install package from build wheel +echo dist/*.whl | xargs -I % python -bb -X dev -W error -m pip install --upgrade % + +# Print environment information +conda list + +# Run tests +cd qtpy +python -I -bb -X dev -W error -m pytest --cov-config ../.coveragerc +cd .. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..19e23c3b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +addopts = --durations=10 -v -r a --color=yes --code-highlight=yes --strict-config --strict-markers --import-mode=importlib --maxfail 10 --cov=qtpy --cov-report=term-missing +empty_parameter_set_mark = fail_at_collect +filterwarnings = + error +log_auto_indent = True +log_level = INFO +minversion = 6.0 +testpaths = + qtpy/tests +xfail_strict = True diff --git a/qtpy/tests/conftest.py b/qtpy/tests/conftest.py index 4cbeb8dd..9322f263 100644 --- a/qtpy/tests/conftest.py +++ b/qtpy/tests/conftest.py @@ -2,9 +2,7 @@ def pytest_configure(config): - """ - This function gets run by py.test at the very start - """ + """Configure the test environment.""" if 'USE_QT_API' in os.environ: os.environ['QT_API'] = os.environ['USE_QT_API'].lower() @@ -15,10 +13,7 @@ def pytest_configure(config): def pytest_report_header(config): - """ - This function is used by py.test to insert a customized header into the - test report. - """ + """Insert a customized header into the test report.""" versions = os.linesep versions += 'PyQt6: ' @@ -67,5 +62,5 @@ def pytest_report_header(config): versions += 'unknown version' versions += os.linesep - + return versions diff --git a/qtpy/tests/runtests.py b/qtpy/tests/runtests.py deleted file mode 100755 index ba43b9e5..00000000 --- a/qtpy/tests/runtests.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# ---------------------------------------------------------------------------- -# Copyright © 2015- The Spyder Development Team -# -# Licensed under the terms of the MIT License -# ---------------------------------------------------------------------------- - -"""File for running tests programmatically.""" - -# Standard library imports -import sys - -# Third party imports -import qtpy -import pytest - - -def main(): - """Run pytest tests.""" - errno = pytest.main(['-x', 'qtpy', '-v', '-rw', '--durations=10', - '--cov=qtpy', '--cov-report=term-missing']) - sys.exit(errno) - -if __name__ == '__main__': - main() diff --git a/qtpy/tests/test_patch_qheaderview.py b/qtpy/tests/test_patch_qheaderview.py index 42e9e3bd..6aaf7c8e 100644 --- a/qtpy/tests/test_patch_qheaderview.py +++ b/qtpy/tests/test_patch_qheaderview.py @@ -18,10 +18,10 @@ def get_qapp(icon_path=None): @pytest.mark.skipif( QT_VERSION.startswith('5.15') or PYSIDE6 or PYQT6 or - ((PYSIDE2) and sys.version_info.major == 3 and sys.version_info.minor == 8 and - (sys.platform == 'darwin' or sys.platform.startswith('linux')) + ((PYSIDE2) and sys.version_info.major == 3 and sys.version_info.minor >= 8 + and (sys.platform == 'darwin' or sys.platform.startswith('linux')) ), - reason="It segfaults with Qt 5.15 and fails with PySide2, Python 3.8, on MacOS and Linux") + reason="Segfaults with Qt 5.15; and PySide2/Python 3.8+ on Mac and Linux") def test_patched_qheaderview(): """ This will test whether QHeaderView has the new methods introduced in Qt5. diff --git a/qtpy/tests/test_qtmultimediawidgets.py b/qtpy/tests/test_qtmultimediawidgets.py index dee6f16d..ac1faefe 100644 --- a/qtpy/tests/test_qtmultimediawidgets.py +++ b/qtpy/tests/test_qtmultimediawidgets.py @@ -1,12 +1,14 @@ import os -import sys import pytest + from qtpy import PYQT5, PYSIDE2 -@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason="Only available in Qt5 bindings") -@pytest.mark.skipif(sys.version_info[0] == 3, - reason="Conda packages don't seem to include QtMultimedia") +@pytest.mark.skipif( + not (PYQT5 or PYSIDE2), reason="Only available in Qt5 bindings") +@pytest.mark.skipif( + os.environ.get('USE_CONDA', 'Yes') == 'Yes', + reason="Conda packages don't seem to include QtMultimedia") def test_qtmultimediawidgets(): """Test the qtpy.QtMultimediaWidgets namespace""" from qtpy import QtMultimediaWidgets @@ -14,4 +16,4 @@ def test_qtmultimediawidgets(): assert QtMultimediaWidgets.QCameraViewfinder is not None assert QtMultimediaWidgets.QGraphicsVideoItem is not None assert QtMultimediaWidgets.QVideoWidget is not None - #assert QtMultimediaWidgets.QVideoWidgetControl is not None + # assert QtMultimediaWidgets.QVideoWidgetControl is not None diff --git a/qtpy/tests/test_qtwinextras.py b/qtpy/tests/test_qtwinextras.py index 261a4875..64b20627 100644 --- a/qtpy/tests/test_qtwinextras.py +++ b/qtpy/tests/test_qtwinextras.py @@ -2,11 +2,14 @@ import sys import pytest -from qtpy import PYSIDE2, PYSIDE6 +from qtpy import PYQT6, PYSIDE2, PYSIDE6 + +@pytest.mark.skipif( + PYQT6 or PYSIDE6, reason="Not availible on Qt6-based bindings") @pytest.mark.skipif( - sys.platform != "win32" or os.environ.get('USE_CONDA', 'Yes') == 'Yes' or PYSIDE6, - reason="Only available in Qt5 bindings > 5.9 (only available with pip in the current CI setup) and Windows platform") + sys.platform != "win32" or os.environ.get('USE_CONDA', 'Yes') == 'Yes', + reason="Only available in Qt5 bindings > 5.9 with pip on Windows in CIs") def test_qtwinextras(): """Test the qtpy.QtWinExtras namespace""" from qtpy import QtWinExtras diff --git a/qtpy/tests/test_uic.py b/qtpy/tests/test_uic.py index 8479de39..86b4312d 100644 --- a/qtpy/tests/test_uic.py +++ b/qtpy/tests/test_uic.py @@ -1,9 +1,11 @@ +import contextlib import os import sys -import contextlib +import warnings import pytest -from qtpy import PYQT5, PYSIDE6, PYSIDE2, QtWidgets + +from qtpy import PYSIDE6, PYSIDE2, QtWidgets from qtpy.QtWidgets import QComboBox if PYSIDE2: @@ -46,31 +48,42 @@ def get_qapp(icon_path=None): return qapp -@pytest.mark.skipif(((PYSIDE2 or PYSIDE6 or PYQT5) - and os.environ.get('CI', None) is not None), - reason="It segfaults in our CIs with PYSIDE2/PYSIDE6 or PYQT5") +@pytest.mark.skipif( + os.environ.get('CI', None) is not None + and sys.platform.startswith('linux'), + reason="Segfaults on Linux CIs under all bindings (PYSIDE2/6 & PYQT5/6)") def test_load_ui(): """ Make sure that the patched loadUi function behaves as expected with a simple .ui file. """ app = get_qapp() - ui = loadUi(os.path.join(os.path.dirname(__file__), 'test.ui')) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, message=".*mode.*") + ui = loadUi(os.path.join(os.path.dirname(__file__), 'test.ui')) assert isinstance(ui.pushButton, QtWidgets.QPushButton) assert isinstance(ui.comboBox, QComboBox) -@pytest.mark.skipif(((PYSIDE2 or PYQT5) - and os.environ.get('CI', None) is not None) or PYSIDE6, - reason="It segfaults in our CIs with PYSIDE2 or PYQT5") +@pytest.mark.skipif( + PYSIDE2 or PYSIDE6, + reason="PySide2uic not consistantly installed across platforms/versions") +@pytest.mark.skipif( + os.environ.get('CI', None) is not None + and sys.platform.startswith('linux'), + reason="Segfaults on Linux CIs under all bindings (PYSIDE2/6 & PYQT5/6)") def test_load_ui_type(): """ Make sure that the patched loadUiType function behaves as expected with a simple .ui file. """ app = get_qapp() - ui_type, ui_base_type = loadUiType( - os.path.join(os.path.dirname(__file__), 'test.ui')) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, message=".*mode.*") + ui_type, ui_base_type = loadUiType( + os.path.join(os.path.dirname(__file__), 'test.ui')) assert ui_type.__name__ == 'Ui_Form' class Widget(ui_base_type, ui_type): @@ -84,9 +97,15 @@ def __init__(self): assert isinstance(ui.comboBox, QComboBox) -@pytest.mark.skipif(((PYSIDE2 or PYSIDE6 or PYQT5) - and os.environ.get('CI', None) is not None), - reason="It segfaults in our CIs with PYSIDE2/PYSIDE6 or PYQT5") +@pytest.mark.skipif( + PYSIDE2 and sys.platform == "darwin" + and sys.version_info.major == 3 and sys.version_info.minor == 9 + and os.environ.get('USE_CONDA', 'No') == 'No', + reason="Fails on this specific platform, at least on our CIs") +@pytest.mark.skipif( + os.environ.get('CI', None) is not None + and sys.platform.startswith('linux'), + reason="Segfaults on Linux CIs under all bindings (PYSIDE2/6 & PYQT5/6)") def test_load_ui_custom_auto(tmpdir): """ Test that we can load a .ui file with custom widgets without having to @@ -98,14 +117,18 @@ def test_load_ui_custom_auto(tmpdir): with enabled_qcombobox_subclass(tmpdir): from qcombobox_subclass import _QComboBoxSubclass - ui = loadUi(os.path.join(os.path.dirname(__file__), 'test_custom.ui')) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, message=".*mode.*") + ui = loadUi( + os.path.join(os.path.dirname(__file__), 'test_custom.ui')) assert isinstance(ui.pushButton, QtWidgets.QPushButton) assert isinstance(ui.comboBox, _QComboBoxSubclass) -@pytest.mark.skipif(PYSIDE6, reason="unavailable") +@pytest.mark.skipif(PYSIDE6, reason="Unavailable on PySide6") def test_load_full_uic(): - """Test that we load the full uic objects for PyQt5.""" + """Test that we load the full uic objects.""" QT_API = os.environ.get('QT_API', '').lower() if QT_API.startswith('pyside'): assert hasattr(uic, 'loadUi')