unit consistency changes

This commit is contained in:
2025-10-01 14:36:42 -07:00
parent 7ffc57a7a8
commit bb30f9eb2b
18 changed files with 726 additions and 120 deletions

View File

@@ -10,7 +10,7 @@ def sample_chart_data(db_session):
# Create sample food
food = Food(
name="Sample Food",
serving_size="100g",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
@@ -35,7 +35,7 @@ def sample_chart_data(db_session):
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=1.0
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()

View File

@@ -25,11 +25,9 @@ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_
def session_fixture():
Base.metadata.create_all(bind=test_engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=test_engine)
yield db
db.close()
Base.metadata.drop_all(bind=test_engine)
@pytest.fixture(name="client")
def client_fixture(session):
@@ -50,7 +48,7 @@ def test_detailed_page_default_date(client, session):
# Create mock data for today
food = Food(
name="Apple",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=52,
protein=0.3,
@@ -71,7 +69,7 @@ def test_detailed_page_default_date(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -84,7 +82,7 @@ def test_detailed_page_default_date(client, session):
response = client.get("/detailed?person=Sarah")
assert response.status_code == 200
# Check for the unescaped version or the page title
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert test_date.strftime('%B %d, %Y') in response.text
assert "Fruit Snack" in response.text
@@ -93,7 +91,7 @@ def test_detailed_page_with_plan_date(client, session):
# Create mock data
food = Food(
name="Apple",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=52,
protein=0.3,
@@ -114,7 +112,7 @@ def test_detailed_page_with_plan_date(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -126,7 +124,7 @@ def test_detailed_page_with_plan_date(client, session):
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
assert response.status_code == 200
# Check for the page content without assuming apostrophe encoding
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert "Fruit Snack" in response.text
@@ -134,7 +132,7 @@ def test_detailed_page_with_template_id(client, session):
# Create mock data
food = Food(
name="Banana",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=89,
protein=1.1,
@@ -155,7 +153,7 @@ def test_detailed_page_with_template_id(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -177,7 +175,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
# Create mock data for a tracked day
food1 = Food(
name="Chicken Breast",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=165, protein=31, carbs=0, fat=3.6,
fiber=0, sugar=0, sodium=74, calcium=11,
@@ -185,7 +183,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
)
food2 = Food(
name="Broccoli",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=55, protein=3.7, carbs=11.2, fat=0.6,
fiber=5.1, sugar=2.2, sodium=33, calcium=47,
@@ -201,8 +199,8 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
session.commit()
session.refresh(meal)
meal_food1 = MealFood(meal_id=meal.id, food_id=food1.id, quantity=1.5) # 150g chicken
meal_food2 = MealFood(meal_id=meal.id, food_id=food2.id, quantity=2.0) # 200g broccoli
meal_food1 = MealFood(meal_id=meal.id, food_id=food1.id, quantity=150.0) # 150g chicken
meal_food2 = MealFood(meal_id=meal.id, food_id=food2.id, quantity=200.0) # 200g broccoli
session.add_all([meal_food1, meal_food2])
session.commit()
@@ -230,8 +228,8 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
assert "Chicken and Broccoli" in response.text
assert "Chicken Breast" in response.text
assert "Broccoli" in response.text
assert "1.5 × 100g" in response.text # Check quantity and unit for chicken
assert "2.0 × 100g" in response.text # Check quantity and unit for broccoli
assert "150.0g of Chicken Breast (1.5 servings of 100.0g)" in response.text
assert "200.0g of Broccoli (2.0 servings of 100.0g)" in response.text
assert "248" in response.text # Check calories for chicken (1.5 * 165 = 247.5, rounded to 248)
assert "110" in response.text # Check calories for broccoli (2.0 * 55 = 110)
@@ -240,7 +238,7 @@ def test_detailed_page_with_invalid_plan_date(client):
response = client.get(f"/detailed?person=Sarah&plan_date={invalid_date.isoformat()}")
assert response.status_code == 200
# Check for content that indicates empty plan
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert "No meals planned for this day." in response.text

View File

@@ -107,8 +107,8 @@ def test_edit_tracked_meal_with_override_flow(client: TestClient, session: Testi
# Prepare update data: update food1 quantity (should create a TrackedMealFood and delete original MealFood)
updated_foods_data = [
{"id": original_meal_food1.id, "food_id": food1.id, "quantity": 175.0, "is_custom": False}, # Original MealFood, but quantity changed
{"id": None, "food_id": food2.id, "quantity": 100.0, "is_custom": False} # Unchanged original MealFood
{"id": original_meal_food1.id, "food_id": food1.id, "grams": 175.0, "is_custom": False}, # Original MealFood, but quantity changed
{"id": None, "food_id": food2.id, "grams": 100.0, "is_custom": False} # Unchanged original MealFood
]
response_update = client.post(
@@ -169,8 +169,8 @@ def test_update_tracked_meal_foods_endpoint(client: TestClient, session: Testing
# Prepare update data
updated_foods = [
{"id": tracked_meal_food1.id, "food_id": food1.id, "quantity": 200.0, "is_custom": True},
{"id": None, "food_id": food2.id, "quantity": 50.0, "is_custom": False} # This represents original meal food
{"id": tracked_meal_food1.id, "food_id": food1.id, "grams": 200.0, "is_custom": True},
{"id": None, "food_id": food2.id, "grams": 50.0, "is_custom": False} # This represents original meal food
]
response = client.post(
@@ -210,7 +210,7 @@ def test_add_food_to_tracked_meal_endpoint(client: TestClient, session: TestingS
json={
"tracked_meal_id": tracked_meal.id,
"food_id": food3.id,
"quantity": 200
"grams": 200
}
)
assert response.status_code == 200

View File

@@ -1,8 +1,11 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from app.database import calculate_multiplier_from_grams
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db, Food, Meal, MealFood
from app.database import TrackedMealFood
from app.database import TrackedDay, TrackedMeal
from main import app
# Setup a test database
@@ -47,19 +50,19 @@ def sample_food_50g(session):
def test_convert_grams_to_quantity_100g_food(session, sample_food_100g):
"""Test convert_grams_to_quantity for a 100g serving size food"""
grams = 150.0
quantity = session.convert_grams_to_quantity(sample_food_100g.id, grams, session)
quantity = calculate_multiplier_from_grams(sample_food_100g.id, grams, session)
assert quantity == 1.5
def test_convert_grams_to_quantity_50g_food(session, sample_food_50g):
"""Test convert_grams_to_quantity for a 50g serving size food"""
grams = 125.0
quantity = session.convert_grams_to_quantity(sample_food_50g.id, grams, session)
quantity = calculate_multiplier_from_grams(sample_food_50g.id, grams, session)
assert quantity == 2.5
def test_convert_grams_to_quantity_invalid_food_id(session):
"""Test convert_grams_to_quantity with an invalid food ID"""
with pytest.raises(ValueError, match="Food with ID 999 not found."):
session.convert_grams_to_quantity(999, 100.0, session)
calculate_multiplier_from_grams(999, 100.0, session)
def test_convert_grams_to_quantity_zero_serving_size(session):
"""Test convert_grams_to_quantity with zero serving size"""
@@ -68,7 +71,7 @@ def test_convert_grams_to_quantity_zero_serving_size(session):
session.commit()
session.refresh(food)
with pytest.raises(ValueError, match="Serving size for food ID .* cannot be zero."):
session.convert_grams_to_quantity(food.id, 100.0, session)
calculate_multiplier_from_grams(food.id, 100.0, session)
def test_add_food_to_meal_grams_input(client, session, sample_food_100g):
"""Test adding food to a meal with grams input"""
@@ -79,14 +82,14 @@ def test_add_food_to_meal_grams_input(client, session, sample_food_100g):
response = client.post(
f"/meals/{meal.id}/add_food",
data={"food_id": sample_food_100g.id, "quantity": 250.0} # 250 grams
data={"food_id": sample_food_100g.id, "grams": 250.0} # 250 grams
)
assert response.status_code == 200
assert response.json()["status"] == "success"
meal_food = session.query(MealFood).filter(MealFood.meal_id == meal.id).first()
assert meal_food.food_id == sample_food_100g.id
assert meal_food.quantity == 2.5 # 250g / 100g serving = 2.5 multiplier
assert meal_food.quantity == 250.0
def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g):
"""Test updating meal food quantity with grams input"""
@@ -97,8 +100,7 @@ def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g)
# Add initial food with 100g (2.0 multiplier for 50g serving)
initial_grams = 100.0
initial_quantity = session.convert_grams_to_quantity(sample_food_50g.id, initial_grams, session)
meal_food = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=initial_quantity)
meal_food = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=initial_grams)
session.add(meal_food)
session.commit()
session.refresh(meal_food)
@@ -106,14 +108,13 @@ def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g)
updated_grams = 150.0
response = client.post(
"/meals/update_food_quantity",
data={"meal_food_id": meal_food.id, "quantity": updated_grams}
data={"meal_food_id": meal_food.id, "grams": updated_grams}
)
assert response.status_code == 200
assert response.json()["status"] == "success"
session.refresh(meal_food)
expected_quantity = session.convert_grams_to_quantity(sample_food_50g.id, updated_grams, session)
assert meal_food.quantity == expected_quantity
assert meal_food.quantity == updated_grams
# Test for bulk_upload_meals would require creating a mock UploadFile and CSV content
# This is more complex and might be deferred or tested manually if the tool's capabilities are limited.
@@ -131,7 +132,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
"person": person,
"date": date_str,
"food_id": sample_food_100g.id,
"quantity": grams, # 75 grams
"grams": grams, # 75 grams
"meal_time": "Breakfast"
}
)
@@ -142,7 +143,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
tracked_meal = session.query(Meal).filter(Meal.name == sample_food_100g.name).first()
assert tracked_meal is not None
meal_food = session.query(MealFood).filter(MealFood.meal_id == tracked_meal.id).first()
assert meal_food.quantity == 0.75 # 75g / 100g serving = 0.75 multiplier
assert meal_food.quantity == grams
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
"""Test updating tracked meal foods with grams input"""
@@ -150,7 +151,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
date_str = "2023-01-02"
# Create a tracked day and meal
tracked_day = TrackedDay(person=person, date="2023-01-02", is_modified=False)
from datetime import date
tracked_day = TrackedDay(person=person, date=date(2023, 1, 2), is_modified=False)
session.add(tracked_day)
session.commit()
session.refresh(tracked_day)
@@ -166,8 +168,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
session.refresh(tracked_meal)
# Add initial foods
meal_food_100g = MealFood(meal_id=meal.id, food_id=sample_food_100g.id, quantity=1.0) # 100g
meal_food_50g = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=2.0) # 100g
meal_food_100g = MealFood(meal_id=meal.id, food_id=sample_food_100g.id, quantity=100.0) # 100g
meal_food_50g = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=100.0) # 100g
session.add_all([meal_food_100g, meal_food_50g])
session.commit()
session.refresh(meal_food_100g)
@@ -175,8 +177,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
# Update quantities: 100g food to 200g, 50g food to 75g
updated_foods_data = [
{"id": meal_food_100g.id, "food_id": sample_food_100g.id, "quantity": 200.0, "is_custom": False},
{"id": meal_food_50g.id, "food_id": sample_food_50g.id, "quantity": 75.0, "is_custom": False}
{"id": meal_food_100g.id, "food_id": sample_food_100g.id, "grams": 200.0, "is_custom": False},
{"id": meal_food_50g.id, "food_id": sample_food_50g.id, "grams": 75.0, "is_custom": False}
]
response = client.post(
@@ -194,10 +196,10 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == sample_food_100g.id
).first()
assert tracked_food_100g.quantity == 2.0 # 200g / 100g serving = 2.0
assert tracked_food_100g.quantity == 200.0
tracked_food_50g = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == sample_food_50g.id
).first()
assert tracked_food_50g.quantity == 1.5 # 75g / 50g serving = 1.5
assert tracked_food_50g.quantity == 75.0

View File

@@ -92,7 +92,7 @@ class TestMealFoods:
"""Test POST /meals/{meal_id}/add_food"""
response = client.post(f"/meals/{sample_meal.id}/add_food", data={
"food_id": sample_food.id,
"quantity": 2.5
"grams": 250
})
assert response.status_code == 200
data = response.json()

View File

@@ -0,0 +1,196 @@
import pytest
from app.database import (
calculate_meal_nutrition,
Food,
Meal,
MealFood,
)
def test_meal_nutrition_uses_grams_correctly(db_session):
"""Verify that MealFood.quantity as grams calculates nutrition correctly"""
# Create a food: 100 cal per 100g
food = Food(
name="Test Food",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
carbs=20.0,
fat=5.0
)
db_session.add(food)
db_session.commit()
# Create a meal with 200g of this food
meal = Meal(name="Test Meal", meal_type="breakfast")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=200.0 # 200 grams
)
db_session.add(meal_food)
db_session.commit()
# Calculate nutrition
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 2x the base values (200g / 100g = 2x multiplier)
assert nutrition['calories'] == 200.0
assert nutrition['protein'] == 20.0
assert nutrition['carbs'] == 40.0
assert nutrition['fat'] == 10.0
def test_fractional_servings(db_session):
"""Test that fractional grams work correctly"""
food = Food(
name="Test Food",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal")
db_session.add(meal)
db_session.commit()
# Add 50g (half serving)
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=50.0
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 50.0
def test_zero_serving_size_handling(db_session):
"""Test handling of zero serving_size - should not divide by zero"""
food = Food(
name="Test Food Zero Serving",
serving_size=0.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Zero")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Multiplier should be 0, so no nutrition added
assert nutrition['calories'] == 0.0
def test_small_gram_values(db_session):
"""Test very small gram values (e.g., 0.1g)"""
food = Food(
name="Test Food Small",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Small")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=0.1 # Very small amount
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 0.001x multiplier
assert nutrition['calories'] == 0.1
def test_large_gram_values(db_session):
"""Test large gram values (e.g., 10000g)"""
food = Food(
name="Test Food Large",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Large")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=10000.0 # Very large amount
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 100x multiplier
assert nutrition['calories'] == 10000.0
def test_invalid_serving_size(db_session):
"""Test invalid/non-numeric serving_size values"""
# First create a valid food to test with
valid_food = Food(
name="Test Food Valid",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(valid_food)
db_session.commit()
# Now create a meal and add the valid food
meal = Meal(name="Test Meal Valid")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=valid_food.id,
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()
# Test that the calculation works with valid serving_size
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 100.0
# Now test with None serving_size by updating the food
valid_food.serving_size = None
db_session.commit()
# Recalculate - should handle None gracefully
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 0

View File

@@ -280,7 +280,7 @@ class TestTrackerEdit:
# Update the food quantity via API
response = client.post("/tracker/update_tracked_food", json={
"tracked_food_id": tracked_food.id,
"quantity": 3.0,
"grams": 300.0,
"is_custom": True
})
assert response.status_code == 200
@@ -290,7 +290,7 @@ class TestTrackerEdit:
# Verify the update
db_session.commit()
updated_food = db_session.query(TrackedMealFood).get(tracked_food.id)
assert updated_food.quantity == 3.0
assert updated_food.quantity == 300.0
assert updated_food.quantity != original_quantity
@@ -332,7 +332,7 @@ class TestTrackerSaveAsNewMeal:
"tracked_meal_id": tracked_meal.id,
"new_meal_name": new_meal_name,
"foods": [
{"food_id": sample_food.id, "quantity": 3.0}
{"food_id": sample_food.id, "grams": 300.0}
]
})
assert response.status_code == 200
@@ -404,7 +404,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": sample_food.id,
"quantity": 150.0,
"grams": 150.0,
"meal_time": "Dinner"
})
assert response.status_code == 200
@@ -422,7 +422,7 @@ class TestTrackerAddFood:
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
assert tracked_meal.meal.meal_foods[0].quantity == 1.5
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
"""
@@ -449,7 +449,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"quantity": grams_to_add,
"grams": grams_to_add,
"meal_time": "Snack 1"
})
assert response.status_code == 200
@@ -469,7 +469,7 @@ class TestTrackerAddFood:
# Verify the food is in the tracked meal's foods and quantity is in servings
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == food.id
assert tracked_meal.meal.meal_foods[0].quantity == expected_servings
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
# Verify nutrition calculation
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)