Skip to content

Commit a61d113

Browse files
authored
Merge pull request #3556 from PolicyEngine/codex/stabilize-live-integration-tests
Stabilize live integration tests
2 parents 1dacf63 + 5f71127 commit a61d113

7 files changed

Lines changed: 131 additions & 107 deletions

File tree

changelog.d/3555.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stabilized deployed integration tests for slow simulation polling and staging cache reuse.

tests/data/calculate_us_1_data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"period": "2023",
1515
"min": 0,
1616
"max": 200000,
17-
"count": 401
17+
"count": 101
1818
}
1919
]
2020
]
@@ -24,4 +24,4 @@
2424
"2023-01-01.2028-12-31": "101"
2525
}
2626
}
27-
}
27+
}

tests/data/calculate_us_2_data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"period": "2023",
1515
"min": 0,
1616
"max": 200000,
17-
"count": 401
17+
"count": 101
1818
}
1919
]
2020
]
@@ -24,4 +24,4 @@
2424
"2023-01-01.2028-12-31": "100"
2525
}
2626
}
27-
}
27+
}

tests/integration/conftest.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import os
2+
import time
3+
import uuid
24

35
import httpx
46
import pytest
57

8+
INTEGRATION_TIMEOUT_SECONDS = float(
9+
os.environ.get("STAGING_API_TEST_TIMEOUT_SECONDS", "900")
10+
)
11+
INTEGRATION_POLL_INTERVAL_SECONDS = float(
12+
os.environ.get("STAGING_API_TEST_POLL_INTERVAL_SECONDS", "5")
13+
)
14+
TRANSIENT_POLL_STATUS_CODES = {500, 502, 503, 504}
15+
616

717
@pytest.fixture
818
def api_base_url() -> str:
@@ -22,6 +32,51 @@ def api_client(api_base_url: str):
2232
yield client
2333

2434

35+
def _response_summary(response: httpx.Response) -> str:
36+
return f"HTTP {response.status_code}: {response.text[:500]}"
37+
38+
39+
def _poll_live_endpoint(
40+
api_client: httpx.Client,
41+
path: str,
42+
params: dict,
43+
*,
44+
route_name: str,
45+
) -> dict:
46+
deadline = time.monotonic() + INTEGRATION_TIMEOUT_SECONDS
47+
last_response = None
48+
49+
while True:
50+
try:
51+
response = api_client.get(path, params=params)
52+
except httpx.RequestError as error:
53+
last_response = f"{type(error).__name__}: {error}"
54+
else:
55+
if response.status_code in TRANSIENT_POLL_STATUS_CODES:
56+
last_response = _response_summary(response)
57+
else:
58+
response.raise_for_status()
59+
payload = response.json()
60+
61+
if payload["status"] != "computing":
62+
return payload
63+
64+
last_response = payload
65+
66+
if time.monotonic() >= deadline:
67+
raise AssertionError(
68+
f"Timed out polling the {route_name} route; "
69+
f"last response was {last_response}"
70+
)
71+
time.sleep(INTEGRATION_POLL_INTERVAL_SECONDS)
72+
73+
2574
@pytest.fixture
75+
def poll_live_endpoint():
76+
return _poll_live_endpoint
77+
78+
79+
@pytest.fixture(scope="session")
2680
def integration_probe_id() -> str:
27-
return os.environ.get("STAGING_API_TEST_PROBE_ID", "local-probe")
81+
base_probe_id = os.environ.get("STAGING_API_TEST_PROBE_ID", "local-probe")
82+
return f"{base_probe_id}-{uuid.uuid4().hex[:8]}"

tests/integration/test_budget_window_in_flight_dedupe.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import os
21
from unittest.mock import MagicMock
32

43
from flask import Flask
54

6-
os.environ.setdefault("POLICYENGINE_DB_PASSWORD", "test")
7-
85

96
class FakeRedis:
107
def __init__(self):
@@ -32,6 +29,9 @@ def _create_client(economy_bp):
3229
def test_budget_window_in_flight_dedupe_uses_existing_batch_without_live_db(
3330
monkeypatch,
3431
):
32+
monkeypatch.setenv("POLICYENGINE_DB_PASSWORD", "test")
33+
monkeypatch.setenv("FLASK_DEBUG", "1")
34+
3535
from policyengine_api.libs.simulation_api_modal import (
3636
ModalBudgetWindowBatchExecution,
3737
)
Lines changed: 33 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import json
2-
import os
3-
import time
42
from pathlib import Path
53

64

