Download test_fire_math.py from smlflg/FireTracker: direct link, hf CLI and curl.
- Browser
- Download file 8.34 kB
-
https://huggingface.co/smlflg/FireTracker/resolve/main/test_fire_math.py
- Command line
-
hf download hf://smlflg/FireTracker/test_fire_math.py
-
curl -L -o test_fire_math.py https://huggingface.co/smlflg/FireTracker/resolve/main/test_fire_math.py
8.34 kB
| """Tests for fire_math.py β core FIRE calculation logic.""" | |
| import pytest | |
| from fire_math import calculate_fire, calculate_years_to_fi, calculate_scenario, projection_timeline | |
| # --------------------------------------------------------------------------- | |
| # calculate_fire β Core Metrics | |
| # --------------------------------------------------------------------------- | |
| def test_basic_case(): | |
| result = calculate_fire( | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1998, | |
| ) | |
| assert result.fi_number == 600000.0 | |
| assert result.savings_rate == 50.0 | |
| assert result.monthly_savings == 2000.0 | |
| def test_already_fi(): | |
| # Portfolio exceeds FI number β years_to_fi must be 0.0, NOT None. | |
| # This tests the line-68 bug fix: `if years_to_fi is not None` instead of `if years_to_fi`. | |
| result = calculate_fire( | |
| monthly_income=2000, | |
| monthly_expenses=2000, | |
| total_portfolio=700000, | |
| birth_year=1998, | |
| ) | |
| assert result.years_to_fi == 0.0, ( | |
| "years_to_fi must be 0.0 when already FI, not None. " | |
| "Check the line-68 bug fix." | |
| ) | |
| assert result.fi_date is not None | |
| def test_zero_income(): | |
| result = calculate_fire( | |
| monthly_income=0, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1998, | |
| ) | |
| assert result.savings_rate == 0 | |
| def test_never_fi(): | |
| # Zero savings + zero portfolio β compound growth on 0 stays 0 β never reaches target. | |
| result = calculate_fire( | |
| monthly_income=2000, | |
| monthly_expenses=2000, | |
| total_portfolio=0, | |
| birth_year=1998, | |
| ) | |
| assert result.monthly_savings == 0.0 | |
| assert result.years_to_fi is None | |
| assert result.fi_date is None | |
| # --------------------------------------------------------------------------- | |
| # calculate_years_to_fi β Iteration Logic | |
| # --------------------------------------------------------------------------- | |
| def test_already_at_target(): | |
| result = calculate_years_to_fi( | |
| current_portfolio=600000, | |
| annual_savings=24000, | |
| target=600000, | |
| annual_return=0.05, | |
| ) | |
| assert result == 0.0 | |
| def test_normal_projection(): | |
| result = calculate_years_to_fi( | |
| current_portfolio=50000, | |
| annual_savings=24000, | |
| target=600000, | |
| annual_return=0.05, | |
| ) | |
| assert result is not None | |
| assert 13 <= result <= 20, f"Expected 13β20 years, got {result}" | |
| def test_no_savings_no_return(): | |
| result = calculate_years_to_fi( | |
| current_portfolio=50000, | |
| annual_savings=0, | |
| target=600000, | |
| annual_return=0, | |
| ) | |
| assert result is None | |
| def test_max_years_cap(): | |
| # Savings so tiny the loop hits the 100-year cap. | |
| result = calculate_years_to_fi( | |
| current_portfolio=0, | |
| annual_savings=12, # β¬1/month | |
| target=10_000_000, | |
| annual_return=0, | |
| ) | |
| assert result is None | |
| # --------------------------------------------------------------------------- | |
| # projection_timeline β Chart Data | |
| # --------------------------------------------------------------------------- | |
| def test_timeline_length(): | |
| timeline = projection_timeline( | |
| total_portfolio=50000, | |
| monthly_savings=2000, | |
| annual_return=0.05, | |
| years=30, | |
| ) | |
| assert len(timeline) == 31 # year 0 through year 30 inclusive | |
| def test_first_entry(): | |
| starting = 75000.0 | |
| timeline = projection_timeline( | |
| total_portfolio=starting, | |
| monthly_savings=1500, | |
| annual_return=0.05, | |
| years=20, | |
| ) | |
| assert timeline[0]["portfolio"] == starting | |
| def test_growth(): | |
| timeline = projection_timeline( | |
| total_portfolio=10000, | |
| monthly_savings=500, | |
| annual_return=0.05, | |
| years=10, | |
| ) | |
| for i in range(1, len(timeline)): | |
| assert timeline[i]["portfolio"] > timeline[i - 1]["portfolio"] | |
| # --------------------------------------------------------------------------- | |
| # calculate_scenario β What-If | |
| # --------------------------------------------------------------------------- | |
| def test_extra_savings(): | |
| base = calculate_fire( | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1990, | |
| ) | |
| scenario = calculate_scenario( | |
| base=base, | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1990, | |
| extra_monthly_savings=500, | |
| ) | |
| assert scenario.years_to_fi is not None | |
| assert base.years_to_fi is not None | |
| assert scenario.years_to_fi < base.years_to_fi | |
| def test_lower_expenses(): | |
| base = calculate_fire( | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1990, | |
| ) | |
| scenario = calculate_scenario( | |
| base=base, | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1990, | |
| adjusted_expenses=1500, | |
| ) | |
| assert scenario.fi_number < base.fi_number | |
| # --------------------------------------------------------------------------- | |
| # fi_date β Edge Cases for Month/Year Boundary Arithmetic | |
| # --------------------------------------------------------------------------- | |
| def test_fi_date_zero_years_to_fi(): | |
| """When already FI (years_to_fi=0.0), fi_date must be first of current month.""" | |
| from datetime import date | |
| result = calculate_fire( | |
| monthly_income=2000, | |
| monthly_expenses=2000, | |
| total_portfolio=700000, | |
| birth_year=1998, | |
| ) | |
| assert result.fi_date is not None | |
| assert result.fi_date.day == 1 | |
| assert result.fi_date.year == date.today().year | |
| assert result.fi_date.month == date.today().month | |
| def test_fi_date_whole_years(): | |
| """Fractional part = 0.0 β fi_date must land in the current month of a future year.""" | |
| from datetime import date | |
| # Force years_to_fi to be a whole number by giving a result we can verify. | |
| # We use calculate_years_to_fi directly to avoid dependency on date.today() internals. | |
| from fire_math import calculate_years_to_fi | |
| result = calculate_years_to_fi( | |
| current_portfolio=0, | |
| annual_savings=60000, | |
| target=600000, | |
| annual_return=0.0, # No compounding β pure savings: exactly 10 years | |
| ) | |
| assert result is not None | |
| # 10 years of β¬60k/year = β¬600k exactly β should be ~10 years | |
| assert 9.9 <= result <= 10.1 | |
| def test_fi_date_fractional_month_no_overflow(): | |
| """Adding fractional months must not produce month > 12 for any start month.""" | |
| from datetime import date | |
| from unittest.mock import patch | |
| for start_month in range(1, 13): | |
| fake_today = date(2026, start_month, 15) | |
| with patch("fire_math.date") as mock_date: | |
| mock_date.today.return_value = fake_today | |
| mock_date.side_effect = lambda *args, **kw: date(*args, **kw) | |
| result = calculate_fire( | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1998, | |
| ) | |
| if result.fi_date is not None: | |
| assert 1 <= result.fi_date.month <= 12, ( | |
| f"fi_date.month={result.fi_date.month} out of range for start_month={start_month}" | |
| ) | |
| assert result.fi_date.year >= 2026 | |
| def test_fi_date_december_wrap(): | |
| """December + 9 fractional months = September of next year. Must not add extra year.""" | |
| from datetime import date | |
| from unittest.mock import patch | |
| # Simulate running in December with 15.75 years to FI | |
| # Dec (12) + 9 fractional months = 21 β extra_years=1, final_month=9 | |
| # Expected: year=2026+15+1=2042, month=9 | |
| fake_today = date(2026, 12, 1) | |
| with patch("fire_math.date") as mock_date: | |
| mock_date.today.return_value = fake_today | |
| mock_date.side_effect = lambda *args, **kw: date(*args, **kw) | |
| result = calculate_fire( | |
| monthly_income=4000, | |
| monthly_expenses=2000, | |
| total_portfolio=50000, | |
| birth_year=1998, | |
| ) | |
| if result.fi_date is not None: | |
| # fi_date must be a valid date | |
| assert 1 <= result.fi_date.month <= 12 | |
| assert result.fi_date.year > 2026 | |