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.")
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

View File

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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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.
"""

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. """
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

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)