7-
INTEGRATION_TIMEOUT_SECONDS = float(
8-
os.environ.get("STAGING_API_TEST_TIMEOUT_SECONDS", "900")
9-
)
10-
INTEGRATION_POLL_INTERVAL_SECONDS = float(
11-
os.environ.get("STAGING_API_TEST_POLL_INTERVAL_SECONDS", "5")
12-
)
13-
14-
155
def _load_reform_payload(filename: str) -> dict:
166
return json.loads(
177
(Path(__file__).resolve().parents[1] / "data" / filename).read_text(
@@ -20,23 +10,6 @@ def _load_reform_payload(filename: str) -> dict:
2010
)
2111

2212

23-
def _poll_budget_window(api_client, path: str, params: dict) -> dict:
24-
deadline = time.monotonic() + INTEGRATION_TIMEOUT_SECONDS
25-
26-
while True:
27-
response = api_client.get(path, params=params)
28-
response.raise_for_status()
29-
payload = response.json()
30-
31-
if payload["status"] != "computing":
32-
return payload
33-
34-
assert time.monotonic() < deadline, (
35-
f"Timed out polling the budget-window route; last response was {payload}"
36-
)
37-
time.sleep(INTEGRATION_POLL_INTERVAL_SECONDS)
38-
39-
4013
def _get_current_law_id(api_client) -> int:
4114
metadata_response = api_client.get("/us/metadata")
4215
metadata_response.raise_for_status()
@@ -52,7 +25,11 @@ def _create_utah_reform_policy(api_client) -> int:
5225
return policy_response.json()["result"]["policy_id"]
5326

5427

55-
def test_live_budget_window_completed_result_cache(api_client, integration_probe_id):
28+
def test_live_budget_window_completed_result_cache(
29+
api_client,
30+
integration_probe_id,
31+
poll_live_endpoint,
32+
):
5633
current_law_id = _get_current_law_id(api_client)
5734
policy_id = _create_utah_reform_policy(api_client)
5835

@@ -64,7 +41,12 @@ def test_live_budget_window_completed_result_cache(api_client, integration_probe
6441
"staging_probe": f"{integration_probe_id}-budget-window-cache",
6542
}
6643

67-
first_payload = _poll_budget_window(api_client, path, params)
44+
first_payload = poll_live_endpoint(
45+
api_client,
46+
path,
47+
params,
48+
route_name="budget-window",
49+
)
6850

6951
assert first_payload["status"] == "ok", first_payload
7052
assert first_payload["progress"] == 100, first_payload
@@ -82,7 +64,11 @@ def test_live_budget_window_completed_result_cache(api_client, integration_probe
8264
assert second_response.headers["X-PolicyEngine-Budget-Window-Cache"] == "result-hit"
8365

8466

85-
def test_live_budget_window_multi_year_run(api_client, integration_probe_id):
67+
def test_live_budget_window_multi_year_run(
68+
api_client,
69+
integration_probe_id,
70+
poll_live_endpoint,
71+
):
8672
current_law_id = _get_current_law_id(api_client)
8773
policy_id = _create_utah_reform_policy(api_client)
8874

@@ -94,7 +80,12 @@ def test_live_budget_window_multi_year_run(api_client, integration_probe_id):
9480
"staging_probe": f"{integration_probe_id}-budget-window-multi-year",
9581
}
9682

97-
payload = _poll_budget_window(api_client, path, params)
83+
payload = poll_live_endpoint(
84+
api_client,
85+
path,
86+
params,
87+
route_name="budget-window",
88+
)
9889

9990
assert payload["status"] == "ok", payload
10091
assert payload["progress"] == 100, payload
@@ -111,7 +102,11 @@ def test_live_budget_window_multi_year_run(api_client, integration_probe_id):
111102
assert result["totals"]["year"] == "Total", payload
112103

113104

114-
def test_live_budget_window_failed_batch_mapping(api_client, integration_probe_id):
105+
def test_live_budget_window_failed_batch_mapping(
106+
api_client,
107+
integration_probe_id,
108+
poll_live_endpoint,
109+
):
115110
current_law_id = _get_current_law_id(api_client)
116111
policy_id = _create_utah_reform_policy(api_client)
117112

@@ -124,40 +119,16 @@ def test_live_budget_window_failed_batch_mapping(api_client, integration_probe_i
124119
"staging_probe": f"{integration_probe_id}-budget-window-failure",
125120
}
126121

127-
payload = _poll_budget_window(api_client, path, params)
122+
payload = poll_live_endpoint(
123+
api_client,
124+
path,
125+
params,
126+
route_name="budget-window",
127+
)
128128

129129
assert payload["status"] == "error", payload
130130
assert payload["result"] is None, payload
131131
assert payload["error"], payload
132132
assert isinstance(payload["completed_years"], list), payload
133133
assert isinstance(payload["computing_years"], list), payload
134134
assert isinstance(payload["queued_years"], list), payload
135-
136-
137-
def test_live_budget_window_in_flight_dedupe(api_client, integration_probe_id):
138-
current_law_id = _get_current_law_id(api_client)
139-
policy_id = _create_utah_reform_policy(api_client)
140-
141-
path = f"/us/economy/{policy_id}/over/{current_law_id}/budget-window"
142-
params = {
143-
"region": "ut",
144-
"start_year": "2026",
145-
"window_size": 2,
146-
"staging_probe": f"{integration_probe_id}-budget-window-in-flight",
147-
}
148-
149-
first_response = api_client.get(path, params=params)
150-
first_response.raise_for_status()
151-
first_payload = first_response.json()
152-
153-
assert first_payload["status"] == "computing", first_payload
154-
assert first_response.headers["X-PolicyEngine-Budget-Window-Cache"] == "miss"
155-
156-
second_response = api_client.get(path, params=params)
157-
second_response.raise_for_status()
158-
second_payload = second_response.json()
159-
160-
assert second_response.headers["X-PolicyEngine-Budget-Window-Cache"] == (
161-
"batch-id-hit"
162-
)
163-
assert second_payload["status"] in ("computing", "ok"), second_payload

0 commit comments

Comments
 (0)