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

This commit is contained in:
Helldragon67 2024-11-30 17:55:33 +01:00
parent 4f169c483e
commit 41e2746d82
11 changed files with 192 additions and 134 deletions

File diff suppressed because one or more lines are too long

View File

@ -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.") raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None: if start is None:
raise HTTPException(status_code=406, detail="Start coordinates not provided") 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: if end is None:
end = start end = start
logger.info("No end coordinates provided. Using start=end.") 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 # First stage optimization
try: try:
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
except ArithmeticError: except ArithmeticError as exc:
raise HTTPException(status_code=500, detail="No solution found") raise HTTPException(status_code=500, detail="No solution found") from exc
except TimeoutError: except TimeoutError as exc:
raise HTTPException(status_code=500, detail="Optimzation took too long") raise HTTPException(status_code=500, detail="Optimzation took too long") from exc
# Second stage optimization # Second stage optimization
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) 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: try:
trip = cache_client.get(f"trip_{trip_uuid}") trip = cache_client.get(f"trip_{trip_uuid}")
return trip return trip
except KeyError: except KeyError as exc:
raise HTTPException(status_code=404, detail="Trip not found") raise HTTPException(status_code=404, detail="Trip not found") from exc
@app.get("/landmark/{landmark_uuid}") @app.get("/landmark/{landmark_uuid}")
@ -106,5 +108,5 @@ def get_landmark(landmark_uuid: str) -> Landmark:
try: try:
landmark = cache_client.get(f"landmark_{landmark_uuid}") landmark = cache_client.get(f"landmark_{landmark_uuid}")
return landmark return landmark
except KeyError: except KeyError as exc:
raise HTTPException(status_code=404, detail="Landmark not found") raise HTTPException(status_code=404, detail="Landmark not found") from exc

View File

@ -71,8 +71,10 @@ sightseeing:
- castle - castle
- museum - museum
museums:
tourism:
- museum
- aquarium
# to be used later on # to be used later on
restauration: restauration:

View File

@ -28,7 +28,7 @@ class DummyClient:
dictionary. dictionary.
""" """
_data = {} _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. Store a key-value pair in the internal dictionary.
@ -39,7 +39,7 @@ class DummyClient:
""" """
self._data[key] = value 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. Update the internal dictionary with multiple key-value pairs.
@ -49,7 +49,7 @@ class DummyClient:
""" """
self._data.update(data) 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. Retrieve the value associated with the given key.

View File

@ -71,9 +71,11 @@ class Landmark(BaseModel) :
time to the next landmark (if available), and whether the landmark is secondary. 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 "" 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 + ')' 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}]' 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: def distance(self, value: 'Landmark') -> float:

View File

@ -30,5 +30,5 @@ class Preferences(BaseModel) :
# Shopping (diriger plutôt vers des zones / rues commerçantes) # Shopping (diriger plutôt vers des zones / rues commerçantes)
shopping : Preference shopping : Preference
max_time_minute: Optional[int] = 6*60 max_time_minute: Optional[int] = 3*60
detour_tolerance_minute: Optional[int] = 0 detour_tolerance_minute: Optional[int] = 0

View File

@ -25,7 +25,7 @@ class Trip(BaseModel):
@classmethod @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. Initialize a new Trip object and ensure it is stored in the cache.
""" """

View 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

View File

@ -1,38 +1,20 @@
"""Collection of tests to ensure correct implementation and track progress. """ """Collection of tests to ensure correct implementation and track progress. """
from typing import List
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest import pytest
from .test_utils import landmarks_to_osmid, load_trip_landmarks, log_trip_details
from ..main import app from ..main import app
from ..structs.landmark import Landmark
@pytest.fixture(scope="module")
@pytest.fixture()
def client(): def client():
"""Client used to call the app."""
return TestClient(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. Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
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.
Args: Args:
client: client:
@ -42,7 +24,11 @@ def test_turckheim(client, request):
response = client.post( response = client.post(
"/trip/new", "/trip/new",
json={ 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] "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 assert len(landmarks) > 2 # check that there is something to visit
# Test no. 3 def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
def test_bellecour(client, request) :
""" """
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: Args:
client: client:
request: request:
""" """
duration_minutes = 60 duration_minutes = 30
response = client.post( response = client.post(
"/trip/new", "/trip/new",
json={ 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): # def test_new_trip_single_prefs(client):
# response = client.post( # response = client.post(
# "/trip/new", # "/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): # def test_new_trip_matches_prefs(client):
# # todo
# pass # pass

View 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)