diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54a973cb..a5f4c41b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,21 +12,28 @@ stages: - deploy - release -qgis-server: +.tests: + image: ${REGISTRY_URL}/factory-ci-runner:qgis-${QGIS_FLAVOR} stage: test + script: + - source ~/.bashrc + - make install-tests FLAVOR=$QGIS_FLAVOR + - pip list -l + - make test FLAVOR=$QGIS_FLAVOR + tags: + - factory + + +qgis-server: + extends: .tests parallel: matrix: - - QGIS_VERSION: [ - "3.16", + - QGIS_FLAVOR: [ "3.22", "3.28", "3.34", "nightly-release", ] - script: - - make tests FLAVOR=${QGIS_VERSION} - tags: - - infrav3 linter: image: ${REGISTRY_URL}/factory-ci-runner:qgis-ltr diff --git a/lizmap_server/tooltip.py b/lizmap_server/tooltip.py index ffb0339f..04511f27 100755 --- a/lizmap_server/tooltip.py +++ b/lizmap_server/tooltip.py @@ -82,7 +82,7 @@ def create_popup_node_item_from_form( alias = field.alias() name = field.name() fname = alias if alias else name - fname = fname.replace("'", "’") # noqa RUF001 + fname = fname.replace("'", "’") # adapt the view depending on the field type field_widget_setup = field.editorWidgetSetup() diff --git a/requirements/dev.txt b/requirements/dev.txt index 863b4337..a2076ba0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,3 +4,4 @@ mypy mypy-extensions pipdeptree xmldiff +Pillow diff --git a/setup.cfg b/setup.cfg index efb2e711..3acb49c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,16 +29,16 @@ ; .local/lib, ; ./test/.local/lib ; -; [isort] -; multi_line_output = 3 -; include_trailing_comma = True -; use_parentheses = True -; ensure_newline_before_comments = True -; lines_between_types = 1 -; skip = -; .venv, -; .local/, -; .cache/, +[isort] +multi_line_output = 3 +include_trailing_comma = True +use_parentheses = True +ensure_newline_before_comments = True +lines_between_types = 1 +skip = + .venv, + .local/, + .cache/, [qgis-plugin-ci] plugin_path = lizmap_server diff --git a/test/conftest.py b/test/conftest.py index 5ce62555..883a0c75 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,22 +5,22 @@ import sys import warnings -import lxml.etree -import pytest - -from qgis.PyQt import Qt - from typing import Any, Dict, Generator, Optional +import pytest + from qgis.core import Qgis, QgsApplication, QgsFontUtils, QgsProject +from qgis.PyQt import Qt from qgis.server import ( QgsBufferServerRequest, QgsBufferServerResponse, QgsServer, - QgsServerRequest, QgsServerInterface, + QgsServerRequest, ) +from .utils import OWSResponse + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) from osgeo import gdal @@ -82,50 +82,6 @@ def pytest_sessionstart(session): # del qgis_application -NAMESPACES = { - 'xlink': "http://www.w3.org/1999/xlink", - 'wms': "http://www.opengis.net/wms", - 'wfs': "http://www.opengis.net/wfs", - 'wcs': "http://www.opengis.net/wcs", - 'ows': "http://www.opengis.net/ows/1.1", - 'gml': "http://www.opengis.net/gml", - 'xsi': "http://www.w3.org/2001/XMLSchema-instance", -} - - -class OWSResponse: - - def __init__(self, resp: QgsBufferServerResponse) -> None: - self._resp = resp - self._xml = None - - @property - def xml(self) -> 'xml': - if self._xml is None and self._resp.headers().get('Content-Type','').find('text/xml')==0: - self._xml = lxml.etree.fromstring(self.content) - return self._xml - - @property - def content(self) -> bytes: - return bytes(self._resp.body()) - - @property - def status_code(self) -> int: - return self._resp.statusCode() - - @property - def headers(self) -> Dict[str,str]: - return self._resp.headers() - - def xpath(self, path: str) -> lxml.etree.Element: - assert self.xml is not None - return self.xml.xpath(path, namespaces=NAMESPACES) - - def xpath_text(self, path: str) -> str: - assert self.xml is not None - return ' '.join(e.text for e in self.xpath(path)) - - @pytest.fixture(scope='session') def client(request): """ Return a qgis server instance @@ -163,7 +119,12 @@ def get_project(self, name: str) -> QgsProject: raise ValueError("Error reading project '%s':" % projectpath.strpath) return qgsproject - def get(self, query: str, project: Optional[str] = None, headers: Optional[Dict[str, str]] = None) -> OWSResponse: + def get( + self, + query: str, + project: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + ) -> OWSResponse: """ Return server response from query """ if headers is None: @@ -177,7 +138,11 @@ def get(self, query: str, project: Optional[str] = None, headers: Optional[Dict[ self.server.handleRequest(server_request, response, project=qgsproject) return OWSResponse(response) - def get_with_project(self, query: str, project: QgsProject, headers: Optional[Dict[str, str]] = None) -> OWSResponse: + def get_with_project( + self, query: str, + project: QgsProject, + headers: Optional[Dict[str, str]] = None, + ) -> OWSResponse: """ Return server response from query """ if headers is None: diff --git a/test/utils.py b/test/utils.py index 25be92d3..6eb40fca 100644 --- a/test/utils.py +++ b/test/utils.py @@ -2,12 +2,60 @@ import json import xml.etree.ElementTree as ET +from typing import Dict, Union + +import lxml.etree + from PIL import Image +from qgis.server import QgsBufferServerResponse __copyright__ = 'Copyright 2024, 3Liz' __license__ = 'GPL version 3' __email__ = 'info@3liz.org' +NAMESPACES = { + 'xlink': "http://www.w3.org/1999/xlink", + 'wms': "http://www.opengis.net/wms", + 'wfs': "http://www.opengis.net/wfs", + 'wcs': "http://www.opengis.net/wcs", + 'ows': "http://www.opengis.net/ows/1.1", + 'gml': "http://www.opengis.net/gml", + 'xsi': "http://www.w3.org/2001/XMLSchema-instance", +} + + +class OWSResponse: + + def __init__(self, resp: QgsBufferServerResponse) -> None: + self._resp = resp + self._xml = None + + @property + def xml(self) -> 'xml': + if self._xml is None and self._resp.headers().get('Content-Type', '').find('text/xml') == 0: + self._xml = lxml.etree.fromstring(self.content) + return self._xml + + @property + def content(self) -> bytes: + return bytes(self._resp.body()) + + @property + def status_code(self) -> int: + return self._resp.statusCode() + + @property + def headers(self) -> Dict[str, str]: + return self._resp.headers() + + def xpath(self, path: str) -> lxml.etree.Element: + assert self.xml is not None + return self.xml.xpath(path, namespaces=NAMESPACES) + + def xpath_text(self, path: str) -> str: + assert self.xml is not None + return ' '.join(e.text for e in self.xpath(path)) + def _build_query_string(params: dict) -> str: """ Build a query parameter from a dictionary. """ @@ -17,7 +65,9 @@ def _build_query_string(params: dict) -> str: return query_string -def _check_request(result, content_type: str = 'application/json', http_code: int = 200): # noqa ANN401 +def _check_request( + result: OWSResponse, content_type: str = 'application/json', http_code: int = 200, +) -> Union[dict, ET.Element, Image.Image]: """ Check the output and return the content. """ assert result.status_code == http_code, f'HTTP code {result.status_code}, expected {http_code}' assert result.headers.get('Content-Type', '').lower().find(content_type) == 0, f'Headers {result.headers}'