implement basic testing fixtures and linting #19
							
								
								
									
										34
									
								
								.gitea/workflows/backend_run-lint.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.gitea/workflows/backend_run-lint.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - backend/** | ||||
|  | ||||
| name: Run linting on the backend code | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|  | ||||
|     - uses: https://gitea.com/actions/checkout@v4 | ||||
|  | ||||
|     - name: Install dependencies | ||||
|       run: | | ||||
|         apt-get update && apt-get install -y python3 python3-pip | ||||
|         pip install pipenv | ||||
|  | ||||
|     - name: Install packages | ||||
|       run: | | ||||
|         ls -la | ||||
|         # only install dev-packages | ||||
|         pipenv install --categories=dev-packages | ||||
|         pipenv run pip freeze | ||||
|  | ||||
|       working-directory: backend | ||||
|  | ||||
|     - name: Run linter | ||||
|       run: pipenv run pylint src | ||||
|       working-directory: backend | ||||
							
								
								
									
										33
									
								
								.gitea/workflows/backend_run-test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.gitea/workflows/backend_run-test.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - backend/** | ||||
|  | ||||
| name: Run testing on the backend code | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|  | ||||
|     - uses: https://gitea.com/actions/checkout@v4 | ||||
|  | ||||
|     - name: Install dependencies | ||||
|       run: | | ||||
|         apt-get update && apt-get install -y python3 python3-pip | ||||
|         pip install pipenv | ||||
|  | ||||
|     - name: Install packages | ||||
|       run: | | ||||
|         ls -la | ||||
|         # install all packages, including dev-packages | ||||
|         pipenv install --dev | ||||
|         pipenv run pip freeze | ||||
|       working-directory: backend | ||||
|  | ||||
|     - name: Run Tests | ||||
|       run: pipenv run pytest src | ||||
|       working-directory: backend | ||||
							
								
								
									
										10
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -14,9 +14,9 @@ | ||||
|                 "DEBUG": "true" | ||||
|             }, | ||||
|             "args": [ | ||||
|                 "--app-dir", | ||||
|                 "src", | ||||
|                 "main:app", | ||||
|                 // "--app-dir", | ||||
|                 // "src", | ||||
|                 "src.main:app", | ||||
|                 "--reload", | ||||
|             ], | ||||
|             "jinja": true, | ||||
| @@ -26,11 +26,11 @@ | ||||
|             "name": "Backend - tester", | ||||
|             "type": "debugpy", | ||||
|             "request": "launch", | ||||
|             "program": "src/tester.py", | ||||
|             "program": "backend/src/tester.py", | ||||
|             "env": { | ||||
|                 "DEBUG": "true" | ||||
|             }, | ||||
|             "cwd": "${workspaceFolder}/backend" | ||||
|             "cwd": "${workspaceFolder}" | ||||
|         }, | ||||
|         // frontend - flutter app | ||||
|         { | ||||
|   | ||||
							
								
								
									
										2
									
								
								backend/.pylintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								backend/.pylintrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [MAIN] | ||||
| max-line-length=240 | ||||
| @@ -4,6 +4,11 @@ verify_ssl = true | ||||
| name = "pypi" | ||||
|  | ||||
| [dev-packages] | ||||
| pylint = "*" | ||||
| pytest = "*" | ||||
| tomli = "*" | ||||
| exceptiongroup = "*" | ||||
| httpx = "*" | ||||
|  | ||||
| [packages] | ||||
| numpy = "*" | ||||
|   | ||||
							
								
								
									
										2381
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2381
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										0
									
								
								backend/src/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,14 +1,14 @@ | ||||
| import logging | ||||
| from fastapi import FastAPI, Query, Body, HTTPException | ||||
|  | ||||
| from structs.landmark import Landmark | ||||
| from structs.preferences import Preferences | ||||
| from structs.linked_landmarks import LinkedLandmarks | ||||
| from structs.trip import Trip | ||||
| from utils.landmarks_manager import LandmarkManager | ||||
| from utils.optimizer import Optimizer | ||||
| from utils.refiner import Refiner | ||||
| from persistence import client as cache_client | ||||
| from .structs.landmark import Landmark | ||||
| from .structs.preferences import Preferences | ||||
| from .structs.linked_landmarks import LinkedLandmarks | ||||
| from .structs.trip import Trip | ||||
| from .utils.landmarks_manager import LandmarkManager | ||||
| from .utils.optimizer import Optimizer | ||||
| from .utils.refiner import Refiner | ||||
| from .persistence import client as cache_client | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from pymemcache.client.base import Client | ||||
|  | ||||
| import constants | ||||
| from .constants import MEMCACHED_HOST_PATH | ||||
|  | ||||
|  | ||||
| class DummyClient: | ||||
| @@ -15,11 +15,11 @@ class DummyClient: | ||||
|         return self._data[key] | ||||
|  | ||||
|  | ||||
| if constants.MEMCACHED_HOST_PATH is None: | ||||
| if MEMCACHED_HOST_PATH is None: | ||||
|     client = DummyClient() | ||||
| else: | ||||
|     client = Client( | ||||
|         constants.MEMCACHED_HOST_PATH, | ||||
|         MEMCACHED_HOST_PATH, | ||||
|         timeout=1, | ||||
|         allow_unicode_keys=True, | ||||
|         encoding='utf-8' | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| from .landmark import Landmark | ||||
| from utils.get_time_separation import get_time | ||||
| from ..utils.get_time_separation import get_time | ||||
|  | ||||
| class LinkedLandmarks: | ||||
|     """ | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import logging | ||||
| import yaml | ||||
|  | ||||
| from utils.landmarks_manager import LandmarkManager | ||||
| from utils.optimizer import Optimizer | ||||
| from utils.refiner import Refiner | ||||
| from structs.landmark import Landmark | ||||
| from structs.linked_landmarks import LinkedLandmarks | ||||
| from structs.preferences import Preferences, Preference | ||||
| from .utils.landmarks_manager import LandmarkManager | ||||
| from .utils.optimizer import Optimizer | ||||
| from .utils.refiner import Refiner | ||||
| from .structs.landmark import Landmark | ||||
| from .structs.linked_landmarks import LinkedLandmarks | ||||
| from .structs.preferences import Preferences, Preference | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|   | ||||
							
								
								
									
										0
									
								
								backend/src/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										141
									
								
								backend/src/tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								backend/src/tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| from fastapi.testclient import TestClient | ||||
| from typing import List | ||||
| import pytest | ||||
| from ..main import app | ||||
| from ..structs.landmark import Landmark | ||||
|  | ||||
|  | ||||
| @pytest.fixture() | ||||
| def client(): | ||||
|     return TestClient(app) | ||||
|  | ||||
|  | ||||
| # Base test for checking if the API returns correct error code when no preferences are specified. | ||||
| def test_new_trip_invalid_prefs(client): | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
|             "preferences": {}, | ||||
|             "start": [48.8566, 2.3522] | ||||
|             } | ||||
|         ) | ||||
|     assert response.status_code == 422 | ||||
|  | ||||
|      | ||||
| # Test no. 1 | ||||
| def test_turckheim(client): | ||||
|     duration_minutes = 15 | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
|             "preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, "nature": {"type": "nature", "score": 5}, "shopping": {"type": "shopping", "score": 5}, "max_time_minute": duration_minutes, "detour_tolerance_minute": 0}, | ||||
|             "start": [48.084588, 7.280405] | ||||
|             } | ||||
|         ) | ||||
|     result = response.json() | ||||
|     landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) | ||||
|  | ||||
|     # checks : | ||||
|     assert response.status_code == 200  # check for successful planning | ||||
|     assert isinstance(landmarks, list)  # check that the return type is a list | ||||
|     assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 | ||||
|     assert len(landmarks) > 2           # check that there is something to visit | ||||
|  | ||||
|  | ||||
| # Test no. 2 | ||||
| def test_bellecour(client) : | ||||
|     duration_minutes = 35 | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
|             "preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, "nature": {"type": "nature", "score": 5}, "shopping": {"type": "shopping", "score": 5}, "max_time_minute": duration_minutes, "detour_tolerance_minute": 0}, | ||||
|             "start": [45.7576485, 4.8330241] | ||||
|             } | ||||
|         ) | ||||
|     result = response.json() | ||||
|     landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) | ||||
|     osm_ids = landmarks_to_osmid(landmarks) | ||||
|  | ||||
|     # checks : | ||||
|     assert response.status_code == 200  # check for successful planning | ||||
|     assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 | ||||
|     assert 136200148 in osm_ids         # check for Cathédrale St. Jean in trip | ||||
|  | ||||
|  | ||||
|  | ||||
| def landmarks_to_osmid(landmarks: List[Landmark]) -> list : | ||||
|     """ | ||||
|     Convert the list of landmarks into a list containing their osm ids for quick landmark checking. | ||||
|      | ||||
|     Args : | ||||
|         landmarks (list): the list of landmarks | ||||
|      | ||||
|     Returns : | ||||
|         ids (list)      : the list of corresponding OSM ids | ||||
|     """ | ||||
|     ids = [] | ||||
|     for landmark in landmarks : | ||||
|         ids.append(landmark.osm_id) | ||||
|  | ||||
|     return ids | ||||
|  | ||||
| def fetch_landmark(client, landmark_uuid): | ||||
|     """ | ||||
|     Fetch landmark data from the API based on the landmark UUID. | ||||
|  | ||||
|     Args: | ||||
|         landmark_uuid (str): The UUID of the landmark. | ||||
|  | ||||
|     Returns: | ||||
|         dict: Landmark data fetched from the API. | ||||
|     """ | ||||
|     response = client.get(f"/landmark/{landmark_uuid}") | ||||
|  | ||||
|     if response.status_code != 200: | ||||
|         raise Exception(f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}") | ||||
|      | ||||
|     json_data = response.json() | ||||
|      | ||||
|     if "detail" in json_data: | ||||
|         raise Exception(json_data["detail"]) | ||||
|      | ||||
|     return json_data | ||||
|  | ||||
|  | ||||
| def load_trip_landmarks(client, first_uuid): | ||||
|     """ | ||||
|     Load all landmarks for a trip using the response from the API. | ||||
|  | ||||
|     Args: | ||||
|         first_uuid (str) : The first UUID of the landmark. | ||||
|  | ||||
|     Returns: | ||||
|         landmarks (list) : An list containing all landmarks for the trip. | ||||
|     """ | ||||
|     landmarks = [] | ||||
|     next_uuid = first_uuid | ||||
|  | ||||
|     while next_uuid is not None: | ||||
|         landmark_data = fetch_landmark(client, next_uuid) | ||||
|         landmarks.append(Landmark(**landmark_data)) # Create Landmark objects | ||||
|         next_uuid = landmark_data.get('next_uuid')  # Prepare for the next iteration | ||||
|  | ||||
|     return landmarks | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # def test_new_trip_single_prefs(client): | ||||
| #     response = client.post( | ||||
| #         "/trip/new", | ||||
| #         json={ | ||||
| #             "preferences": {"sightseeing": {"type": "sightseeing", "score": 1}, "nature": {"type": "nature", "score": 1}, "shopping": {"type": "shopping", "score": 1}, "max_time_minute": 360, "detour_tolerance_minute": 0}, | ||||
| #             "start": [48.8566, 2.3522] | ||||
| #             } | ||||
| #         ) | ||||
| #     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| # def test_new_trip_matches_prefs(client): | ||||
| #     # todo | ||||
| #     pass | ||||
| @@ -1,9 +1,9 @@ | ||||
| import yaml | ||||
| from geopy.distance import geodesic | ||||
|  | ||||
| import constants | ||||
| from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||
|  | ||||
| with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
| with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|     parameters = yaml.safe_load(f) | ||||
|     DETOUR_FACTOR = parameters['detour_factor'] | ||||
|     AVERAGE_WALKING_SPEED = parameters['average_walking_speed'] | ||||
|   | ||||
| @@ -9,11 +9,10 @@ from pywikibot import config | ||||
| config.put_throttle = 0 | ||||
| config.maxlag = 0 | ||||
|  | ||||
| from structs.preferences import Preferences, Preference | ||||
| from structs.landmark import Landmark | ||||
| from ..structs.preferences import Preferences, Preference | ||||
| from ..structs.landmark import Landmark | ||||
| from .take_most_important import take_most_important | ||||
| import constants | ||||
|  | ||||
| from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -30,10 +29,10 @@ class LandmarkManager: | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|  | ||||
|         with constants.AMENITY_SELECTORS_PATH.open('r') as f: | ||||
|         with AMENITY_SELECTORS_PATH.open('r') as f: | ||||
|             self.amenity_selectors = yaml.safe_load(f) | ||||
|  | ||||
|         with constants.LANDMARK_PARAMETERS_PATH.open('r') as f: | ||||
|         with LANDMARK_PARAMETERS_PATH.open('r') as f: | ||||
|             parameters = yaml.safe_load(f) | ||||
|             self.max_bbox_side = parameters['city_bbox_side'] | ||||
|             self.radius_close_to = parameters['radius_close_to'] | ||||
| @@ -42,13 +41,13 @@ class LandmarkManager: | ||||
|             self.tag_coeff = parameters['tag_coeff'] | ||||
|             self.N_important = parameters['N_important'] | ||||
|              | ||||
|         with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|         with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|             parameters = yaml.safe_load(f) | ||||
|             self.walking_speed = parameters['average_walking_speed'] | ||||
|             self.detour_factor = parameters['detour_factor'] | ||||
|  | ||||
|         self.overpass = Overpass() | ||||
|         CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR) | ||||
|         CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR) | ||||
|  | ||||
|  | ||||
|     def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]: | ||||
|   | ||||
| @@ -5,9 +5,9 @@ from scipy.optimize import linprog | ||||
| from collections import defaultdict, deque | ||||
| from geopy.distance import geodesic | ||||
|  | ||||
| from structs.landmark import Landmark | ||||
| from ..structs.landmark import Landmark | ||||
| from .get_time_separation import get_time | ||||
| import constants | ||||
| from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||
|  | ||||
|      | ||||
|  | ||||
| @@ -26,7 +26,7 @@ class Optimizer: | ||||
|     def __init__(self) : | ||||
|  | ||||
|         # load parameters from file | ||||
|         with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|         with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|             parameters = yaml.safe_load(f) | ||||
|             self.detour_factor = parameters['detour_factor'] | ||||
|             self.average_walking_speed = parameters['average_walking_speed'] | ||||
|   | ||||
| @@ -3,10 +3,10 @@ import yaml, logging | ||||
| from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull | ||||
| from math import pi | ||||
|  | ||||
| from structs.landmark import Landmark | ||||
| from ..structs.landmark import Landmark | ||||
| from . import take_most_important, get_time_separation | ||||
| from .optimizer import Optimizer | ||||
| import constants | ||||
| from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -24,7 +24,7 @@ class Refiner : | ||||
|         self.optimizer = optimizer | ||||
|  | ||||
|         # load parameters from file | ||||
|         with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|         with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|             parameters = yaml.safe_load(f) | ||||
|             self.detour_factor = parameters['detour_factor'] | ||||
|             self.detour_corridor_width = parameters['detour_corridor_width'] | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| from structs.landmark import Landmark | ||||
| from ..structs.landmark import Landmark | ||||
|  | ||||
| def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] : | ||||
|     L = len(landmarks) | ||||
|   | ||||
| @@ -292,18 +292,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: leak_tracker | ||||
|       sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" | ||||
|       sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.0.4" | ||||
|     version: "10.0.5" | ||||
|   leak_tracker_flutter_testing: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: leak_tracker_flutter_testing | ||||
|       sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" | ||||
|       sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.3" | ||||
|     version: "3.0.5" | ||||
|   leak_tracker_testing: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -332,18 +332,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: material_color_utilities | ||||
|       sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" | ||||
|       sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.0" | ||||
|     version: "0.11.1" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: meta | ||||
|       sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" | ||||
|       sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.12.0" | ||||
|     version: "1.15.0" | ||||
|   nested: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -609,10 +609,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: test_api | ||||
|       sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" | ||||
|       sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.0" | ||||
|     version: "0.7.2" | ||||
|   typed_data: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -641,10 +641,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: vm_service | ||||
|       sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" | ||||
|       sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.2.1" | ||||
|     version: "14.2.4" | ||||
|   web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user