From 3715dd6ce737c8a2624cfd0ae24e0575bfcbb3ae Mon Sep 17 00:00:00 2001 From: Merydian Date: Wed, 20 Mar 2024 14:21:58 +0100 Subject: [PATCH 01/21] squashed testing commits --- .github/workflows/test.yml | 35 ++ ORStools/proc/base_processing_algorithm.py | 1 + ORStools/utils/configmanager.py | 1 + ORStools/utils/processing.py | 1 + README.md | 3 + requirements.txt | 1 + tests/__init__.py | 0 tests/conftest.py | 34 ++ tests/test_common.py | 378 +++++++++++++++++++++ tests/test_gui.py | 64 ++++ tests/test_proc.py | 178 ++++++++++ tests/test_utils.py | 59 ++++ tests/utils/__init__.py | 0 tests/utils/qgis_interface.py | 237 +++++++++++++ tests/utils/utilities.py | 101 ++++++ 15 files changed, 1093 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_common.py create mode 100644 tests/test_gui.py create mode 100644 tests/test_proc.py create mode 100644 tests/test_utils.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/qgis_interface.py create mode 100644 tests/utils/utilities.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e54d25ce --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ + +name: Testing + +on: + push: + pull_request: + +jobs: + test_3_16: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run test 3.16 + run: | + docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:release-3_16 sh -c 'apt-get -y update && apt-get -y install xvfb && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && export DISPLAY=:0.0 && pip install -U pytest && xvfb-run pytest' + env: + DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} + test_3_22: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run test 3.22 + run: | + docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:release-3_22 sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && export DISPLAY=:0.0 && pip install -U pytest && xvfb-run pytest' + env: + DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} + test_latest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run test latest + run: | + docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:latest sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && pip install -U pytest && xvfb-run pytest' + env: + DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 206bc347..7431efaa 100644 --- a/ORStools/proc/base_processing_algorithm.py +++ b/ORStools/proc/base_processing_algorithm.py @@ -26,6 +26,7 @@ * * ***************************************************************************/ """ + from PyQt5.QtCore import QCoreApplication, QSettings from qgis.core import ( QgsProcessing, diff --git a/ORStools/utils/configmanager.py b/ORStools/utils/configmanager.py index 26e4d614..0005e0c4 100644 --- a/ORStools/utils/configmanager.py +++ b/ORStools/utils/configmanager.py @@ -26,6 +26,7 @@ * * ***************************************************************************/ """ + import os import yaml diff --git a/ORStools/utils/processing.py b/ORStools/utils/processing.py index 86c16a97..5d627811 100644 --- a/ORStools/utils/processing.py +++ b/ORStools/utils/processing.py @@ -26,6 +26,7 @@ * * ***************************************************************************/ """ + import os from qgis.core import QgsPointXY diff --git a/README.md b/README.md index 90f21505..e8190c01 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # ORS Tools QGIS plugin +![Testing](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/test.yml/badge.svg) +![Ruff](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/ruff.yml/badge.svg) + ![ORS Tools](https://user-images.githubusercontent.com/23240110/122937401-3ee72400-d372-11eb-8e3b-6c435d1dd964.png) Set of tools for QGIS to use the [openrouteservice](https://openrouteservice.org) (ORS) API. diff --git a/requirements.txt b/requirements.txt index af3ee576..68fc11a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ ruff +pytest diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..d39efd01 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +import os +import yaml + +from ORStools.utils.configmanager import read_config + +with open("ORStools/config.yml", "r+") as file: + data = yaml.safe_load(file) + + +def pytest_sessionstart(session): + """ + Called after the Session object has been created and + before performing collection and entering the run test loop. + """ + if data["providers"][0]["key"] == "": + data["providers"][0]["key"] = os.environ.get("ORS_API_KEY") + with open("ORStools/config.yml", "w") as file: + yaml.dump(data, file) + else: + raise ValueError("API key is not empty.") + + +def pytest_sessionfinish(session, exitstatus): + """ + Called after whole test run finished, right before + returning the exit status to the system. + """ + with open("ORStools/config.yml", "w") as file: + if not data["providers"][0]["key"] == "": + data['providers'][0]['key'] = '' # fmt: skip + yaml.dump(data, file) + + config = read_config() + assert config["providers"][0]["key"] == '' # fmt: skip diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 00000000..5905200d --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,378 @@ +from qgis._core import QgsPointXY +from qgis.testing import unittest + +from ORStools.common import client, directions_core, isochrones_core +import os + + +class TestCommon(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.api_key = os.environ.get("ORS_API_KEY") + if cls.api_key is None: + raise ValueError("ORS_API_KEY environment variable is not set") + + def test_client_request_geometry(self): + test_response = { + "type": "FeatureCollection", + "metadata": { + "id": "1", + "attribution": "openrouteservice.org | OpenStreetMap contributors", + "service": "routing", + "timestamp": 1708505372024, + "query": { + "coordinates": [[8.684101, 50.131613], [8.68534, 50.131651]], + "profile": "driving-car", + "id": "1", + "preference": "fastest", + "format": "geojson", + "geometry": True, + "elevation": True, + }, + "engine": { + "version": "7.1.1", + "build_date": "2024-01-29T14:41:12Z", + "graph_date": "2024-02-18T14:05:28Z", + }, + "system_message": "Preference 'fastest' has been deprecated, using 'recommended'.", + }, + "bbox": [8.684088, 50.131187, 131.0, 8.686212, 50.131663, 133.8], + "features": [ + { + "bbox": [8.684088, 50.131187, 131.0, 8.686212, 50.131663, 133.8], + "type": "Feature", + "properties": { + "ascent": 2.8, + "descent": 0.0, + "transfers": 0, + "fare": 0, + "way_points": [0, 13], + "summary": {"distance": 247.2, "duration": 45.1}, + }, + "geometry": { + "coordinates": [ + [8.684088, 50.131587, 131.0], + [8.684173, 50.13157, 131.0], + [8.684413, 50.131523, 131.0], + [8.684872, 50.131432, 131.0], + [8.685652, 50.131272, 132.1], + [8.685937, 50.131187, 132.7], + [8.686097, 50.131227, 132.9], + [8.686204, 50.131325, 133.1], + [8.686212, 50.13143, 133.3], + [8.686184, 50.13148, 133.4], + [8.68599, 50.131544, 133.6], + [8.685774, 50.131612, 133.7], + [8.685559, 50.131663, 133.7], + [8.68534, 50.13166, 133.8], + ], + "type": "LineString", + }, + } + ], + } + + provider = { + "ENV_VARS": { + "ORS_QUOTA": "X-Ratelimit-Limit", + "ORS_REMAINING": "X-Ratelimit-Remaining", + }, + "base_url": "https://api.openrouteservice.org", + "key": self.api_key, + "name": "openrouteservice", + "timeout": 60, + } + + params = { + "preference": "fastest", + "geometry": "true", + "instructions": "false", + "elevation": True, + "id": 1, + "coordinates": [[8.684101, 50.131613], [8.68534, 50.131651]], + } + agent = "QGIS_ORStools_testing" + profile = "driving-car" + clnt = client.Client(provider, agent) + response = clnt.request("/v2/directions/" + profile + "/geojson", {}, post_json=params) + self.assertAlmostEqual( + response["features"][0]["geometry"], test_response["features"][0]["geometry"] + ) + + def test_output_feature_directions(self): + response = { + "type": "FeatureCollection", + "metadata": { + "id": "1", + "attribution": "openrouteservice.org | OpenStreetMap contributors", + "service": "routing", + "timestamp": 1708522371289, + "query": { + "coordinates": [ + [-68.199488, -16.518187], + [-68.199201, -16.517873], + [-68.198438, -16.518486], + [-68.198067, -16.518183], + ], + "profile": "driving-car", + "id": "1", + "preference": "fastest", + "format": "geojson", + "geometry": True, + "elevation": True, + }, + "engine": { + "version": "7.1.1", + "build_date": "2024-01-29T14:41:12Z", + "graph_date": "2024-02-18T14:05:28Z", + }, + "system_message": "Preference 'fastest' has been deprecated, using 'recommended'.", + }, + "bbox": [-68.199495, -16.518504, 4025.0, -68.198061, -16.51782, 4025.07], + "features": [ + { + "bbox": [-68.199495, -16.518504, 4025.0, -68.198061, -16.51782, 4025.07], + "type": "Feature", + "properties": { + "ascent": 0.1, + "descent": 0.0, + "transfers": 0, + "fare": 0, + "way_points": [0, 2, 6, 9], + "summary": {"distance": 222.4, "duration": 53.0}, + }, + "geometry": { + "coordinates": [ + [-68.199495, -16.518181, 4025.0], + [-68.199485, -16.51817, 4025.0], + [-68.199206, -16.517869, 4025.0], + [-68.199161, -16.51782, 4025.0], + [-68.198799, -16.518142, 4025.0], + [-68.198393, -16.518478, 4025.0], + [-68.198417, -16.518504, 4025.0], + [-68.198393, -16.518478, 4025.0], + [-68.198078, -16.518162, 4025.0], + [-68.198061, -16.518177, 4025.1], + ], + "type": "LineString", + }, + } + ], + } + profile = "driving-car" + preference = "fastest" + feature = directions_core.get_output_feature_directions(response, profile, preference) + coordinates = [(vertex.x(), vertex.y()) for vertex in feature.geometry().vertices()] + test_coords = [ + (-68.199495, -16.518181), + (-68.199485, -16.51817), + (-68.199206, -16.517869), + (-68.199161, -16.51782), + (-68.198799, -16.518142), + (-68.198393, -16.518478), + (-68.198417, -16.518504), + (-68.198393, -16.518478), + (-68.198078, -16.518162), + (-68.198061, -16.518177), + ] + + self.assertAlmostEqual(coordinates, test_coords) + + def test_output_features_optimization(self): + response = { + "code": 0, + "summary": { + "cost": 36, + "routes": 1, + "unassigned": 0, + "setup": 0, + "service": 0, + "duration": 36, + "waiting_time": 0, + "priority": 0, + "distance": 152, + "violations": [], + "computing_times": {"loading": 23, "solving": 0, "routing": 12}, + }, + "unassigned": [], + "routes": [ + { + "vehicle": 0, + "cost": 36, + "setup": 0, + "service": 0, + "duration": 36, + "waiting_time": 0, + "priority": 0, + "distance": 152, + "steps": [ + { + "type": "start", + "location": [-68.193407, -16.472978], + "setup": 0, + "service": 0, + "waiting_time": 0, + "arrival": 0, + "duration": 0, + "violations": [], + "distance": 0, + }, + { + "type": "job", + "location": [-68.192889, -16.472475], + "id": 0, + "setup": 0, + "service": 0, + "waiting_time": 0, + "job": 0, + "arrival": 18, + "duration": 18, + "violations": [], + "distance": 76, + }, + { + "type": "end", + "location": [-68.193407, -16.472978], + "setup": 0, + "service": 0, + "waiting_time": 0, + "arrival": 36, + "duration": 36, + "violations": [], + "distance": 152, + }, + ], + "violations": [], + "geometry": "lkpcBd_f_LuBiAtBhA", + } + ], + } + profile = "driving-car" + preference = "fastest" + feature = directions_core.get_output_features_optimization(response, profile, preference) + coordinates = [(vertex.x(), vertex.y()) for vertex in feature.geometry().vertices()] + + test_coords = [(-68.19331, -16.47303), (-68.19294, -16.47244), (-68.19331, -16.47303)] + self.assertAlmostEqual(coordinates, test_coords) + + def test_build_default_parameters(self): + preference, point_list, coordinates, options = ( + "fastest", + [ + QgsPointXY(-68.1934067732971414, -16.47297756153070125), + QgsPointXY(-68.19288936751472363, -16.47247452813111934), + ], + None, + {}, + ) + params = directions_core.build_default_parameters( + preference, point_list, coordinates, options + ) + test_params = { + "coordinates": [[-68.193407, -16.472978], [-68.192889, -16.472475]], + "preference": "fastest", + "geometry": "true", + "instructions": "false", + "elevation": True, + "id": None, + "options": {}, + } + + self.assertAlmostEqual(params, test_params) + + def test_isochrones(self): + response = { + "type": "FeatureCollection", + "metadata": { + "attribution": "openrouteservice.org | OpenStreetMap contributors", + "service": "isochrones", + "timestamp": 1710421093483, + "query": { + "profile": "driving-car", + "locations": [[-112.594673, 43.554193]], + "location_type": "start", + "range": [60.0], + "range_type": "time", + "options": {}, + "attributes": ["total_pop"], + }, + "engine": { + "version": "7.1.1", + "build_date": "2024-01-29T14:41:12Z", + "graph_date": "2024-03-10T15:19:08Z", + }, + }, + "bbox": [-112.637014, 43.548994, -112.550441, 43.554343], + "features": [ + { + "type": "Feature", + "properties": { + "group_index": 0, + "value": 60.0, + "center": [-112.5946738217447, 43.55409137088865], + "total_pop": 0.0, + }, + "geometry": { + "coordinates": [ + [ + [-112.637014, 43.549342], + [-112.63692, 43.548994], + [-112.631205, 43.550527], + [-112.625496, 43.552059], + [-112.623482, 43.552518], + [-112.617781, 43.553548], + [-112.615319, 43.553798], + [-112.612783, 43.553937], + [-112.61154, 43.553971], + [-112.609679, 43.553977], + [-112.607819, 43.553983], + [-112.603711, 43.553958], + [-112.599603, 43.553932], + [-112.598575, 43.553928], + [-112.594187, 43.553909], + [-112.593002, 43.553904], + [-112.588772, 43.553886], + [-112.587429, 43.553881], + [-112.578142, 43.553673], + [-112.568852, 43.553464], + [-112.559651, 43.553232], + [-112.55045, 43.553], + [-112.550441, 43.55336], + [-112.559642, 43.553592], + [-112.568844, 43.553824], + [-112.578134, 43.554032], + [-112.587427, 43.554241], + [-112.58877, 43.554246], + [-112.593, 43.554264], + [-112.594186, 43.554269], + [-112.598573, 43.554288], + [-112.599601, 43.554292], + [-112.603709, 43.554318], + [-112.607817, 43.554343], + [-112.60968, 43.554337], + [-112.611541, 43.554331], + [-112.612793, 43.554297], + [-112.614041, 43.554262], + [-112.615348, 43.554157], + [-112.616646, 43.554052], + [-112.617826, 43.553905], + [-112.618998, 43.553758], + [-112.620272, 43.553544], + [-112.621537, 43.553331], + [-112.623562, 43.552869], + [-112.625576, 43.55241], + [-112.631298, 43.550875], + [-112.637014, 43.549342], + ] + ], + "type": "Polygon", + }, + } + ], + } + id_field_value = None + isochrones = isochrones_core.Isochrones() + isochrones.set_parameters("driving-car", "time", 60) + + feats = isochrones.get_features(response, id_field_value) + self.assertAlmostEqual(next(feats).geometry().area(), 3.176372365487623e-05) diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 00000000..306a8111 --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,64 @@ +from qgis.testing import unittest + +from qgis.PyQt.QtTest import QTest +from qgis.PyQt.QtCore import Qt, QEvent, QPoint +from qgis.PyQt.QtWidgets import QPushButton +from qgis.gui import QgsMapCanvas, QgsMapMouseEvent +from qgis.core import ( + QgsCoordinateReferenceSystem, + QgsRectangle, +) +import pytest + +from tests.utils.utilities import get_qgis_app + +CANVAS: QgsMapCanvas +QGISAPP, CANVAS, IFACE, PARENT = get_qgis_app() + + +@pytest.mark.filterwarnings("ignore:.*imp module is deprecated.*") +class TestGui(unittest.TestCase): + def test_ORStoolsDialog(self): + from ORStools.gui.ORStoolsDialog import ORStoolsDialog + from ORStools.utils import maptools + + CRS = QgsCoordinateReferenceSystem.fromEpsgId(3857) + CANVAS.setExtent(QgsRectangle(258889, 7430342, 509995, 7661955)) + CANVAS.setDestinationCrs(CRS) + + dlg = ORStoolsDialog(IFACE) + dlg.open() + self.assertTrue(dlg.isVisible()) + + map_button: QPushButton = dlg.routing_fromline_map + # click 'routing_fromline_map' + QTest.mouseClick(map_button, Qt.LeftButton) + self.assertFalse(dlg.isVisible()) + self.assertIsInstance(CANVAS.mapTool(), maptools.LineTool) + + map_dclick = QgsMapMouseEvent( + CANVAS, + QEvent.MouseButtonDblClick, + QPoint(5, 5), # Relative to the canvas' dimensions + Qt.LeftButton, + Qt.LeftButton, + Qt.NoModifier, + ) + + map_click = QgsMapMouseEvent( + CANVAS, + QEvent.MouseButtonRelease, + QPoint(0, 0), # Relative to the canvas' dimensions + Qt.LeftButton, + Qt.LeftButton, + Qt.NoModifier, + ) + # click on canvas at [0, 0] + dlg.line_tool.canvasReleaseEvent(map_click) + # doubleclick on canvas at [5, 5] + dlg.line_tool.canvasDoubleClickEvent(map_dclick) + + self.assertTrue(dlg.isVisible()) + self.assertAlmostEqual( + dlg.routing_fromline_list.item(0).text(), "Point 0: -0.187575, 56.516620" + ) diff --git a/tests/test_proc.py b/tests/test_proc.py new file mode 100644 index 00000000..b6b30e3f --- /dev/null +++ b/tests/test_proc.py @@ -0,0 +1,178 @@ +from qgis._core import ( + QgsPointXY, + QgsProcessingFeedback, + QgsProcessingContext, + QgsProcessingUtils, + QgsVectorLayer, + QgsFeature, + QgsGeometry, +) +from qgis.testing import unittest + +from ORStools.proc.directions_lines_proc import ORSDirectionsLinesAlgo +from ORStools.proc.directions_points_layer_proc import ORSDirectionsPointsLayerAlgo +from ORStools.proc.directions_points_layers_proc import ORSDirectionsPointsLayersAlgo +from ORStools.proc.isochrones_layer_proc import ORSIsochronesLayerAlgo +from ORStools.proc.isochrones_point_proc import ORSIsochronesPointAlgo +from ORStools.proc.matrix_proc import ORSMatrixAlgo + + +class TestProc(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + uri = "point?crs=epsg:4326" + cls.point_layer_1 = QgsVectorLayer(uri, "Scratch point layer", "memory") + points_of_interest = [QgsPointXY(-118.2394, 34.0739), QgsPointXY(-118.3215, 34.1399)] + for point in points_of_interest: + feature = QgsFeature() + feature.setGeometry(QgsGeometry.fromPointXY(point)) + cls.point_layer_1.dataProvider().addFeatures([feature]) + + cls.point_layer_2 = QgsVectorLayer(uri, "Scratch point layer", "memory") + points_of_interest = [QgsPointXY(-118.5, 34.2), QgsPointXY(-118.5, 34.3)] + for point in points_of_interest: + feature = QgsFeature() + feature.setGeometry(QgsGeometry.fromPointXY(point)) + cls.point_layer_2.dataProvider().addFeatures([feature]) + + cls.line_layer = QgsVectorLayer(uri, "Scratch point layer", "memory") + vertices = [(-118.2394, 34.0739), (-118.3215, 34.1341), (-118.4961, 34.5)] + line_geometry = QgsGeometry.fromPolylineXY([QgsPointXY(x, y) for x, y in vertices]) + feature = QgsFeature() + feature.setGeometry(line_geometry) + cls.line_layer.dataProvider().addFeatures([feature]) + + cls.feedback = QgsProcessingFeedback() + cls.context = QgsProcessingContext() + + def test_directions_lines(self): + parameters = { + "INPUT_AVOID_BORDERS": None, + "INPUT_AVOID_COUNTRIES": "", + "INPUT_AVOID_FEATURES": [], + "INPUT_AVOID_POLYGONS": None, + "INPUT_LAYER_FIELD": None, + "INPUT_LINE_LAYER": self.line_layer, + "INPUT_OPTIMIZE": None, + "INPUT_PREFERENCE": 0, + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_METRIC": 0, + "LOCATION_TYPE": 0, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + directions = ORSDirectionsLinesAlgo().create() + dest_id = directions.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) + + def test_directions_points_layer(self): + parameters = { + "INPUT_AVOID_BORDERS": None, + "INPUT_AVOID_COUNTRIES": "", + "INPUT_AVOID_FEATURES": [], + "INPUT_AVOID_POLYGONS": None, + "INPUT_LAYER_FIELD": None, + "INPUT_OPTIMIZE": None, + "INPUT_POINT_LAYER": self.point_layer_1, + "INPUT_PREFERENCE": 0, + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_SORTBY": None, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + directions = ORSDirectionsPointsLayerAlgo().create() + dest_id = directions.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) + + def test_directions_points_layers(self): + parameters = { + "INPUT_AVOID_BORDERS": None, + "INPUT_AVOID_COUNTRIES": "", + "INPUT_AVOID_FEATURES": [], + "INPUT_AVOID_POLYGONS": None, + "INPUT_END_FIELD": None, + "INPUT_END_LAYER": self.point_layer_1, + "INPUT_MODE": 0, + "INPUT_PREFERENCE": 0, + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_SORT_END_BY": None, + "INPUT_SORT_START_BY": None, + "INPUT_START_FIELD": None, + "INPUT_START_LAYER": self.point_layer_2, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + directions = ORSDirectionsPointsLayersAlgo().create() + dest_id = directions.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) + + def test_isochrones_layer(self): + parameters = { + "INPUT_AVOID_BORDERS": None, + "INPUT_AVOID_COUNTRIES": "", + "INPUT_AVOID_FEATURES": [], + "INPUT_AVOID_POLYGONS": None, + "INPUT_FIELD": None, + "INPUT_METRIC": 0, + "INPUT_POINT_LAYER": self.point_layer_1, + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_RANGES": "5, 10", + "INPUT_SMOOTHING": None, + "LOCATION_TYPE": 0, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + iso = ORSIsochronesLayerAlgo().create() + dest_id = iso.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) + + def test_isochrones_point(self): + parameters = { + "INPUT_AVOID_BORDERS": None, + "INPUT_AVOID_COUNTRIES": "", + "INPUT_AVOID_FEATURES": [], + "INPUT_AVOID_POLYGONS": None, + "INPUT_METRIC": 0, + "INPUT_POINT": "-12476269.994314,3961968.635469 [EPSG:3857]", + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_RANGES": "5, 10", + "INPUT_SMOOTHING": None, + "LOCATION_TYPE": 0, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + iso = ORSIsochronesPointAlgo().create() + dest_id = iso.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) + + def test_matrix(self): + parameters = { + "INPUT_END_FIELD": None, + "INPUT_END_LAYER": self.point_layer_1, + "INPUT_PROFILE": 0, + "INPUT_PROVIDER": 0, + "INPUT_START_FIELD": None, + "INPUT_START_LAYER": self.point_layer_2, + "OUTPUT": "TEMPORARY_OUTPUT", + } + + matrix = ORSMatrixAlgo().create() + dest_id = matrix.processAlgorithm(parameters, self.context, self.feedback) + processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) + + self.assertEqual(type(processed_layer), QgsVectorLayer) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..e7fccc87 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,59 @@ +from qgis.testing import unittest + +from qgis.core import QgsCoordinateReferenceSystem, QgsPointXY + +from ORStools.utils.transform import transformToWGS +from ORStools.utils.convert import decode_polyline +from ORStools.utils.processing import get_params_optimize + + +class TestUtils(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.WGS = QgsCoordinateReferenceSystem.fromEpsgId(4326) + cls.PSEUDO = QgsCoordinateReferenceSystem.fromEpsgId(3857) + + def test_to_wgs_pseudo(self): + point = QgsPointXY(1493761.05913532, 6890799.81730105) + transformer = transformToWGS(self.PSEUDO) + self.assertEqual( + transformer.transform(point), QgsPointXY(13.41868390243822162, 52.49867709045137332) + ) + + def test_polyline_convert(self): + polyline = "psvcBxg}~KAGUoBMo@Ln@TnB@F" + decoded = decode_polyline(polyline) + self.assertEqual( + decoded, + [ + [-68.14861, -16.50505], + [-68.14857, -16.50504], + [-68.14801, -16.50493], + [-68.14777, -16.50486], + [-68.14801, -16.50493], + [-68.14857, -16.50504], + [-68.14861, -16.50505], + ], + ) + + def test_get_params_optimize(self): + points = [ + QgsPointXY(-68.14860459410432725, -16.5050554680791457), + QgsPointXY(-68.14776841920792094, -16.50487191749212812), + ] + profile = "driving-car" + mode = 0 + + params = { + "jobs": [{"location": [-68.147768, -16.504872], "id": 0}], + "vehicles": [ + { + "id": 0, + "profile": "driving-car", + "start": [-68.148605, -16.505055], + "end": [-68.148605, -16.505055], + } + ], + "options": {"g": True}, + } + self.assertEqual(get_params_optimize(points, profile, mode), params) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/qgis_interface.py b/tests/utils/qgis_interface.py new file mode 100644 index 00000000..6b157f73 --- /dev/null +++ b/tests/utils/qgis_interface.py @@ -0,0 +1,237 @@ +"""QGIS plugin implementation. + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +.. note:: This source code was copied from the 'postgis viewer' application + with original authors: + Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk + Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org + Copyright (c) 2014 Tim Sutton, tim@linfiniti.com + +""" + +__author__ = "tim@linfiniti.com" +__revision__ = "$Format:%H$" +__date__ = "10/01/2011" +__copyright__ = ( + "Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and " + "Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org" + "Copyright (c) 2014 Tim Sutton, tim@linfiniti.com" +) + +import logging +from typing import List +from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QSize +from qgis.PyQt.QtWidgets import QDockWidget +from qgis.core import QgsProject, QgsMapLayer +from qgis.gui import QgsMapCanvas, QgsMessageBar + +LOGGER = logging.getLogger("QGIS") + + +# noinspection PyMethodMayBeStatic,PyPep8Naming +# pylint: disable=too-many-public-methods +class QgisInterface(QObject): + """Class to expose QGIS objects and functions to plugins. + + This class is here for enabling us to run unit tests only, + so most methods are simply stubs. + """ + + currentLayerChanged = pyqtSignal(QgsMapLayer) + + def __init__(self, canvas: QgsMapCanvas): + """Constructor + :param canvas: + """ + QObject.__init__(self) + self.canvas = canvas + # Set up slots so we can mimic the behaviour of QGIS when layers + # are added. + LOGGER.debug("Initialising canvas...") + # noinspection PyArgumentList + QgsProject.instance().layersAdded.connect(self.addLayers) + # noinspection PyArgumentList + QgsProject.instance().layerWasAdded.connect(self.addLayer) + # noinspection PyArgumentList + QgsProject.instance().removeAll.connect(self.removeAllLayers) + + # For processing module + self.destCrs = None + + self.message_bar = QgsMessageBar() + + def addLayers(self, layers: List[QgsMapLayer]): + """Handle layers being added to the registry so they show up in canvas. + + :param layers: list list of map layers that were added + + .. note:: The QgsInterface api does not include this method, + it is added here as a helper to facilitate testing. + """ + # LOGGER.debug('addLayers called on qgis_interface') + # LOGGER.debug('Number of layers being added: %s' % len(layers)) + # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) + current_layers = self.canvas.layers() + final_layers = [] + for layer in current_layers: + final_layers.append(layer) + for layer in layers: + final_layers.append(layer) + + self.canvas.setLayers(final_layers) + # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) + + def addLayer(self, layer: QgsMapLayer): + """Handle a layer being added to the registry so it shows up in canvas. + + :param layer: list list of map layers that were added + + .. note: The QgsInterface api does not include this method, it is added + here as a helper to facilitate testing. + + .. note: The addLayer method was deprecated in QGIS 1.8 so you should + not need this method much. + """ + pass # pylint: disable=unnecessary-pass + + @pyqtSlot() + def removeAllLayers(self): # pylint: disable=no-self-use + """Remove layers from the canvas before they get deleted.""" + self.canvas.setLayers([]) + + def newProject(self): # pylint: disable=no-self-use + """Create new project.""" + # noinspection PyArgumentList + QgsProject.instance().clear() + + # ---------------- API Mock for QgsInterface follows ------------------- + + def zoomFull(self): + """Zoom to the map full extent.""" + pass # pylint: disable=unnecessary-pass + + def zoomToPrevious(self): + """Zoom to previous view extent.""" + pass # pylint: disable=unnecessary-pass + + def zoomToNext(self): + """Zoom to next view extent.""" + pass # pylint: disable=unnecessary-pass + + def zoomToActiveLayer(self): + """Zoom to extent of active layer.""" + pass # pylint: disable=unnecessary-pass + + def addVectorLayer(self, path: str, base_name: str, provider_key: str): + """Add a vector layer. + + :param path: Path to layer. + :type path: str + + :param base_name: Base name for layer. + :type base_name: str + + :param provider_key: Provider key e.g. 'ogr' + :type provider_key: str + """ + pass # pylint: disable=unnecessary-pass + + def addRasterLayer(self, path: str, base_name: str): + """Add a raster layer given a raster layer file name + + :param path: Path to layer. + :type path: str + + :param base_name: Base name for layer. + :type base_name: str + """ + pass # pylint: disable=unnecessary-pass + + def activeLayer(self) -> QgsMapLayer: # pylint: disable=no-self-use + """Get pointer to the active layer (layer selected in the legend).""" + # noinspection PyArgumentList + layers = QgsProject.instance().mapLayers() + for item in layers: + return layers[item] + + def addToolBarIcon(self, action): + """Add an icon to the plugins toolbar. + + :param action: Action to add to the toolbar. + :type action: QAction + """ + pass # pylint: disable=unnecessary-pass + + def removeToolBarIcon(self, action): + """Remove an action (icon) from the plugin toolbar. + + :param action: Action to add to the toolbar. + :type action: QAction + """ + pass # pylint: disable=unnecessary-pass + + def addToolBar(self, name): + """Add toolbar with specified name. + + :param name: Name for the toolbar. + :type name: str + """ + pass # pylint: disable=unnecessary-pass + + def mapCanvas(self) -> QgsMapCanvas: + """Return a pointer to the map canvas.""" + return self.canvas + + def mainWindow(self): + """Return a pointer to the main window. + + In case of QGIS it returns an instance of QgisApp. + """ + pass # pylint: disable=unnecessary-pass + + def addDockWidget(self, area, dock_widget: QDockWidget): + """Add a dock widget to the main window. + + :param area: Where in the ui the dock should be placed. + :type area: + + :param dock_widget: A dock widget to add to the UI. + :type dock_widget: QDockWidget + """ + pass # pylint: disable=unnecessary-pass + + def removeDockWidget(self, dock_widget: QDockWidget): + """Remove a dock widget to the main window. + + :param area: Where in the ui the dock should be placed. + :type area: + + :param dock_widget: A dock widget to add to the UI. + :type dock_widget: QDockWidget + """ + pass # pylint: disable=unnecessary-pass + + def legendInterface(self): + """Get the legend.""" + return self.canvas + + def iconSize(self, dockedToolbar) -> int: # pylint: disable=no-self-use + """ + Returns the toolbar icon size. + :param dockedToolbar: If True, the icon size + for toolbars contained within docks is returned. + """ + if dockedToolbar: + return QSize(16, 16) + + return QSize(24, 24) + + def messageBar(self) -> QgsMessageBar: + """ + Return the message bar of the main app + """ + return self.message_bar diff --git a/tests/utils/utilities.py b/tests/utils/utilities.py new file mode 100644 index 00000000..54af22b5 --- /dev/null +++ b/tests/utils/utilities.py @@ -0,0 +1,101 @@ +"""Common functionality used by regression tests.""" + +import sys +import logging +import os +import atexit +from qgis.core import QgsApplication +from qgis.gui import QgsMapCanvas +from qgis.PyQt.QtCore import QSize +from qgis.PyQt.QtWidgets import QWidget +from qgis.utils import iface +from tests.utils.qgis_interface import QgisInterface + +LOGGER = logging.getLogger("QGIS") +QGIS_APP = None # Static variable used to hold hand to running QGIS app +CANVAS = None +PARENT = None +IFACE = None + + +def get_qgis_app(cleanup=True): + """Start one QGIS application to test against. + + :returns: Handle to QGIS app, canvas, iface and parent. If there are any + errors the tuple members will be returned as None. + :rtype: (QgsApplication, CANVAS, IFACE, PARENT) + + If QGIS is already running the handle to that app will be returned. + """ + + global QGIS_APP, PARENT, IFACE, CANVAS # pylint: disable=W0603 + + if iface: + QGIS_APP = QgsApplication + CANVAS = iface.mapCanvas() + PARENT = iface.mainWindow() + IFACE = iface + return QGIS_APP, CANVAS, IFACE, PARENT + + global QGISAPP # pylint: disable=global-variable-undefined + + try: + QGISAPP # pylint: disable=used-before-assignment + except NameError: + myGuiFlag = False # All test will run qgis not in gui mode + + # In python3 we need to convert to a bytes object (or should + # QgsApplication accept a QString instead of const char* ?) + try: + argvb = list(map(os.fsencode, sys.argv)) + except AttributeError: + argvb = sys.argv + + # Note: QGIS_PREFIX_PATH is evaluated in QgsApplication - + # no need to mess with it here. + QGISAPP = QgsApplication(argvb, myGuiFlag) + + QGISAPP.initQgis() + s = QGISAPP.showSettings() + LOGGER.debug(s) + + def debug_log_message(message, tag, level): + """ + Prints a debug message to a log + :param message: message to print + :param tag: log tag + :param level: log message level (severity) + :return: + """ + 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() # noqa: F823 + QGISAPP = None # noqa: F841 + except NameError: + pass + + if PARENT is None: + # noinspection PyPep8Naming + PARENT = QWidget() + + if CANVAS is None: + # noinspection PyPep8Naming + CANVAS = QgsMapCanvas(PARENT) + CANVAS.resize(QSize(400, 400)) + + if IFACE is None: + # QgisInterface is a stub implementation of the QGIS plugin interface + # noinspection PyPep8Naming + IFACE = QgisInterface(CANVAS) + + return QGISAPP, CANVAS, IFACE, PARENT From 33132938892a785aa22f47b7c55565cbc4dc6352 Mon Sep 17 00:00:00 2001 From: Merydian Date: Mon, 25 Mar 2024 09:36:31 +0100 Subject: [PATCH 02/21] docs: add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e39d9cb..f8dc9152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ RELEASING: --> +# Unreleased +### Added +- Unit- and e2e-testing + ## [1.7.1] - 2024-01-15 ### Added From 6f499be208c6781986ad31199d97af482f9d9f13 Mon Sep 17 00:00:00 2001 From: Merydian Date: Mon, 25 Mar 2024 10:40:41 +0100 Subject: [PATCH 03/21] docs: add documentation on how to run tests --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index e8190c01..068ae0f7 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,49 @@ where `` is one of: - Windows: `C:\Users\USER\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\ORStools` - Mac OS: `Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/ORStools` +### CI +#### Testing +The repository tests on the QGis Versions *3.16*, *3.22* and the *latest* version. +Until now, it's only possible to test one version at a time. + +To do local test runs you can use a [conda installation](https://github.com/opengisch/qgis-conda-builder) of the QGis version you want to test. +You will also have to install *xvfb* to run the tests on involving an interface. +Lastly, we need [*Pytest*](https://docs.pytest.org/en/8.0.x/) to run tests in general. + +To do the above run use these commands: +1. Install a version of anaconda, preferrably [*miniforge*](https://github.com/conda-forge/miniforge). + +2. Create and prepare the environment. +```shell +# create environment +mamba create --name qgis_test +# activate envotonment +conda activate qgis_test +# install pip +mamba install qgis pip +``` +3. Install QGis using mamba. +```shell +mamba install -c conda-forge qgis=[3.16, 3.22, latest] # choose one +``` + +4. Install *xvfb* +```shell +sudo apt-get update +sudo apt install xvfb +``` + +5. Install *Pytest* using pip in testing environment. +```shell +pip install -U pytest +``` + +To run the tests you will need an ORS-API key: +```shell +cd orstools-qgis-plugin +export ORS_API_KEY=[Your API key here] && xvfb-run pytest +``` + ### Debugging In the **PyCharm community edition** you will have to use logging and printing to inspect elements. The First Aid QGIS plugin can probably also be used additionally. From 016efa8143e650f6eb4478634f4c5d4e01a368a6 Mon Sep 17 00:00:00 2001 From: Merydian Date: Mon, 25 Mar 2024 10:44:16 +0100 Subject: [PATCH 04/21] build: set tests to run on pull requests only --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e54d25ce..d81bab81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,6 @@ name: Testing on: - push: pull_request: jobs: From 66da4e6176082d3f3df05dd4c6b246c88ee6d628 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 5 Jul 2024 13:04:55 -0400 Subject: [PATCH 05/21] docs: add pyaml to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 68fc11a2..77d13bcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ ruff pytest +pyaml From bac9f3da47d73d0476eadf8d4562d1e8ac00ac86 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 5 Jul 2024 13:05:39 -0400 Subject: [PATCH 06/21] docs: add pytest to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 77d13bcd..f6c06aef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ ruff pytest pyaml +pytest From 6d434551e71d7018cd0ce558be7be1932306e86e Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 5 Jul 2024 14:54:15 -0400 Subject: [PATCH 07/21] docs: add commentson usage to requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index f6c06aef..c5eccfe4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ +# developement ruff pytest + +# testing pyaml pytest From 55706bf6aaeabd00dec9e24a6cdd261d001d425a Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 5 Jul 2024 14:54:59 -0400 Subject: [PATCH 08/21] feat: re-add empty provider --- ORStools/config.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ORStools/config.yml b/ORStools/config.yml index a08cd7d1..e69de29b 100755 --- a/ORStools/config.yml +++ b/ORStools/config.yml @@ -1,8 +0,0 @@ -providers: -- ENV_VARS: - ORS_QUOTA: X-Ratelimit-Limit - ORS_REMAINING: X-Ratelimit-Remaining - base_url: https://api.openrouteservice.org - key: '' - name: openrouteservice - timeout: 60 From 30f63be7d202888de70d4019e262dbfd0d7b391c Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 5 Jul 2024 15:02:27 -0400 Subject: [PATCH 09/21] Revert "feat: re-add empty provider" This reverts commit 55706bf6aaeabd00dec9e24a6cdd261d001d425a. --- ORStools/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ORStools/config.yml b/ORStools/config.yml index e69de29b..a08cd7d1 100755 --- a/ORStools/config.yml +++ b/ORStools/config.yml @@ -0,0 +1,8 @@ +providers: +- ENV_VARS: + ORS_QUOTA: X-Ratelimit-Limit + ORS_REMAINING: X-Ratelimit-Remaining + base_url: https://api.openrouteservice.org + key: '' + name: openrouteservice + timeout: 60 From 3b6cfed6d120f760243652de0bf3f1a420862e08 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 22 Jul 2024 14:09:43 +0200 Subject: [PATCH 10/21] fix: import from qgis.core not qgis._core --- tests/test_common.py | 2 +- tests/test_proc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 5905200d..0ed8693c 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,4 +1,4 @@ -from qgis._core import QgsPointXY +from qgis.core import QgsPointXY from qgis.testing import unittest from ORStools.common import client, directions_core, isochrones_core diff --git a/tests/test_proc.py b/tests/test_proc.py index b6b30e3f..df4cd8d7 100644 --- a/tests/test_proc.py +++ b/tests/test_proc.py @@ -1,4 +1,4 @@ -from qgis._core import ( +from qgis.core import ( QgsPointXY, QgsProcessingFeedback, QgsProcessingContext, From bc26cf676519d3c4d7e6bd566f13021c43301d26 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 22 Jul 2024 14:10:15 +0200 Subject: [PATCH 11/21] feat: adapt documentation to show linux and windows processes --- README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 068ae0f7..9a9d2bdb 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,21 @@ where `` is one of: The repository tests on the QGis Versions *3.16*, *3.22* and the *latest* version. Until now, it's only possible to test one version at a time. -To do local test runs you can use a [conda installation](https://github.com/opengisch/qgis-conda-builder) of the QGis version you want to test. +#### Linux +On linux machines you can run the tests with your local QGIS installation. + +1. Install QGIS and make sure it's available in your currently activated environment. + +You will need an ORS-API key. Either set it as an environment variable or do `export ORS_API_KEY=[Your API key here]` before you run the tests. + +To run the tests do: +```shell +cd orstools-qgis-plugin +pytest +``` + +#### Windows +Do all the following steps in a [*WSL*](https://learn.microsoft.com/en-us/windows/wsl/install). To run tests locally you can use a [conda installation](https://github.com/opengisch/qgis-conda-builder) of the QGis version you want to test. You will also have to install *xvfb* to run the tests on involving an interface. Lastly, we need [*Pytest*](https://docs.pytest.org/en/8.0.x/) to run tests in general. @@ -136,17 +150,19 @@ To do the above run use these commands: 1. Install a version of anaconda, preferrably [*miniforge*](https://github.com/conda-forge/miniforge). 2. Create and prepare the environment. + ```shell # create environment -mamba create --name qgis_test -# activate envotonment +conda create --name qgis_test +# activate environment conda activate qgis_test # install pip -mamba install qgis pip +conda install pip ``` + 3. Install QGis using mamba. ```shell -mamba install -c conda-forge qgis=[3.16, 3.22, latest] # choose one +conda install -c conda-forge qgis=[3.16, 3.22, latest] # choose one ``` 4. Install *xvfb* From 5011fc7820f692ec87b07c505bce95933e644d0f Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 29 Jul 2024 09:40:23 +0200 Subject: [PATCH 12/21] Test on float not dict --- tests/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_common.py b/tests/test_common.py index 0ed8693c..e12e04bd 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -96,7 +96,7 @@ def test_client_request_geometry(self): clnt = client.Client(provider, agent) response = clnt.request("/v2/directions/" + profile + "/geojson", {}, post_json=params) self.assertAlmostEqual( - response["features"][0]["geometry"], test_response["features"][0]["geometry"] + response["features"][0]["geometry"]["coordinates"][0][0], test_response["features"][0]["geometry"]["coordinates"][0][0] ) def test_output_feature_directions(self): From d05c732f209e53c78321d989ad63e1e0a6c5945b Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 29 Jul 2024 09:41:04 +0200 Subject: [PATCH 13/21] Wrap processing import in try/except to pass gui test --- ORStools/gui/ORStoolsDialog.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index bb6652d1..ff680db5 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -29,7 +29,10 @@ import json import os -import processing +try: + import processing +except: + pass import webbrowser from qgis._core import Qgis From 012f8fa26a7bcb432d1c04c631629bbf6e56cca9 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 29 Jul 2024 09:41:52 +0200 Subject: [PATCH 14/21] style: run ruff --- ORStools/gui/ORStoolsDialog.py | 1 + tests/test_common.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index ff680db5..00c2a99f 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -29,6 +29,7 @@ import json import os + try: import processing except: diff --git a/tests/test_common.py b/tests/test_common.py index e12e04bd..4ae1d8d0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -96,7 +96,8 @@ def test_client_request_geometry(self): clnt = client.Client(provider, agent) response = clnt.request("/v2/directions/" + profile + "/geojson", {}, post_json=params) self.assertAlmostEqual( - response["features"][0]["geometry"]["coordinates"][0][0], test_response["features"][0]["geometry"]["coordinates"][0][0] + response["features"][0]["geometry"]["coordinates"][0][0], + test_response["features"][0]["geometry"]["coordinates"][0][0], ) def test_output_feature_directions(self): From a16469b0553af1a664899f4887a62b981b9bc0c7 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Mon, 29 Jul 2024 09:43:09 +0200 Subject: [PATCH 15/21] fix: add ModuleNotFound argument to except statement. --- ORStools/gui/ORStoolsDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 00c2a99f..59da9fbd 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -32,7 +32,7 @@ try: import processing -except: +except ModuleNotFoundError: pass import webbrowser From 163492bf2a8d34d403c9c23ca3f1f9c922e60182 Mon Sep 17 00:00:00 2001 From: Jakob Schnell Date: Fri, 23 Aug 2024 10:31:44 +0200 Subject: [PATCH 16/21] fix: use pyyaml instead of pyaml As python is python, both `pyaml` and `pyyaml` exist, the former being mainly used for pretty-printing. As we don't print anything, we can depend on `pyyaml` instead. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c5eccfe4..ca4e9990 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ ruff pytest # testing -pyaml +pyyaml pytest From e61884eb73d226a409ede1af76e92c49bbd24f6a Mon Sep 17 00:00:00 2001 From: merydian Date: Fri, 23 Aug 2024 11:09:26 +0200 Subject: [PATCH 17/21] fix: switch to apt install python3-xyz --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d81bab81..08ccf6d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,6 @@ jobs: - uses: actions/checkout@v4 - name: Run test latest run: | - docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:latest sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && pip install -U pytest && xvfb-run pytest' + docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:latest sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && apt install python3-pytest && xvfb-run pytest' env: DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} From 4fb4d3268101a9d544694122137128940ffae3c4 Mon Sep 17 00:00:00 2001 From: merydian Date: Fri, 23 Aug 2024 11:15:07 +0200 Subject: [PATCH 18/21] fix: switch to assertDictEqual for dict comparison --- tests/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_common.py b/tests/test_common.py index 4ae1d8d0..a6bee8cb 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -279,7 +279,7 @@ def test_build_default_parameters(self): "options": {}, } - self.assertAlmostEqual(params, test_params) + self.assertDictEqual(params, test_params) def test_isochrones(self): response = { From 18e550543c8f8fbb7b5724390d6b1edabba366a8 Mon Sep 17 00:00:00 2001 From: merydian Date: Fri, 23 Aug 2024 11:20:57 +0200 Subject: [PATCH 19/21] fix: use typing List instead of builtin list --- ORStools/common/directions_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py index 705abdea..06c530a9 100644 --- a/ORStools/common/directions_core.py +++ b/ORStools/common/directions_core.py @@ -264,7 +264,7 @@ def build_default_parameters( def get_extra_info_features_directions( - response: dict, extra_info_order: list[str], to_from_values: Optional[list] = None + response: dict, extra_info_order: List[str], to_from_values: Optional[list] = None ): extra_info_order = [ key if key != "waytype" else "waytypes" for key in extra_info_order From d4d0c2dc7b17430827f95983a82bc9abadb2cbd6 Mon Sep 17 00:00:00 2001 From: merydian Date: Fri, 23 Aug 2024 11:25:49 +0200 Subject: [PATCH 20/21] fix: add extra info to test data --- tests/test_common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_common.py b/tests/test_common.py index a6bee8cb..2061f097 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -277,6 +277,7 @@ def test_build_default_parameters(self): "elevation": True, "id": None, "options": {}, + "extra_info": None, } self.assertDictEqual(params, test_params) From 3eea5c0a606558d970c847d9846782e080faaf72 Mon Sep 17 00:00:00 2001 From: merydian Date: Fri, 23 Aug 2024 11:36:29 +0200 Subject: [PATCH 21/21] feat: set canvas scale and frameStayle --- tests/test_gui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gui.py b/tests/test_gui.py index 306a8111..17c97adf 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -25,6 +25,10 @@ def test_ORStoolsDialog(self): CRS = QgsCoordinateReferenceSystem.fromEpsgId(3857) CANVAS.setExtent(QgsRectangle(258889, 7430342, 509995, 7661955)) CANVAS.setDestinationCrs(CRS) + CANVAS.setFrameStyle(0) + CANVAS.resize(600, 400) + self.assertEqual(CANVAS.width(), 600) + self.assertEqual(CANVAS.height(), 400) dlg = ORStoolsDialog(IFACE) dlg.open()