diff --git a/.docker/run-tests.sh b/.docker/run-tests.sh new file mode 100755 index 00000000..64986a07 --- /dev/null +++ b/.docker/run-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Run test in docker QGIS image +# + +set -e + +cd /src + +VENV=/src/.docker-venv-$QGIS_VERSION + +python3 -m venv $VENV --system-site-package + +echo "Installing requirements..." +$VENV/bin/pip install -q --no-cache -r REQUIREMENTS_TESTING.txt + +cd tests && $VENV/bin/python -m pytest -v + + diff --git a/.github/workflows/test_plugin.yaml b/.github/workflows/test_plugin.yaml index d55e80f7..d8c24b34 100644 --- a/.github/workflows/test_plugin.yaml +++ b/.github/workflows/test_plugin.yaml @@ -26,31 +26,26 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - docker_tags: [release-3_28, latest] - + qgis_version: [ + "3.34", + "3.40", + ] steps: - name: Checkout - uses: actions/checkout@v2 - - - name: Docker pull and create qgis-testing-environment - run: | - docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }} - docker run -d --name qgis-testing-environment -v "$GITHUB_WORKSPACE":/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} - - - name: Docker set up QGIS - run: | - docker exec qgis-testing-environment sh -c "qgis_setup.sh $PLUGIN_NAME" - docker exec qgis-testing-environment sh -c "rm -f /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" - docker exec qgis-testing-environment sh -c "ln -s /tests_directory/$PLUGIN_NAME /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" - docker exec qgis-testing-environment sh -c "pip3 install -r /tests_directory/REQUIREMENTS_TESTING.txt" - docker exec qgis-testing-environment sh -c "apt-get update" - docker exec qgis-testing-environment sh -c "apt-get install -y python3-pyqt5.qtwebkit" - - - name: Docker run plugin tests - run: | - docker exec qgis-testing-environment sh -c "qgis_testrunner.sh $TESTS_RUN_FUNCTION" + uses: actions/checkout@v5 + + - name: Running tests + run: >- + docker run --quiet --rm --name qgis-DataPlotly-tests + --network host + --user $(id -u):$(id -g) + --mount type=bind,source=$GITHUB_WORKSPACE,target=/src + --workdir /src + --env QGIS_VERSION=${{ matrix.qgis_version }} + $DOCKER_IMAGE:${{ matrix.qgis_version }} .docker/run-tests.sh Check-code-quality: runs-on: ubuntu-latest diff --git a/REQUIREMENTS_TESTING.txt b/REQUIREMENTS_TESTING.txt index 466eb346..7f020612 100644 --- a/REQUIREMENTS_TESTING.txt +++ b/REQUIREMENTS_TESTING.txt @@ -1,8 +1,9 @@ # For tests execution: +pytest deepdiff mock flake8 pep257 plotly pylint -pandas \ No newline at end of file +pandas diff --git a/DataPlotly/test/__init__.py b/tests/__init__.py similarity index 100% rename from DataPlotly/test/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..2adc1304 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,100 @@ +import logging +import os +import sys + +from pathlib import Path + +import pytest + +from qgis.core import Qgis, QgsApplication +from qgis.PyQt import Qt +from qgis.testing import start_app + +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore", category=DeprecationWarning) +# from osgeo import gdal + + +def pytest_report_header(config): + from osgeo import gdal + + message = ( + f"QGIS : {Qgis.QGIS_VERSION_INT}\n" + f"Python GDAL : {gdal.VersionInfo('VERSION_NUM')}\n" + f"Python : {sys.version}\n" + f"QT : {Qt.QT_VERSION_STR}" + ) + return message + + +# +# Fixtures +# + + +@pytest.fixture(scope="session") +def rootdir(request: pytest.FixtureRequest) -> Path: + return Path(request.config.rootdir.strpath) + + +@pytest.fixture(scope="session") +def data(rootdir: Path) -> Path: + return rootdir.joinpath("data") + + +# +# Session +# + + +# Path the 'qgis.utils.iface' property +# Which is not initialized when QGIS app +# is initialized from testing module +def _patch_iface(): + import qgis.utils + + from qgis.testing.mocked import get_iface + + qgis.utils.iface = get_iface() + + +def pytest_sessionstart(session): + """Start qgis application""" + os.environ["QT_QPA_PLATFORM"] = "offscreen" + + sys.path.append("/usr/share/qgis/python") + + # NOTE: we need to prevent cleanup in this case + # because failing tests leads to a segfault. + start_app(cleanup=False) + install_logger_hook(verbose=True) + + # XXX The mock does not work here + + # Patch 'iface' in qgis.utils + #_patch_iface() + + +# +# Logger hook +# + + +def install_logger_hook(verbose: bool = False) -> None: + """Install message log hook""" + from qgis.core import Qgis + + # Add a hook to qgis message log + def writelogmessage(message, tag, level): + arg = f"{tag}: {message}" + if level == Qgis.MessageLevel.Warning: + logging.warning(arg) + elif level == Qgis.MessageLevel.Critical: + logging.error(arg) + elif verbose: + # Qgis is somehow very noisy + # log only if verbose is set + logging.info(arg) + + messageLog = QgsApplication.messageLog() + messageLog.messageReceived.connect(writelogmessage) diff --git a/DataPlotly/test/processing_scatter.html b/tests/processing_scatter.html similarity index 100% rename from DataPlotly/test/processing_scatter.html rename to tests/processing_scatter.html diff --git a/DataPlotly/test/qgis_interface.py b/tests/qgis_interface.py similarity index 100% rename from DataPlotly/test/qgis_interface.py rename to tests/qgis_interface.py diff --git a/DataPlotly/test/scatterplot.json b/tests/scatterplot.json similarity index 100% rename from DataPlotly/test/scatterplot.json rename to tests/scatterplot.json diff --git a/DataPlotly/test/tenbytenraster.asc b/tests/tenbytenraster.asc similarity index 100% rename from DataPlotly/test/tenbytenraster.asc rename to tests/tenbytenraster.asc diff --git a/DataPlotly/test/tenbytenraster.asc.aux.xml b/tests/tenbytenraster.asc.aux.xml similarity index 100% rename from DataPlotly/test/tenbytenraster.asc.aux.xml rename to tests/tenbytenraster.asc.aux.xml diff --git a/DataPlotly/test/tenbytenraster.keywords b/tests/tenbytenraster.keywords similarity index 100% rename from DataPlotly/test/tenbytenraster.keywords rename to tests/tenbytenraster.keywords diff --git a/DataPlotly/test/tenbytenraster.lic b/tests/tenbytenraster.lic similarity index 100% rename from DataPlotly/test/tenbytenraster.lic rename to tests/tenbytenraster.lic diff --git a/DataPlotly/test/tenbytenraster.prj b/tests/tenbytenraster.prj similarity index 100% rename from DataPlotly/test/tenbytenraster.prj rename to tests/tenbytenraster.prj diff --git a/DataPlotly/test/tenbytenraster.qml b/tests/tenbytenraster.qml similarity index 100% rename from DataPlotly/test/tenbytenraster.qml rename to tests/tenbytenraster.qml diff --git a/DataPlotly/test/test_data_plotly_dialog.py b/tests/test_data_plotly_dialog.py similarity index 99% rename from DataPlotly/test/test_data_plotly_dialog.py rename to tests/test_data_plotly_dialog.py index db655b55..f0baf7a3 100644 --- a/DataPlotly/test/test_data_plotly_dialog.py +++ b/tests/test_data_plotly_dialog.py @@ -31,7 +31,8 @@ from DataPlotly.gui.plot_settings_widget import DataPlotlyPanelWidget from DataPlotly.layouts.plot_layout_item import PlotLayoutItem from DataPlotly.layouts.plot_layout_item import PlotLayoutItemMetadata -from DataPlotly.test.utilities import get_qgis_app + +from .utilities import get_qgis_app QGIS_APP, CANVAS, IFACE, PARENT = get_qgis_app() diff --git a/DataPlotly/test/test_dock_manager.py b/tests/test_dock_manager.py similarity index 99% rename from DataPlotly/test/test_dock_manager.py rename to tests/test_dock_manager.py index f83cec41..8b750ba9 100644 --- a/DataPlotly/test/test_dock_manager.py +++ b/tests/test_dock_manager.py @@ -15,10 +15,11 @@ from qgis.PyQt.QtXml import QDomDocument from DataPlotly.core.core_utils import restore, restore_safe_str_xml, safe_str_xml -from DataPlotly.test.utilities import get_qgis_app from DataPlotly.gui.dock import (DataPlotlyDock, DataPlotlyDockManager) from DataPlotly.gui.add_new_dock_dlg import DataPlotlyNewDockIdValidator +from .utilities import get_qgis_app + QGIS_APP, CANVAS, IFACE, PARENT = get_qgis_app() diff --git a/DataPlotly/test/test_guiutils.py b/tests/test_guiutils.py similarity index 100% rename from DataPlotly/test/test_guiutils.py rename to tests/test_guiutils.py diff --git a/DataPlotly/test/test_init.py b/tests/test_init.py similarity index 86% rename from DataPlotly/test/test_init.py rename to tests/test_init.py index b74d5b4c..09bbdaec 100644 --- a/DataPlotly/test/test_init.py +++ b/tests/test_init.py @@ -12,6 +12,8 @@ import logging import configparser +from importlib import resources + LOGGER = logging.getLogger('QGIS') @@ -41,9 +43,13 @@ def test_read_init(self): 'email', 'author'] - file_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), os.pardir, - 'metadata.txt')) + #file_path = os.path.abspath(os.path.join( + # os.path.dirname(__file__), os.pardir, + # 'metadata.txt')) + + file_path = resources.files("DataPlotly").joinpath("metadata.txt") + assert file_path.exists() + LOGGER.info(file_path) metadata = [] parser = configparser.ConfigParser() diff --git a/DataPlotly/test/test_layer.cpg b/tests/test_layer.cpg similarity index 100% rename from DataPlotly/test/test_layer.cpg rename to tests/test_layer.cpg diff --git a/DataPlotly/test/test_layer.dbf b/tests/test_layer.dbf similarity index 100% rename from DataPlotly/test/test_layer.dbf rename to tests/test_layer.dbf diff --git a/DataPlotly/test/test_layer.geojson b/tests/test_layer.geojson similarity index 100% rename from DataPlotly/test/test_layer.geojson rename to tests/test_layer.geojson diff --git a/DataPlotly/test/test_layer.prj b/tests/test_layer.prj similarity index 100% rename from DataPlotly/test/test_layer.prj rename to tests/test_layer.prj diff --git a/DataPlotly/test/test_layer.qpj b/tests/test_layer.qpj similarity index 100% rename from DataPlotly/test/test_layer.qpj rename to tests/test_layer.qpj diff --git a/DataPlotly/test/test_layer.shp b/tests/test_layer.shp similarity index 100% rename from DataPlotly/test/test_layer.shp rename to tests/test_layer.shp diff --git a/DataPlotly/test/test_layer.shx b/tests/test_layer.shx similarity index 100% rename from DataPlotly/test/test_layer.shx rename to tests/test_layer.shx diff --git a/DataPlotly/test/test_plot_factory.py b/tests/test_plot_factory.py similarity index 100% rename from DataPlotly/test/test_plot_factory.py rename to tests/test_plot_factory.py diff --git a/DataPlotly/test/test_plot_settings.py b/tests/test_plot_settings.py similarity index 100% rename from DataPlotly/test/test_plot_settings.py rename to tests/test_plot_settings.py diff --git a/DataPlotly/test/test_processing.py b/tests/test_processing.py similarity index 98% rename from DataPlotly/test/test_processing.py rename to tests/test_processing.py index 3e722b6f..170c1c0f 100644 --- a/DataPlotly/test/test_processing.py +++ b/tests/test_processing.py @@ -3,7 +3,7 @@ import os import json -import processing +from qgis import processing from qgis.core import QgsApplication, QgsVectorLayer from qgis.testing import unittest diff --git a/DataPlotly/test/test_project_with_state.qgs b/tests/test_project_with_state.qgs similarity index 100% rename from DataPlotly/test/test_project_with_state.qgs rename to tests/test_project_with_state.qgs diff --git a/DataPlotly/test/test_project_without_state.qgs b/tests/test_project_without_state.qgs similarity index 100% rename from DataPlotly/test/test_project_without_state.qgs rename to tests/test_project_without_state.qgs diff --git a/DataPlotly/test/test_qgis_environment.py b/tests/test_qgis_environment.py similarity index 100% rename from DataPlotly/test/test_qgis_environment.py rename to tests/test_qgis_environment.py diff --git a/DataPlotly/test/test_resources.py b/tests/test_resources.py similarity index 100% rename from DataPlotly/test/test_resources.py rename to tests/test_resources.py diff --git a/DataPlotly/test/utilities.py b/tests/utilities.py similarity index 74% rename from DataPlotly/test/utilities.py rename to tests/utilities.py index 0e00598b..8ad6a0d4 100644 --- a/DataPlotly/test/utilities.py +++ b/tests/utilities.py @@ -53,11 +53,11 @@ def get_qgis_app(cleanup=True): # Note: QGIS_PREFIX_PATH is evaluated in QgsApplication - # no need to mess with it here. - QGISAPP = QgsApplication(argvb, myGuiFlag) + #QGISAPP = QgsApplication(argvb, myGuiFlag) - QGISAPP.initQgis() - s = QGISAPP.showSettings() - LOGGER.debug(s) + #QGISAPP.initQgis() + #s = QGISAPP.showSettings() + #LOGGER.debug(s) def debug_log_message(message, tag, level): """ @@ -69,23 +69,26 @@ def debug_log_message(message, tag, level): """ print(f'{tag}({level}): {message}') - QgsApplication.instance().messageLog().messageReceived.connect( - debug_log_message) - - if cleanup: - @atexit.register - def exitQgis(): # pylint: disable=unused-variable - """ - Gracefully closes the QgsApplication instance - """ - try: - QGISAPP.exitQgis() # pylint: disable=used-before-assignment - QGISAPP = None # pylint: disable=redefined-outer-name - except NameError: - pass + QGISAPP = QgsApplication.instance() + + #QgsApplication.instance().messageLog().messageReceived.connect( + # debug_log_message) + + # XXX Don't cleanup twice + #if cleanup: + # @atexit.register + # def exitQgis(): # pylint: disable=unused-variable + # """ + # Gracefully closes the QgsApplication instance + # """ + # try: + # QGISAPP.exitQgis() # pylint: disable=used-before-assignment + # QGISAPP = None # pylint: disable=redefined-outer-name + # except NameError: + # pass if PARENT is None: - # noinspection PyPep8Naming + # # noinspection PyPep8Naming PARENT = QWidget() if CANVAS is None: