Merge pull request 'fix(tests): согласовать AURORA-1844/1846/1850 с null-strip из AURORA-1848' (#279) from fix/null-strip-test-contract into test
This commit is contained in:
commit
f407b5c296
@ -3,6 +3,10 @@
|
||||
Каждая строка ответа PATCH /cell(s) должна совпасть (data и sort_order) со строкой
|
||||
того же (row_type, depth, sort_order) из последующего GET /sheet.
|
||||
Используется в AURORA-1844 и далее (AURORA-1850: пересчёт только затронутого поддерева).
|
||||
|
||||
AURORA-1848 убрал null-поля из data только в GET (jsonb_strip_nulls), PATCH отдаёт
|
||||
null явно — осознанно, без изменений. Сравнение ниже снимает это различие на стороне
|
||||
теста: strip_nulls(PATCH) должен совпасть с GET, который сервер уже отдал без null.
|
||||
"""
|
||||
|
||||
|
||||
@ -10,6 +14,15 @@ def row_key(row: dict) -> tuple:
|
||||
return (row["row_type"], row["depth"], row["sort_order"])
|
||||
|
||||
|
||||
def strip_nulls(value):
|
||||
"""Python-эквивалент jsonb_strip_nulls: убирает поля объектов со значением null, рекурсивно."""
|
||||
if isinstance(value, dict):
|
||||
return {k: strip_nulls(v) for k, v in value.items() if v is not None}
|
||||
if isinstance(value, list):
|
||||
return [strip_nulls(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def assert_patch_rows_match_sheet(patch_rows: list[dict], sheet_rows: list[dict]) -> None:
|
||||
"""Строки ответа на правку целиком совпадают со строками листа."""
|
||||
sheet_by_key = {row_key(r): r for r in sheet_rows}
|
||||
@ -19,7 +32,7 @@ def assert_patch_rows_match_sheet(patch_rows: list[dict], sheet_rows: list[dict]
|
||||
for row in patch_rows:
|
||||
key = row_key(row)
|
||||
assert key in sheet_by_key, f"строки {key} нет в GET /sheet"
|
||||
assert row["data"] == sheet_by_key[key]["data"], f"data строки {key} расходится с GET"
|
||||
assert strip_nulls(row["data"]) == sheet_by_key[key]["data"], f"data строки {key} расходится с GET"
|
||||
|
||||
|
||||
# --- AURORA-1850: эталон (фильтр 0050 поверх полного v_form_view) и хелперы SQL ---
|
||||
|
||||
@ -12,6 +12,8 @@ from itertools import count
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
from sheet_contract import strip_nulls
|
||||
|
||||
SHEET = "CAP"
|
||||
CAP_ITEM = 525 # статья CAP depth=2
|
||||
BLOCKS = [
|
||||
@ -172,14 +174,17 @@ def check_sums(f4, sections=None):
|
||||
root, groups, items, inputs = _split(rows)
|
||||
ref = f4.reference(sections)
|
||||
|
||||
# Эталон 1a: ответ API совпадает с v3.jsonb_sum (числа сравниваются по значению)
|
||||
assert canon_num({b: root[b] for b in BLOCKS}) == canon_num(ref[("R", 0)])
|
||||
# Эталон 1a: ответ API (без null-полей, AURORA-1848) совпадает с v3.jsonb_sum
|
||||
# после strip_nulls (числа сравниваются по значению). Блок, целиком null
|
||||
# (не выбран в sections или нет значений), в ответе API отсутствует как ключ —
|
||||
# проецируем обе стороны только на реально присутствующие блоки.
|
||||
assert canon_num({b: root[b] for b in BLOCKS if b in root}) == canon_num(strip_nulls(ref[("R", 0)]))
|
||||
for pid, data in groups.items():
|
||||
if ("P", pid) in ref:
|
||||
assert canon_num({b: data[b] for b in BLOCKS}) == canon_num(ref[("P", pid)]), f"program {pid}"
|
||||
assert canon_num({b: data[b] for b in BLOCKS if b in data}) == canon_num(strip_nulls(ref[("P", pid)])), f"program {pid}"
|
||||
for jid, data in items.items():
|
||||
if ("J", jid) in ref:
|
||||
assert canon_num({b: data[b] for b in BLOCKS}) == canon_num(ref[("J", jid)]), f"project {jid}"
|
||||
assert canon_num({b: data[b] for b in BLOCKS if b in data}) == canon_num(strip_nulls(ref[("J", jid)])), f"project {jid}"
|
||||
|
||||
# Эталон 1b: прямой вызов функции совпадает с v3.jsonb_sum точно, включая масштаб чисел
|
||||
direct = f4.sql(
|
||||
@ -196,14 +201,16 @@ def check_sums(f4, sections=None):
|
||||
assert canon({b: data[b] for b in BLOCKS}) == canon(ref[("J", jid)]), f"project {jid}"
|
||||
|
||||
# Эталон 2: сумма числовых листьев на Python
|
||||
# AURORA-1848 вырезает null-поля из GET — целиком null-блок строки отсутствует,
|
||||
# отсутствующий header.program_id/project_id (строка не на этом уровне) тоже.
|
||||
def expect(level_inputs, agg):
|
||||
for b in BLOCKS:
|
||||
paths = {p for inp in level_inputs for p, _ in leaves(inp[b])}
|
||||
paths = {p for inp in level_inputs for p, _ in leaves(inp.get(b, {}))}
|
||||
for path in paths:
|
||||
nums = [
|
||||
Decimal(str(v))
|
||||
for inp in level_inputs
|
||||
for p, v in leaves(inp[b])
|
||||
for p, v in leaves(inp.get(b, {}))
|
||||
if p == path and isinstance(v, (int, Decimal)) and not isinstance(v, bool)
|
||||
]
|
||||
if nums:
|
||||
@ -211,11 +218,11 @@ def check_sums(f4, sections=None):
|
||||
|
||||
expect(inputs, root)
|
||||
for pid, data in groups.items():
|
||||
lvl_inputs = [i for i in inputs if i["header"]["program_id"] == pid]
|
||||
lvl_inputs = [i for i in inputs if i["header"].get("program_id") == pid]
|
||||
if lvl_inputs:
|
||||
expect(lvl_inputs, data)
|
||||
for jid, data in items.items():
|
||||
lvl_inputs = [i for i in inputs if i["header"]["project_id"] == jid]
|
||||
lvl_inputs = [i for i in inputs if i["header"].get("project_id") == jid]
|
||||
if lvl_inputs:
|
||||
expect(lvl_inputs, data)
|
||||
return root, groups, items, inputs
|
||||
@ -257,9 +264,10 @@ def test_single_line_project_keeps_text_two_lines_null(f4):
|
||||
_, groups, items, _ = check_sums(f4)
|
||||
assert items[single]["contract"]["counterparty"] == "ООО Один"
|
||||
assert items[single]["q1"]["adj_comment"] == "комментарий"
|
||||
assert items[pair]["contract"]["counterparty"] is None
|
||||
assert items[pair]["q1"]["adj_comment"] is None
|
||||
assert groups[prog]["contract"]["counterparty"] is None
|
||||
# AURORA-1848: null-поля в GET вырезаны (jsonb_strip_nulls) — ключа может не быть вовсе
|
||||
assert items[pair]["contract"].get("counterparty") is None
|
||||
assert items[pair]["q1"].get("adj_comment") is None
|
||||
assert groups[prog]["contract"].get("counterparty") is None
|
||||
|
||||
|
||||
def test_single_line_program_keeps_text(f4):
|
||||
@ -275,9 +283,10 @@ def test_empty_program_and_project(f4):
|
||||
empty_prog = f4.program()
|
||||
empty_prj = f4.project(empty_prog)
|
||||
_, groups, items, _ = check_sums(f4)
|
||||
# comment: None — AURORA-1848 вырезает такие поля из GET (jsonb_strip_nulls)
|
||||
zero_plan = {"q1": 0, "q2": 0, "q3": 0, "q4": 0, "year": 0, "comment": None}
|
||||
assert canon(groups[empty_prog]["plan"]) == canon(zero_plan)
|
||||
assert canon(items[empty_prj]["plan"]) == canon(zero_plan)
|
||||
assert canon(groups[empty_prog]["plan"]) == canon(strip_nulls(zero_plan))
|
||||
assert canon(items[empty_prj]["plan"]) == canon(strip_nulls(zero_plan))
|
||||
|
||||
|
||||
def test_decimals_negative_and_null(f4):
|
||||
@ -318,7 +327,8 @@ def test_sections_filter(f4):
|
||||
assert Decimal(str(items[prj]["booking"]["y2026"]["q1"])) == 30
|
||||
for b in BLOCKS:
|
||||
if b not in ("plan", "booking"):
|
||||
assert root[b] is None and groups[prog][b] is None and items[prj][b] is None
|
||||
# AURORA-1848: не выбранный в sections блок вырезан из data, а не null
|
||||
assert root.get(b) is None and groups[prog].get(b) is None and items[prj].get(b) is None
|
||||
|
||||
|
||||
def test_cell_patch_returns_updated_ancestor_sums(f4, client):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user