From 4dd06618bdd2d8a48f85642ac590fa943429f943 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 30 May 2024 15:59:11 -0700 Subject: [PATCH 1/6] Prepared backend unit tests Created some unit tests for the backend, cleaned some of the backend code. --- backend/.flaskenv | 2 - backend/app/base.py | 19 ++-- backend/tests/test_basic.py | 168 ++++++++++++++++++++++++++++-------- 3 files changed, 143 insertions(+), 46 deletions(-) delete mode 100644 backend/.flaskenv diff --git a/backend/.flaskenv b/backend/.flaskenv deleted file mode 100644 index 5bba54d1..00000000 --- a/backend/.flaskenv +++ /dev/null @@ -1,2 +0,0 @@ -FLASK_APP=app/base.py -FLASK_ENV=development \ No newline at end of file diff --git a/backend/app/base.py b/backend/app/base.py index d766f614..5c7232f3 100644 --- a/backend/app/base.py +++ b/backend/app/base.py @@ -169,24 +169,20 @@ def get_precipitation_data(selectedYear: int): return monthly_avg.to_dict(orient='records') -app.get('/bird-data/{bird_name}') -def get_bird_data(bird_name): - if bird_name in bird_data: - return bird_data - - class PredictionInputs(BaseModel): bird: str year: int emissions: str @app.put('/prediction') -async def predict(prediction_input: PredictionInputs): +async def get_predictions(prediction_input: PredictionInputs): selected_bird = prediction_input.bird selected_year = str(prediction_input.year) emission_Type = prediction_input.emissions ''' + If the backend needs to cache the bird, year, or emissions such as recording jobs that it is performing, store it in a session. Currently unimplemented, as the backend can remain stateless. + # Store the inputs into a session, cached for future utility session['bird'] = input_bird#prediction_input['bird'] session['year'] = input_year#prediction_input['year'] @@ -206,10 +202,13 @@ async def predict(prediction_input: PredictionInputs): buffer = BytesIO() dataImg.save(buffer, format="png") - # Artificial delay to simulate model prediction time - #sleep(2.5) + # FastAPI requests can handle asynchronous predictions. + # In practice, this would be waiting for a machine learning model to generate + # predictions. + # Simualted here with a sleep() function. + + # sleep(2.5) - #If we'll need to encapsulate a file, use this: return { "prediction": base64.b64encode(buffer.getvalue()).decode(), "resFormat": dataImg.format diff --git a/backend/tests/test_basic.py b/backend/tests/test_basic.py index 17684111..3c89a446 100644 --- a/backend/tests/test_basic.py +++ b/backend/tests/test_basic.py @@ -1,46 +1,146 @@ import pytest -from app.base import api as flask_app -class TestConfig: - TESTING = True - DEBUG = False +from fastapi.responses import Response +from fastapi.testclient import TestClient +import json + +from app.base import app as fast_app + + +''' +Helper function for tests +''' + +def read_response_body(response): + return json.loads(response.content.decode()) + +''' +Test functions +''' @pytest.fixture def app(): - flask_app.config.update({ - "TESTING": True, - }) - yield flask_app + + yield fast_app @pytest.fixture def client(app): - return app.test_client() - -def test_get_bird_data(client): - # Testing for a bird that exists - response = client.get('/bird-data/Bird 1') - assert response.status_code == 200 - assert 'Generic info for unique bird 1' in response.get_data(as_text=True) - - # Testing for a bird that does not exist - response = client.get('/bird-data/Unknown Bird') - assert response.status_code == 404 - assert 'Invalid bird' in response.get_data(as_text=True) - -def test_get_invalid_bird_data(client): - response = client.get('/bird-data/invalid_bird_name') + + yield TestClient(app) + + +def test_get_bird_info(client): + + valid_birds = { + "Blackpoll Warbler" : "Migration details about Blackpoll Warbler", + "Bald Eagle" : "Migration details about Bald Eagle", + "White Fronted Goose" : "Migration details about White Fronted Goose", + "Long Billed Curlew" : "Migration details about Long Billed Curlew", + "Whimbrel" : "Migration details about Whimbrel" + } + + # Test for each bird that exists + for bird in valid_birds: + + response = client.get(f'/bird-info/{bird}') + + assert response.status_code == 200 + + # Decode the response body. + response_body = read_response_body(response) + assert response_body['name'] == bird + assert response_body['info'] == valid_birds[bird] + + # Test an invalid entry + response = client.get(f'/bird-info/Invalid_Bird') + assert response.status_code == 404 - assert 'Invalid bird' in response.get_data(as_text=True) - -def test_get_bird_data_empty_bird_name(client): - response = client.get('/bird-data/') - assert response.status_code == 404 # Or whatever your expected behavior is + +def test_get_temp_data(client): + + response = client.get(f'/temperature/{2021}') + + assert response.status_code == 200 + # TODO: continue coverage for climate data + # response_body = read_response_body(response) + +@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") +def test_valid_predictions(client): + + valid_birds = [ + "warbler", + "eagle", + "anser", + "curlew", + "whimbrel" + ] + + for bird in valid_birds: + + # Test all possible valid emission types. + ssps = [ + 'ssp126', + 'ssp245', + 'ssp370', + 'ssp585' + ] + + for ssp in ssps: + + # Test all possible valid years. + for year in range(2021, 2100): + payload = { + "bird": bird, + "year": year, + "emissions": ssp, + } + + response = client.put('/prediction', json= payload) + response_body = read_response_body(response) + + assert response.status_code == 200 + assert response_body['resFormat'] == "PNG" -def test_get_bird_data_special_characters(client): - response = client.get('/bird-data/@#!$') - assert response.status_code == 404 # Assuming special characters are not valid +def test_invalid_predictions(client): + + # Test invalid inputs + invalid_entries = [ + ("bad_bird", 2050, 'ssp126'), + ("eagle", 2101, 'ssp126'), + ("anser", 2050, 'invalid') + ] + + for invalid_entry in invalid_entries: + + # Test bad inputs. + try: + bad_payload = { + "bird": invalid_entry[0], + "year": invalid_entry[1], + "emissions": invalid_entry[2] + } + + client.put('/prediction', json= bad_payload) + except Exception as e: + + if invalid_entry[0] == 'bad_bird': + # This should throw a key error. + assert isinstance(e, KeyError) + else: + # This should throw a file not found error. + assert isinstance(e, FileNotFoundError) -def test_post_bird_data_not_allowed(client): - response = client.post('/bird-data/Bird 1', data={}) - assert response.status_code == 405 # Method Not Allowed +def test_get_bird_ids(client): + + # Test the + valid_birds = [ + "warbler", + "eagle", + "anser", + "curlew", + "whimbrel" + ] + + for bird in valid_birds: + client.get('/get_bird_ids', ) \ No newline at end of file From 41a401d400272134f360050b132dd291756547d1 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 31 May 2024 02:47:46 -0700 Subject: [PATCH 2/6] Created unit tests for backend Created backend unit tests for the backend RESTful API. --- backend/app/base.py | 20 ++-- .../tests/{test_basic.py => test_restful.py} | 92 ++++++++++++++++++- src/components/GeneralMigrationMap.js | 2 +- 3 files changed, 95 insertions(+), 19 deletions(-) rename backend/tests/{test_basic.py => test_restful.py} (59%) diff --git a/backend/app/base.py b/backend/app/base.py index 5c7232f3..f299546d 100644 --- a/backend/app/base.py +++ b/backend/app/base.py @@ -240,19 +240,10 @@ def get_bird_sdm_data(bird_name): else: raise HTTPException(status_code=404, detail='Bird not found') - -@app.get('/json/{filename}') -def send_json(filename): - climate_file_loc = os.path.join('climate_data/json_data', filename) - if not os.path.exists(climate_file_loc): - raise HTTPException( - status_code= 404, - detail= f"File path for {climate_file_loc} does not exist") - return FileResponse(climate_file_loc) - @app.get('/get_trajectory_data') def get_trajectory_data(bird: str, birdID: str): + filename = f'./data/{bird}.csv' try: df = pd.read_csv(filename) @@ -262,7 +253,7 @@ def get_trajectory_data(bird: str, birdID: str): if bird_data.empty: raise HTTPException( status_code= 404, - details= 'No trajectory data found for given bird ID') + detail= 'No trajectory data found for given bird ID') # Convert data to dictionary format trajectory_data = bird_data[['LATITUDE', 'LONGITUDE', 'TIMESTAMP']].to_dict(orient='records') @@ -270,12 +261,13 @@ def get_trajectory_data(bird: str, birdID: str): except FileNotFoundError: raise HTTPException( status_code= 404, - details= f'CSV file for {bird} not found') + detail= f'CSV file for {filename} not found') @app.get('/get_bird_ids') def get_bird_ids(bird: str): filename = f'./data/{bird}.csv' + try: df = pd.read_csv(filename) bird_ids = df['ID'].unique().tolist() @@ -283,13 +275,15 @@ def get_bird_ids(bird: str): except FileNotFoundError: raise HTTPException( status_code=404, - detail= f'CSV file for {bird} not found') + detail= f'CSV file for {filename} not found') @app.get('/get_general_migration') def get_general_migration(selected_bird: str): filename = f'./data/{selected_bird}.csv' + print(filename) + try: df = pd.read_csv(filename, low_memory=False) diff --git a/backend/tests/test_basic.py b/backend/tests/test_restful.py similarity index 59% rename from backend/tests/test_basic.py rename to backend/tests/test_restful.py index 3c89a446..bc48e875 100644 --- a/backend/tests/test_basic.py +++ b/backend/tests/test_restful.py @@ -4,6 +4,7 @@ from fastapi.testclient import TestClient import json +import os from app.base import app as fast_app @@ -60,10 +61,7 @@ def test_get_bird_info(client): def test_get_temp_data(client): response = client.get(f'/temperature/{2021}') - assert response.status_code == 200 - # TODO: continue coverage for climate data - # response_body = read_response_body(response) @pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") def test_valid_predictions(client): @@ -131,9 +129,27 @@ def test_invalid_predictions(client): # This should throw a file not found error. assert isinstance(e, FileNotFoundError) +def test_get_sdm_data(client): + + valid_birds = [ + "Blackpoll Warbler", + "Bald Eagle", + "White Fronted Goose", + "Long Billed Curlew", + "Whimbrel" + ] + + for bird in valid_birds: + response = client.get(f'/bird-info/{bird}') + + assert response.status_code == 200 + + assert client.get(f'/bird-info/bad_get').status_code == 404 + +@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") def test_get_bird_ids(client): - # Test the + # Test if the backend can retreive bird ids for all valid birds. valid_birds = [ "warbler", "eagle", @@ -142,5 +158,71 @@ def test_get_bird_ids(client): "whimbrel" ] + test_directory = os.getcwd() + os.chdir('../app') + for bird in valid_birds: - client.get('/get_bird_ids', ) \ No newline at end of file + + ids_response = client.get(f'/get_bird_ids?bird={bird}') + assert ids_response.status_code == 200 + + ids_content = read_response_body(ids_response) + for id in ids_content: + traj_resp = client.get(f'/get_trajectory_data?bird={bird}&birdID={id}') + + assert traj_resp.status_code == 200 + + # Assert that for each bird, a bad response is handled properly. + assert client.get(f'/get_trajectory_data?bird={bird}&birdID=BAD_ID').status_code == 404 + + # Ensure that bad 'get' requests are handled properly. + response = client.get(f'/get_bird_ids?bird=bad_ID') + assert response.status_code == 404 + + assert client.get(f'/get_trajectory_data?bird=BAD_ID&birdID=BAD_ID').status_code == 404 + + os.chdir(test_directory) + +def test_general_migration(client): + + valid_birds = [ + "warbler", + "eagle", + "anser", + "curlew", + "whimbrel" + ] + + test_directory = os.getcwd() + os.chdir('../app') + + for bird in valid_birds: + + response = client.get(f'/get_general_migration?selected_bird={bird}') + assert response.status_code == 200 + + assert client.get(f'/get_general_migration?selected_bird=BAD_ID').status_code == 400 + + os.chdir(test_directory) + + +def test_get_heatmap(client): + valid_birds = [ + "warbler", + "eagle", + "anser", + "curlew", + "whimbrel" + ] + + test_directory = os.getcwd() + os.chdir('../app') + + for bird in valid_birds: + + response = client.get(f'/get_heatmap_data?bird={bird}') + assert response.status_code == 200 + + assert client.get(f'/get_heatmap_data?bird=BAD_ID').status_code == 400 + + os.chdir(test_directory) \ No newline at end of file diff --git a/src/components/GeneralMigrationMap.js b/src/components/GeneralMigrationMap.js index a3af838a..a2be826a 100644 --- a/src/components/GeneralMigrationMap.js +++ b/src/components/GeneralMigrationMap.js @@ -49,7 +49,7 @@ function GeneralMigrationMap({ selectedBird }) { const getMigrationPattern = async () => { setLoading(true); try { - const response = await axios.get(`${baseUrl}/get_general_migration?bird=${selectedBird}`); + const response = await axios.get(`${baseUrl}/get_general_migration?selectedBird=${selectedBird}`); if (response && response.data && response.data.segmented_polylines) { setSegmentedPolylines(response.data.segmented_polylines); } else { From a35af47f4c57f384020b69dcc77af4b596fd7101 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 31 May 2024 02:52:21 -0700 Subject: [PATCH 3/6] readded pillow dependency --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 29ce6402..7bb6548b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,6 +4,7 @@ python-dotenv jinja2 pandas Shapely +pillow pytest requests \ No newline at end of file From 0d7d515c0390fcb6524441814f8e092efc175e9a Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 31 May 2024 02:58:11 -0700 Subject: [PATCH 4/6] Update test_restful.py Updated tests to match those of CI/CD --- backend/tests/test_restful.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_restful.py b/backend/tests/test_restful.py index bc48e875..cc44751c 100644 --- a/backend/tests/test_restful.py +++ b/backend/tests/test_restful.py @@ -159,7 +159,7 @@ def test_get_bird_ids(client): ] test_directory = os.getcwd() - os.chdir('../app') + os.chdir('./app') for bird in valid_birds: @@ -194,7 +194,7 @@ def test_general_migration(client): ] test_directory = os.getcwd() - os.chdir('../app') + os.chdir('./app') for bird in valid_birds: @@ -216,7 +216,7 @@ def test_get_heatmap(client): ] test_directory = os.getcwd() - os.chdir('../app') + os.chdir('./app') for bird in valid_birds: From 56c72474e096a0de7a7e63825615e9edc5dbc9dc Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 4 Jun 2024 12:49:22 -0700 Subject: [PATCH 5/6] Minor changes to backend testing Minor changes to tests to ensure commented tests work and run in reasonable amount of time. --- backend/tests/test_restful.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_restful.py b/backend/tests/test_restful.py index cc44751c..dc95d688 100644 --- a/backend/tests/test_restful.py +++ b/backend/tests/test_restful.py @@ -63,9 +63,12 @@ def test_get_temp_data(client): response = client.get(f'/temperature/{2021}') assert response.status_code == 200 -@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") +#@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") def test_valid_predictions(client): + test_directory = os.getcwd() + os.chdir('./app') + valid_birds = [ "warbler", "eagle", @@ -83,6 +86,8 @@ def test_valid_predictions(client): 'ssp370', 'ssp585' ] + ''' + uncomment to test every possible SSP and every possible year. for ssp in ssps: @@ -98,7 +103,18 @@ def test_valid_predictions(client): response_body = read_response_body(response) assert response.status_code == 200 - assert response_body['resFormat'] == "PNG" + assert response_body['resFormat'] == "PNG"''' + payload = { + "bird": "warbler", + "year": 2025, + "emissions": 'ssp245', + } + + response = client.put('/prediction', json= payload) + assert response.status_code == 200 + + os.chdir(test_directory) + def test_invalid_predictions(client): @@ -146,7 +162,7 @@ def test_get_sdm_data(client): assert client.get(f'/bird-info/bad_get').status_code == 404 -@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") +#@pytest.mark.skip(reason= "This test takes a very long time. Uncomment when doing other tests.") def test_get_bird_ids(client): # Test if the backend can retreive bird ids for all valid birds. @@ -167,10 +183,9 @@ def test_get_bird_ids(client): assert ids_response.status_code == 200 ids_content = read_response_body(ids_response) - for id in ids_content: - traj_resp = client.get(f'/get_trajectory_data?bird={bird}&birdID={id}') + traj_resp = client.get(f'/get_trajectory_data?bird={bird}&birdID={ids_content[0]}') - assert traj_resp.status_code == 200 + assert traj_resp.status_code == 200 # Assert that for each bird, a bad response is handled properly. assert client.get(f'/get_trajectory_data?bird={bird}&birdID=BAD_ID').status_code == 404 From f27ecc06d0b626a5b449fc9b592219cb84d843d2 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 4 Jun 2024 13:59:24 -0700 Subject: [PATCH 6/6] Updated readme --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c51f072f..f8474e8a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # Usage ## To start the Flask Backend: -- cd into /birdmig/backend/app -- enter ```uvicorn base:app --reload``` in your terminal +- ```cd``` into ```/birdmig/backend/app``` +- Run the backend + - To run the backend for showcase and documentation purposes, enter ```fastapi dev base.py``` in your terminal. + - Access docs by navigating to ```localhost:8000/docs``` on your web browser + - To run the backend for debugging purposes, run ```uvicorn base:app --reload``` in your terminal. + - Note that this only runs the backend. It can not be navigated to on the web browser, nor will documentation be available. ## To start the React Frontend: