more testing and better pylint score
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m48s
				
			
		
			
				
	
				Run linting on the backend code / Build (pull_request) Failing after 29s
				
			
		
			
				
	
				Run testing on the backend code / Build (pull_request) Failing after 1m13s
				
			
		
			
				
	
				Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 47s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m48s
				
			Run linting on the backend code / Build (pull_request) Failing after 29s
				
			Run testing on the backend code / Build (pull_request) Failing after 1m13s
				
			Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 47s
				
			This commit is contained in:
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -39,6 +39,8 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl | ||||
|         raise HTTPException(status_code=406, detail="All preferences are 0.") | ||||
|     if start is None: | ||||
|         raise HTTPException(status_code=406, detail="Start coordinates not provided") | ||||
|     if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180): | ||||
|         raise HTTPException(status_code=423, detail="Start coordinates not in range") | ||||
|     if end is None: | ||||
|         end = start | ||||
|         logger.info("No end coordinates provided. Using start=end.") | ||||
| @@ -59,10 +61,10 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl | ||||
|     # First stage optimization | ||||
|     try: | ||||
|         base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) | ||||
|     except ArithmeticError: | ||||
|         raise HTTPException(status_code=500, detail="No solution found") | ||||
|     except TimeoutError: | ||||
|         raise HTTPException(status_code=500, detail="Optimzation took too long") | ||||
|     except ArithmeticError as exc: | ||||
|         raise HTTPException(status_code=500, detail="No solution found") from exc | ||||
|     except TimeoutError as exc: | ||||
|         raise HTTPException(status_code=500, detail="Optimzation took too long") from exc | ||||
|  | ||||
|     # Second stage optimization | ||||
|     refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) | ||||
| @@ -88,8 +90,8 @@ def get_trip(trip_uuid: str) -> Trip: | ||||
|     try: | ||||
|         trip = cache_client.get(f"trip_{trip_uuid}") | ||||
|         return trip | ||||
|     except KeyError: | ||||
|         raise HTTPException(status_code=404, detail="Trip not found") | ||||
|     except KeyError as exc: | ||||
|         raise HTTPException(status_code=404, detail="Trip not found") from exc | ||||
|  | ||||
|  | ||||
| @app.get("/landmark/{landmark_uuid}") | ||||
| @@ -106,5 +108,5 @@ def get_landmark(landmark_uuid: str) -> Landmark: | ||||
|     try: | ||||
|         landmark = cache_client.get(f"landmark_{landmark_uuid}") | ||||
|         return landmark | ||||
|     except KeyError: | ||||
|         raise HTTPException(status_code=404, detail="Landmark not found") | ||||
|     except KeyError as exc: | ||||
|         raise HTTPException(status_code=404, detail="Landmark not found") from exc | ||||
|   | ||||
| @@ -71,8 +71,10 @@ sightseeing: | ||||
|     - castle | ||||
|     - museum | ||||
|  | ||||
|  | ||||
|  | ||||
| museums: | ||||
|   tourism: | ||||
|     - museum | ||||
|     - aquarium | ||||
|  | ||||
| # to be used later on | ||||
| restauration: | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class DummyClient: | ||||
|             dictionary. | ||||
|     """ | ||||
|     _data = {} | ||||
|     def set(self, key, value, **kwargs): | ||||
|     def set(self, key, value, **kwargs):    # pylint: disable=unused-argument | ||||
|         """ | ||||
|         Store a key-value pair in the internal dictionary. | ||||
|  | ||||
| @@ -39,7 +39,7 @@ class DummyClient: | ||||
|         """ | ||||
|         self._data[key] = value | ||||
|  | ||||
|     def set_many(self, data, **kwargs): | ||||
|     def set_many(self, data, **kwargs): # pylint: disable=unused-argument | ||||
|         """ | ||||
|         Update the internal dictionary with multiple key-value pairs. | ||||
|  | ||||
| @@ -49,7 +49,7 @@ class DummyClient: | ||||
|         """ | ||||
|         self._data.update(data) | ||||
|  | ||||
|     def get(self, key, **kwargs): | ||||
|     def get(self, key, **kwargs):   # pylint: disable=unused-argument | ||||
|         """ | ||||
|         Retrieve the value associated with the given key. | ||||
|  | ||||
|   | ||||
| @@ -71,9 +71,11 @@ class Landmark(BaseModel) : | ||||
|             time to the next landmark (if available), and whether the landmark is secondary. | ||||
|         """ | ||||
|         time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" | ||||
|         is_secondary_str = f", secondary" if self.is_secondary else "" | ||||
|         is_secondary_str = ", secondary" if self.is_secondary else "" | ||||
|         type_str = '(' + self.type + ')' | ||||
|         if self.type in ["start", "finish", "nature", "shopping"] : type_str += '\t ' | ||||
|         if self.type in ["start", "finish", "nature", "shopping"] : | ||||
|             type_str += '\t ' | ||||
|  | ||||
|         return f'Landmark{type_str}: [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}{is_secondary_str}]' | ||||
|  | ||||
|     def distance(self, value: 'Landmark') -> float: | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class LinkedLandmarks: | ||||
|     to reference the next landmark in the list. This is not very efficient, but appropriate for the expected use case | ||||
|     ("short" trips with onyl few landmarks). | ||||
|     """ | ||||
|      | ||||
|  | ||||
|     _landmarks = list[Landmark] | ||||
|     total_time: int = 0 | ||||
|  | ||||
|   | ||||
| @@ -30,5 +30,5 @@ class Preferences(BaseModel) : | ||||
|     # Shopping (diriger plutôt vers des zones / rues commerçantes) | ||||
|     shopping : Preference | ||||
|  | ||||
|     max_time_minute: Optional[int] = 6*60 | ||||
|     max_time_minute: Optional[int] = 3*60 | ||||
|     detour_tolerance_minute: Optional[int] = 0 | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class Trip(BaseModel): | ||||
|  | ||||
|  | ||||
|     @classmethod | ||||
|     def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client: Client) -> "Trip": | ||||
|     def from_linked_landmarks(cls, landmarks: LinkedLandmarks, cache_client: Client) -> "Trip": | ||||
|         """ | ||||
|         Initialize a new Trip object and ensure it is stored in the cache. | ||||
|         """ | ||||
|   | ||||
							
								
								
									
										62
									
								
								backend/src/tests/test_invalid_input.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								backend/src/tests/test_invalid_input.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| """Collection of tests to ensure correct handling of invalid input.""" | ||||
