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:
parent
4f169c483e
commit
41e2746d82
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.")
|
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
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
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. """
|
"""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
|
||||||
|
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)
|
Loading…
x
Reference in New Issue
Block a user