diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 25156c6..2b3adca 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -42,8 +42,10 @@ jobs: - name: pytest and coverage run: | + docker-compose -f tests/docker-compose.yml up -d pip install -e . coverage run --source=routingpy --module pytest coverage lcov --include "routingpy/*" + docker-compose -f tests/docker-compose.yml down env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 5ccd06b..434fb96 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ test.py *.pyc *.idea/ .DS_Store +**/test_data/* +!test_data/andorra-latest.osm.pbf diff --git a/CHANGELOG.md b/CHANGELOG.md index d6894d3..4f20f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## **Unreleased** +### Added +- Added integration tests using dockerized instances of Valhalla, OSRM, ORS and GraphHopper + ### Fixed - Unit conversion did not work properly in ors' directions method - Docstring for `intersections` parameter in ors' isochrones method was missing - `profile` parameter was unnecessarily passed to POST params in ors' isochrones and matrix methods +- Valhalla's Isochrones center property was unnecessarily wrapped in a list +- GraphHopper Isochrones center coordinates were strings, not floats + ## [v1.1.0](https://pypi.org/project/routingpy/1.1.0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45891a3..29e91d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,11 +48,7 @@ pip install -r requirements_dev.txt poetry install ``` -3. Run tests to check if all goes well: -```bash -# From the root of your git project -nosetests -v -``` +3. Run tests to check if all goes well (see test section below) 4. Please install the pre-commit hook, so your code gets auto-formatted and linted before committing it: ```bash @@ -62,10 +58,8 @@ pre-commit install ### Tests -We only do mocked tests as routing results heavily depend on underlying data, which, at least in the case of the FOSS routing -engines, changes on a very regular basis due to OpenStreetMap updates. All queries and most mocked responses are located in -`test/test_helper.py` in `dict`s. This unfortunately also means, that our tests are less API tests and more unit tests and can't catch -updates on providers' API changes. +Run `docker-compose -f tests/docker-compose-yml up` from +the project root to set up local instances of Valhalla, OSRM, ORS and GraphHopper. Run `nosetests` with a `coverage` flag, which shouldn't report < 90% coverage overall (the starting point at the beginning of the project). A [`coveralls.io`](https://coveralls.io) instance will check for coverage when you submit a PR. diff --git a/routingpy/routers/graphhopper.py b/routingpy/routers/graphhopper.py index ffcfc21..ebb645b 100644 --- a/routingpy/routers/graphhopper.py +++ b/routingpy/routers/graphhopper.py @@ -276,7 +276,7 @@ def directions( # noqa: C901 ``snap_prevention``, ``curb_side``, ``turn_costs`` parameters """ - params = [("vehicle", profile)] + params = [("profile", profile)] for coordinate in locations: coord_latlng = reversed([convert.format_float(f) for f in coordinate]) @@ -477,7 +477,7 @@ def isochrones( :rtype: :class:`routingpy.isochrone.Isochrones` """ - params = [("vehicle", profile), ("type", type)] + params = [("profile", profile), ("type", type)] if convert.is_list(intervals): if interval_type in (None, "time"): @@ -510,7 +510,7 @@ def isochrones( type, intervals[0], buckets, - center, + locations, interval_type, ) @@ -585,7 +585,7 @@ def matrix( :returns: A matrix from the specified sources and destinations. :rtype: :class:`routingpy.matrix.Matrix` """ - params = [("vehicle", profile)] + params = [("profile", profile)] if self.key is not None: params.append(("key", self.key)) diff --git a/routingpy/routers/valhalla.py b/routingpy/routers/valhalla.py index 61e1402..d38dcc6 100644 --- a/routingpy/routers/valhalla.py +++ b/routingpy/routers/valhalla.py @@ -519,7 +519,7 @@ def parse_isochrone_json(response, intervals, locations, interval_type): Isochrone( geometry=feature["geometry"]["coordinates"], interval=intervals[idx], - center=locations, + center=locations[0] if isinstance(locations[0], list) else locations, interval_type=interval_type, ) ) diff --git a/test_data/andorra-latest.osm.pbf b/test_data/andorra-latest.osm.pbf new file mode 100644 index 0000000..d561ee3 Binary files /dev/null and b/test_data/andorra-latest.osm.pbf differ diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..0953437 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.8" +services: +# ors: +# container_name: ors-tests +# ports: +# - "8005:8080" +# image: ghcr.io/gis-ops/openrouteservice:latest +# volumes: +# - ../test_data/andorra-latest.osm.pbf:/ors-core/data/osm_file.pbf +# environment: +# - "JAVA_OPTS=-Djava.awt.headless=true -server -XX:TargetSurvivorRatio=75 -XX:SurvivorRatio=64 -XX:MaxTenuringThreshold=3 -XX:+UseG1GC -XX:+ScavengeBeforeFullGC -XX:ParallelGCThreads=4 -Xms1g -Xmx2g" +# - "CATALINA_OPTS=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9001 -Dcom.sun.management.jmxremote.rmi.port=9001 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost" + valhalla: + container_name: valhalla-tests + image: gisops/valhalla:latest + ports: + - "8002:8002" + volumes: + - ../test_data/:/custom_files + osrm: + container_name: osrm-tests + image: osrm/osrm-backend:latest + ports: + - "5000:5000" + volumes: + - ../test_data/:/data + command: '/bin/bash -c "osrm-extract -p /opt/car.lua /data/andorra-latest.osm.pbf && osrm-partition /data/andorra-latest.osrm && osrm-customize /data/andorra-latest.osrm && osrm-routed --algorithm=MLD /data/andorra-latest.osrm"' + graphhopper: + container_name: graphhopper-tests + image: israelhikingmap/graphhopper:latest + ports: + - "8989:8989" + volumes: + - ../test_data/:/graphhopper/data + command: "-i data/andorra-latest.osm.pbf --host 0.0.0.0" \ No newline at end of file diff --git a/tests/test_graphhopper.py b/tests/test_graphhopper.py index 50a816d..c721d31 100644 --- a/tests/test_graphhopper.py +++ b/tests/test_graphhopper.py @@ -58,7 +58,7 @@ def test_full_directions(self): "elevation=true&heading=50%2C50%2C50&heading_penalty=100&instructions=false&key=sample_key&locale=en&" "optimize=true&pass_through=true&point=49.415776%2C8.680916&point=49.420577%2C8.688641&" "point=49.445776%2C8.780916&point_hint=Graphhopper%20Lane&point_hint=OSM%20Street&point_hint=Routing%20Broadway&" - "&points_encoded=true&vehicle=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true&fake_option=42", + "&points_encoded=true&profile=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true&fake_option=42", responses.calls[0].request.url, ) @@ -89,7 +89,7 @@ def test_full_directions_alternatives(self): "elevation=true&heading=50%2C50%2C50&heading_penalty=100&instructions=false&key=sample_key&locale=en&" "optimize=true&pass_through=true&point=49.415776%2C8.680916&point=49.420577%2C8.688641&" "point=49.445776%2C8.780916&point_hint=Graphhopper%20Lane&point_hint=OSM%20Street&point_hint=Routing%20Broadway" - "&points_encoded=true&vehicle=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true", + "&points_encoded=true&profile=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true", responses.calls[0].request.url, ) self.assertIsInstance(routes, Directions) @@ -125,7 +125,7 @@ def test_full_directions_not_encoded(self): "elevation=true&heading=50%2C50%2C50&heading_penalty=100&instructions=false&key=sample_key&locale=en&" "optimize=true&pass_through=true&point=49.415776%2C8.680916&point=49.420577%2C8.688641&" "point=49.445776%2C8.780916&point_hint=Graphhopper%20Lane&point_hint=OSM%20Street&point_hint=Routing%20Broadway" - "&points_encoded=false&vehicle=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true", + "&points_encoded=false&profile=car&type=json&weighting=short_fastest&snap_prevention=trunk%2Cferry&curb_side=any%2Cright&turn_costs=true", responses.calls[0].request.url, ) @@ -153,7 +153,7 @@ def test_full_isochrones(self): self.assertEqual(1, len(responses.calls)) self.assertURLEqual( "https://graphhopper.com/api/1/isochrone?buckets=3&debug=false&key=sample_key&" - "point=48.23424%2C8.34234&vehicle=car&reverse_flow=true&time_limit=1000&type=json&fake_option=42", + "point=48.23424%2C8.34234&profile=car&reverse_flow=true&time_limit=1000&type=json&fake_option=42", responses.calls[0].request.url, ) @@ -184,7 +184,7 @@ def test_full_matrix(self): self.assertEqual(1, len(responses.calls)) self.assertURLEqual( "https://graphhopper.com/api/1/matrix?key=sample_key&out_array=distances&out_array=times&out_array=weights&" - "point=49.415776%2C8.680916&point=49.420577%2C8.688641&point=49.445776%2C8.780916&vehicle=car&debug=true&fake_option=42", + "point=49.415776%2C8.680916&point=49.420577%2C8.688641&point=49.445776%2C8.780916&profile=car&debug=true&fake_option=42", responses.calls[0].request.url, ) @@ -225,14 +225,14 @@ def test_few_sources_destinations_matrix(self): self.assertURLEqual( "https://graphhopper.com/api/1/matrix?from_point=49.415776%2C8.680916&from_point=49.420577%2C8.688641&" "from_point=49.445776%2C8.780916&key=sample_key&out_array=distances" - "&out_array=times&out_array=weights&vehicle=car&to_point=49.415776%2C8.680916&to_point=49.420577%2C8.688641&" + "&out_array=times&out_array=weights&profile=car&to_point=49.415776%2C8.680916&to_point=49.420577%2C8.688641&" "&to_point=49.445776%2C8.780916&debug=true", responses.calls[0].request.url, ) self.assertURLEqual( "https://graphhopper.com/api/1/matrix?point=49.415776%2C8.680916&point=49.420577%2C8.688641&" "point=49.445776%2C8.780916&key=sample_key&out_array=distances" - "&out_array=times&out_array=weights&vehicle=car" + "&out_array=times&out_array=weights&profile=car" "&debug=true", responses.calls[1].request.url, ) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..78c0867 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,146 @@ +import tests as _test +from routingpy import OSRM, Graphhopper, Valhalla + + +class TestRoutingpyIntegration(_test.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.valhalla = Valhalla("http://localhost:8002") + cls.osrm = OSRM("http://localhost:5000") + # ignore ORS for now, not working in GH Actions + # (see e.g. https://github.com/gis-ops/routingpy/actions/runs/4022971575/jobs/6913297227) + # cls.ors = ORS(base_url="http://localhost:8005/ors") + cls.gh = Graphhopper(base_url="http://localhost:8989") + + def test_valhalla_directions(self): + + directions = self.valhalla.directions( + [ + [1.51886, 42.5063], + [1.53789, 42.51007], + ], + "auto", + ) + + self.assertIsInstance(directions.geometry, list) + self.assertIsInstance(directions.duration, int) + self.assertIsInstance(directions.distance, int) + + def test_valhalla_isochrones(self): + + isochrones = self.valhalla.isochrones( + [ + [1.51886, 42.5063], + ], + "auto", + [20, 50], + ) + + self.assertIsInstance(isochrones[1].geometry, list) + self.assertEqual(isochrones[0].interval, 20) + self.assertEqual(isochrones[1].interval, 50) + self.assertEqual(isochrones[1].center, [1.51886, 42.5063]) + + def test_valhalla_matrix(self): + matrix = self.valhalla.matrix( + [ + [1.51886, 42.5063], + [1.53789, 42.51007], + [1.53489, 42.52007], + [1.53189, 42.51607], + ], + "auto", + ) + + self.assertEqual(len(matrix.distances), 4) + self.assertEqual(len(matrix.durations), 4) + self.assertEqual(len(matrix.durations[0]), 4) + self.assertEqual(len(matrix.distances[0]), 4) + + def test_osrm_directions(self): + directions = self.osrm.directions( + [ + [1.51886, 42.5063], + [1.53789, 42.51007], + ] + ) + + self.assertIsInstance(directions.geometry, list) + self.assertIsInstance(directions.duration, int) + self.assertIsInstance(directions.distance, int) + + def test_osrm_matrix(self): + matrix = self.osrm.matrix( + [ + [1.51886, 42.5063], + [1.53789, 42.51007], + [1.53489, 42.52007], + [1.53189, 42.51607], + ], + "auto", + ) + + self.assertEqual(len(matrix.distances), 4) + self.assertEqual(len(matrix.durations), 4) + self.assertEqual(len(matrix.durations[0]), 4) + self.assertEqual(len(matrix.distances[0]), 4) + + # def test_ors_directions(self): + # directions = self.ors.directions( + # [ + # [1.51886, 42.5063], + # [1.53789, 42.51007], + # ], + # "driving-car", + # ) + # + # self.assertIsInstance(directions.geometry, list) + # self.assertIsInstance(directions.duration, int) + # self.assertIsInstance(directions.distance, int) + # + # def test_ors_isochrones(self): + # isochrones = self.ors.isochrones([1.51886, 42.5063], "driving-car", [20, 50]) + # + # self.assertIsInstance(isochrones[1].geometry, list) + # self.assertEqual(isochrones[0].interval, 20) + # self.assertEqual(isochrones[1].interval, 50) + # self.assertAlmostEqual(isochrones[1].center[0], 1.51886, 1) + # self.assertAlmostEqual(isochrones[1].center[1], 42.5063, 1) + # + # def test_ors_matrix(self): + # matrix = self.ors.matrix( + # [ + # [1.51886, 42.5063], + # [1.53789, 42.51007], + # [1.53489, 42.52007], + # [1.53189, 42.51607], + # ], + # "driving-car", + # metrics=["distance", "duration"], + # ) + # + # self.assertEqual(len(matrix.distances), 4) + # self.assertEqual(len(matrix.durations), 4) + # self.assertEqual(len(matrix.durations[0]), 4) + # self.assertEqual(len(matrix.distances[0]), 4) + + def test_graphhopper_directions(self): + directions = self.gh.directions( + [ + [1.51886, 42.5063], + [1.53789, 42.51007], + ], + "car", + ) + + self.assertIsInstance(directions.geometry, list) + self.assertIsInstance(directions.duration, int) + self.assertIsInstance(directions.distance, int) + + def test_graphhopper_isochrones(self): + isochrones = self.gh.isochrones([1.51886, 42.5063], "car", [50]) + + self.assertIsInstance(isochrones[0].geometry, list) + self.assertEqual(isochrones[0].interval, 50) + self.assertAlmostEqual(isochrones[0].center[0], 1.51886, 1) + self.assertAlmostEqual(isochrones[0].center[1], 42.5063, 1)