|  | ||||
| from fastapi.testclient import TestClient | ||||
| import pytest | ||||
|  | ||||
| from ..main import app | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="module") | ||||
| def invalid_client(): | ||||
|     """Client used to call the app.""" | ||||
|     return TestClient(app) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "start,preferences,status_code", | ||||
|     [ | ||||
|         # Invalid case: no preferences at all. | ||||
|         ([48.8566, 2.3522], {}, 422), | ||||
|  | ||||
|         # Invalid cases: incomplete preferences. | ||||
|         ([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no shopping | ||||
|                                  "nature": {"type": "nature", "score": 5}, | ||||
|                                  }, 422), | ||||
|         ([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no nature | ||||
|                                  "shopping": {"type": "shopping", "score": 5}, | ||||
|                                  }, 422), | ||||
|         ([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5},      # no sightseeing | ||||
|                                  "shopping": {"type": "shopping", "score": 5}, | ||||
|                                  }, 422), | ||||
|  | ||||
|         # Invalid cases: unexisting coords | ||||
|         ([91, 181], {"sightseeing": {"type": "nature", "score": 5}, | ||||
|                      "nature": {"type": "nature", "score": 5}, | ||||
|                      "shopping": {"type": "shopping", "score": 5}, | ||||
|                     }, 423), | ||||
|         ([-91, 181], {"sightseeing": {"type": "nature", "score": 5}, | ||||
|                      "nature": {"type": "nature", "score": 5}, | ||||
|                      "shopping": {"type": "shopping", "score": 5}, | ||||
|                     }, 423), | ||||
|         ([91, -181], {"sightseeing": {"type": "nature", "score": 5}, | ||||
|                      "nature": {"type": "nature", "score": 5}, | ||||
|                      "shopping": {"type": "shopping", "score": 5}, | ||||
|                     }, 423), | ||||
|         ([-91, -181], {"sightseeing": {"type": "nature", "score": 5}, | ||||
|                      "nature": {"type": "nature", "score": 5}, | ||||
|                      "shopping": {"type": "shopping", "score": 5}, | ||||
|                     }, 423), | ||||
|     ] | ||||
| ) | ||||
| def test_input(invalid_client, start, preferences, status_code):   # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test new trip creation with different sets of preferences and locations. | ||||
|     """ | ||||
|     response = invalid_client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
|             "preferences": preferences, | ||||
|             "start": start | ||||
|         } | ||||
|     ) | ||||
|     assert response.status_code == status_code | ||||
| @@ -1,38 +1,20 @@ | ||||
| """Collection of tests to ensure correct implementation and track progress. """ | ||||
|  | ||||
| from typing import List | ||||
| from fastapi.testclient import TestClient | ||||
| import pytest | ||||
|  | ||||
| from .test_utils import landmarks_to_osmid, load_trip_landmarks, log_trip_details | ||||
| from ..main import app | ||||
| from ..structs.landmark import Landmark | ||||
|  | ||||
|  | ||||
| @pytest.fixture() | ||||
| @pytest.fixture(scope="module") | ||||
| def client(): | ||||
|     """Client used to call the app.""" | ||||
|     return TestClient(app) | ||||
|  | ||||
|  | ||||
| def test_new_trip_invalid_prefs(client): | ||||
| def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test n°1 : base test for checking if the API returns correct error code when no preferences are specified. | ||||
|  | ||||
|     Args: | ||||
|         client: | ||||
|     """ | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
|             "preferences": {}, | ||||
|             "start": [48.8566, 2.3522] | ||||
|             } | ||||
|         ) | ||||
|     assert response.status_code == 422 | ||||
|  | ||||
|  | ||||
| def test_turckheim(client, request): | ||||
|     """ | ||||
|     Test n°2 : Custom test in Turckheim to ensure small villages are also supported. | ||||
|     Test n°1 : Custom test in Turckheim to ensure small villages are also supported. | ||||
|  | ||||
|     Args: | ||||
|         client: | ||||
| @@ -42,7 +24,11 @@ def test_turckheim(client, request): | ||||
|     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}, | ||||
|             "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] | ||||
|             } | ||||
|         ) | ||||
| @@ -59,16 +45,15 @@ def test_turckheim(client, request): | ||||
|     assert len(landmarks) > 2           # check that there is something to visit | ||||
|  | ||||
|  | ||||
| # Test no. 3 | ||||
| def test_bellecour(client, request) : | ||||
| def test_bellecour(client, request) :   # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test n°3 : Custom test in Lyon centre to ensure proper decision making in crowded area. | ||||
|     Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area. | ||||
|      | ||||
|     Args: | ||||
|         client: | ||||
|         request: | ||||
|     """ | ||||
|     duration_minutes = 60 | ||||
|     duration_minutes = 30 | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
|         json={ | ||||
| @@ -90,89 +75,6 @@ def test_bellecour(client, request) : | ||||
|  | ||||
|  | ||||
|  | ||||
| def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] : | ||||
|     """ | ||||
|     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: str): | ||||
|     """ | ||||
|     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: str) -> List[Landmark]: | ||||
|     """ | ||||
|     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) | ||||
|         # # Convert UUIDs to strings explicitly | ||||
|         # landmark_data = { | ||||
|         #     key: str(value) if isinstance(value, UUID) else value | ||||
|         #     for key, value in landmark_data.items() | ||||
|         # } | ||||
|         landmarks.append(Landmark(**landmark_data)) # Create Landmark objects | ||||
|         next_uuid = landmark_data.get('next_uuid')  # Prepare for the next iteration | ||||
|  | ||||
|     return landmarks | ||||
|  | ||||
|  | ||||
| def log_trip_details(request, landmarks: List[Landmark], duration: int, target_duration: int) : | ||||
|     """ | ||||
|     Allows to show the detailed trip in the html test report. | ||||
|      | ||||
|     Args: | ||||
|         request: | ||||
|         landmarks (list): the ordered list of visited landmarks  | ||||
|         duration (int): the total duration of this trip | ||||
|         target_duration(int): the target duration of this trip | ||||
|     """ | ||||
|     trip_string = [f"{landmark.name} ({landmark.attractiveness} | {landmark.duration}) - {landmark.time_to_reach_next}" for landmark in landmarks] | ||||
|  | ||||
|     # Pass additional info to pytest for reporting | ||||
|     request.node.trip_details = trip_string | ||||
|     request.node.trip_duration = str(duration)              # result['total_time'] | ||||
|     request.node.target_duration = str(target_duration) | ||||
|  | ||||
|  | ||||
| # def test_new_trip_single_prefs(client): | ||||
| #     response = client.post( | ||||
| #         "/trip/new", | ||||
| @@ -185,5 +87,4 @@ def log_trip_details(request, landmarks: List[Landmark], duration: int, target_d | ||||
|  | ||||
|  | ||||
| # def test_new_trip_matches_prefs(client): | ||||
| #     # todo | ||||
| #     pass | ||||
|   | ||||
							
								
								
									
										89
									
								
								backend/src/tests/test_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								backend/src/tests/test_utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| """Helper methods for testing.""" | ||||
| from typing import List | ||||
| from fastapi import HTTPException | ||||
|  | ||||
| from ..structs.landmark import Landmark | ||||
|  | ||||
|  | ||||
| def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] : | ||||
|     """ | ||||
|     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: str): | ||||
|     """ | ||||
|     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 HTTPException(status_code=999, detail=f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}") | ||||
|  | ||||
|     json_data = response.json() | ||||
|  | ||||
|     if "detail" in json_data: | ||||
|         raise HTTPException(status_code=999, detail=json_data["detail"]) | ||||
|  | ||||
|  | ||||
|     return json_data | ||||
|  | ||||
|  | ||||
| def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]: | ||||
|     """ | ||||
|     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) | ||||
|         # # Convert UUIDs to strings explicitly | ||||
|         # landmark_data = { | ||||
|         #     key: str(value) if isinstance(value, UUID) else value | ||||
|         #     for key, value in landmark_data.items() | ||||
|         # } | ||||
|         landmarks.append(Landmark(**landmark_data)) # Create Landmark objects | ||||
|         next_uuid = landmark_data.get('next_uuid')  # Prepare for the next iteration | ||||
|  | ||||
|     return landmarks | ||||
|  | ||||
|  | ||||
| def log_trip_details(request, landmarks: List[Landmark], duration: int, target_duration: int) : | ||||
|     """ | ||||
|     Allows to show the detailed trip in the html test report. | ||||
|      | ||||
|     Args: | ||||
|         request: | ||||
|         landmarks (list): the ordered list of visited landmarks  | ||||
|         duration (int): the total duration of this trip | ||||
|         target_duration(int): the target duration of this trip | ||||
|     """ | ||||
|     trip_string = [f"{landmark.name} ({landmark.attractiveness} | {landmark.duration}) - {landmark.time_to_reach_next}" for landmark in landmarks] | ||||
|  | ||||
|     # Pass additional info to pytest for reporting | ||||
|     request.node.trip_details = trip_string | ||||
|     request.node.trip_duration = str(duration)              # result['total_time'] | ||||
|     request.node.target_duration = str(target_duration) | ||||
		Reference in New Issue
	
	Block a user