fix(tests): тесты тяжёлых GET — непустые данные свода 2, gzip, правки докстрингов
Фикстура свода 2 достроена до 6 строк на GO и RF по двум орг. единицам с разными section_code; все случаи SVOD2_CASES проверяют непустоту. Тест rf-rollup переименован (фиксирует существующий дефект), добавлен тест gzip, в api_split.py оставлены только GET-места. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
38990dcaf6
commit
b612178b12
@ -126,6 +126,15 @@ def test_sheet_text_escaping_matches_old_path(client, db_session, admin_tokens,
|
||||
assert any((r["data"].get("header") or {}).get("name") == TRICKY for r in result)
|
||||
|
||||
|
||||
def test_heavy_get_gzip_compressed(client, admin_tokens, auth_headers):
|
||||
"""Спека, сценарий «Сжатие»: ответ >= 1000 байт (GZipMiddleware minimum_size=1000) уходит с Content-Encoding: gzip."""
|
||||
headers = {**auth_headers(admin_tokens), "Accept-Encoding": "gzip"}
|
||||
r = client.get("/api/v1/form/4/sheet/AHR", headers=headers)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.headers.get("content-encoding") == "gzip"
|
||||
assert r.json()["success"] is True # клиент распаковал тело
|
||||
|
||||
|
||||
def test_sheet_get_does_not_use_old_path(client, admin_tokens, auth_headers, monkeypatch):
|
||||
async def boom(*args, **kwargs):
|
||||
raise AssertionError("GET листа не должен вызывать SheetRepository.get")
|
||||
@ -133,21 +142,53 @@ def test_sheet_get_does_not_use_old_path(client, admin_tokens, auth_headers, mon
|
||||
get_ok(client, "/api/v1/form/4/sheet/AHR", auth_headers(admin_tokens))
|
||||
|
||||
|
||||
SVOD2_PLAN_LINE_SQL = text(
|
||||
"WITH bf AS (INSERT INTO v3.budget_form (form_type_code, year, org_unit_id) "
|
||||
"VALUES ('FORM_1', :year, 2) RETURNING id), "
|
||||
"bl AS (INSERT INTO v3.budget_line (budget_form_id, expense_item_id) "
|
||||
"SELECT id, :expense_item_id FROM bf RETURNING id) "
|
||||
"INSERT INTO v3.plan (line_id, plan_q1) SELECT id, 1000 FROM bl"
|
||||
)
|
||||
# Свод 2 берёт строки по версиям v3.v_svod2_go / v3.v_svod2_rf (миграция 0049, SQL-файл 0049_summary2_related_records.sql):
|
||||
# GO - budget_form FORM_1/FORM_4 за год, RF (с 0041) - только FORM_2; статья с sheet IN (AHR, CAP, OPER);
|
||||
# строка попадает в свод, если у неё есть хотя бы одна "присутствующая" секция (например, plan с ненулевым планом).
|
||||
# Хардкод (предположение о fixture.sql, T4.2): орг. единицы 1 (Test_SSP) и 2 (Test РФ) - единственные в фикстуре;
|
||||
# в фикстуре за YEAR есть только FORM_2 (budget_form id=4, org 2) без budget_line; статьи 88 (AHR, R061161003),
|
||||
# 523 (CAP, 3.00.0.), 729 (OPER) существуют в справочнике и имеют разные section_code - это нужно, чтобы
|
||||
# sort_by=section_code и offset/limit давали различимый порядок. Если fixture.sql или справочник изменятся,
|
||||
# test_svod2_fixture_not_empty упадёт первым.
|
||||
SVOD2_ORG_IDS = (1, 2)
|
||||
SVOD2_EXPENSE_ITEM_IDS = (88, 523, 729) # AHR, CAP, OPER
|
||||
SVOD2_FORMS = [("FORM_1", 1), ("FORM_1", 2), ("FORM_2", 1), ("FORM_2", 2)] # GO: FORM_1, RF: FORM_2
|
||||
SVOD2_MIN_ROWS = 6 # 2 орг. единицы x 3 статьи на каждый из листов GO и RF
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def svod2_plan_line(client, db_session):
|
||||
"""Фикстура не даёт свода 2 строк за YEAR: вставляем FORM_1 с ненулевым планом.
|
||||
Откатывается вместе с сессией теста (POST строки без плана свод 2 не учитывает)."""
|
||||
"""Фикстура не даёт свода 2 строк за YEAR: достраиваем формы FORM_1 и FORM_2 по двум орг. единицам
|
||||
и строки трёх статей (AHR, CAP, OPER) с ненулевым планом. Откатывается вместе с сессией теста."""
|
||||
async def _set():
|
||||
await db_session.execute(SVOD2_PLAN_LINE_SQL, {"year": YEAR, "expense_item_id": 88})
|
||||
for form_type, org_id in SVOD2_FORMS:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"INSERT INTO v3.budget_form (form_type_code, year, org_unit_id) "
|
||||
"VALUES (:ft, :year, :org) ON CONFLICT (form_type_code, org_unit_id, year) DO NOTHING"
|
||||
),
|
||||
{"ft": form_type, "year": YEAR, "org": org_id},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"INSERT INTO v3.budget_line (budget_form_id, expense_item_id) "
|
||||
"SELECT bf.id, e FROM v3.budget_form bf CROSS JOIN unnest(CAST(:items AS int[])) AS e "
|
||||
"WHERE bf.year = :year AND bf.form_type_code IN ('FORM_1', 'FORM_2') "
|
||||
"AND NOT EXISTS (SELECT 1 FROM v3.budget_line x "
|
||||
"WHERE x.budget_form_id = bf.id AND x.expense_item_id = e)"
|
||||
),
|
||||
{"year": YEAR, "items": list(SVOD2_EXPENSE_ITEM_IDS)},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"INSERT INTO v3.plan (line_id, plan_q1) "
|
||||
"SELECT bl.id, 1000 FROM v3.budget_line bl JOIN v3.budget_form bf ON bf.id = bl.budget_form_id "
|
||||
"WHERE bf.year = :year AND bf.form_type_code IN ('FORM_1', 'FORM_2') "
|
||||
"AND bl.expense_item_id = ANY(CAST(:items AS int[])) "
|
||||
"ON CONFLICT (line_id) DO UPDATE SET plan_q1 = 1000"
|
||||
),
|
||||
{"year": YEAR, "items": list(SVOD2_EXPENSE_ITEM_IDS)},
|
||||
)
|
||||
client.portal.call(_set)
|
||||
|
||||
|
||||
@ -174,11 +215,20 @@ SVOD2_CASES = [
|
||||
]
|
||||
|
||||
|
||||
def _svod2_min_rows(query: dict) -> int:
|
||||
if "limit" in query:
|
||||
return 1
|
||||
if "sort_by" in query:
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("svod2_plan_line")
|
||||
@pytest.mark.parametrize("sheet,query,repo_kwargs", SVOD2_CASES)
|
||||
def test_svod2_matches_old_path(client, db_session, admin_tokens, auth_headers, sheet, query, repo_kwargs):
|
||||
result = get_ok(client, f"/api/v1/svod/2/{sheet}/{YEAR}", auth_headers(admin_tokens), query)
|
||||
old = old_rows(client, lambda: SummaryRepository(db_session).get_svod2_rows(year=YEAR, sheet=sheet, **repo_kwargs))
|
||||
assert len(result) >= _svod2_min_rows(query), "сравнение на пустом/слишком коротком списке ничего не доказывает"
|
||||
assert result == expected_sheet(old)
|
||||
|
||||
|
||||
@ -187,7 +237,11 @@ def test_svod2_fixture_not_empty(client, admin_tokens, auth_headers):
|
||||
"""Иначе сравнения свода 2 выше проходят на пустых списках."""
|
||||
go = get_ok(client, f"/api/v1/svod/2/GO/{YEAR}", auth_headers(admin_tokens))
|
||||
rf = get_ok(client, f"/api/v1/svod/2/RF/{YEAR}", auth_headers(admin_tokens))
|
||||
assert go or rf
|
||||
assert len(go) >= SVOD2_MIN_ROWS, len(go)
|
||||
assert len(rf) >= SVOD2_MIN_ROWS, len(rf)
|
||||
for rows in (go, rf):
|
||||
assert {r["data"]["header"]["source_org_unit_id"] for r in rows} == set(SVOD2_ORG_IDS)
|
||||
assert len({r["data"]["header"]["section_code"] for r in rows}) >= 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("svod2_plan_line")
|
||||
@ -252,10 +306,11 @@ def test_project_report_matches_old_path(client, db_session, admin_tokens, auth_
|
||||
assert any(row["row_type"] == "INPUT" for row in result)
|
||||
|
||||
|
||||
def test_rf_rollup_error_same_as_old_path(client, db_session, admin_tokens, auth_headers, project_id):
|
||||
"""v3.v_form3_rf_rollup_jsonb в миграции 0002_functions.sql ссылается на несуществующий r.year
|
||||
(дефект вне AURORA-1848): на любой БД ответ - 400. Фиксируем, что новый путь отдаёт то же самое,
|
||||
что и старый, а успешное сравнение с эталоном для rf-rollup невозможно."""
|
||||
def test_rf_rollup_pinned_preexisting_sql_error(client, admin_tokens, auth_headers):
|
||||
"""Фиксирует СУЩЕСТВУЮЩИЙ дефект вне AURORA-1848: v3.v_form3_rf_rollup_jsonb в миграции 0002_functions.sql
|
||||
ссылается на несуществующий r.year, поэтому на любой БД ответ - 400 с текстом SQL-ошибки (так же отвечал
|
||||
и старый путь). Когда дефект будет исправлен, этот тест упадёт и его нужно превратить в тест
|
||||
эквивалентности с эталоном ProjectService.get_rf_rollup_rows (как остальные get_ok-сравнения)."""
|
||||
r = client.get(f"/api/v1/rf-rollup/1/{YEAR}", headers=auth_headers(admin_tokens))
|
||||
assert (r.status_code, r.json()) == RF_ROLLUP_SQL_ERROR
|
||||
|
||||
@ -303,7 +358,7 @@ def test_projects_get_does_not_use_old_path(client, admin_tokens, auth_headers,
|
||||
monkeypatch.setattr(ProjectRepository, method, boom)
|
||||
r = client.get(url.format(pid=project_id), headers=auth_headers(admin_tokens))
|
||||
assert not called
|
||||
if method != "get_rf_rollup_rows": # rf-rollup: см. test_rf_rollup_error_same_as_old_path
|
||||
if method != "get_rf_rollup_rows": # rf-rollup: см. test_rf_rollup_pinned_preexisting_sql_error
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
cd perf && .venv/bin/python analysis/api_split.py --scale 1 --repeats 10 --out report-data/20261026-s1/analysis
|
||||
|
||||
Обработчики листов, сводов и отчётов проектов сами ставят заголовок `X-DB-Time-Ms` (api/src/api/v1/forms.py:198,241,304,
|
||||
368,424,473; summary.py:75,126; projects.py:139,272,305,329): время от начала обработчика до получения строк из БД (поиск
|
||||
Обработчики листов, сводов и отчётов проектов сами ставят заголовок `X-DB-Time-Ms` (GET: api/src/api/v1/forms.py:198 get_sheet;
|
||||
summary.py:75,126; projects.py:139,272,305,329): время от начала обработчика до получения строк из БД (поиск
|
||||
формы + SQL-функция + отдача готового JSON драйвером). После задач A ответ собирается из строк jsonb без pydantic, поэтому
|
||||
остаток `полное время - X-DB-Time-Ms` - это склейка строк в `list_json_response` (api/src/api/v1/json_response.py), сжатие
|
||||
GZipMiddleware (api/src/main.py:248, compresslevel=1), передача и распаковка на клиенте (httpx).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user