Fix business compare history, search, sampling and exports

This commit is contained in:
oliver 2026-10-10 23:20:50 +08:00
parent afa8e06b2d
commit 4d5101ec3a
19 changed files with 1598 additions and 274 deletions

View file

@ -0,0 +1,87 @@
"""Export must page lazily, keep tied sequence rows, and produce valid UTF-8 CSV."""
from __future__ import annotations
import csv
import io
import unittest
import zipfile
from unittest.mock import patch
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session
from netx_api.biz_state.compare_service import _iter_sheet_diffs, export_run_zip
from netx_api.db import Base
from netx_api.models import BizCompareDiff, BizCompareRun
class CompareExportTests(unittest.TestCase):
def setUp(self):
self.engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(self.engine)
self.db = Session(self.engine)
self.sheet = {"sheet_id": "sheet", "metric_id": "test", "key_fields": ["id"],
"compare_fields": ["note"], "display_fields": ["id", "note"],
"summary": {"unchanged": 7}, "status": "done"}
self.run = BizCompareRun(id="run", metric_id="test", status="success",
summary_json={"sheets": [self.sheet]})
self.db.add(self.run)
for seq, ids in ((0, "cba"), (1, "ed"), (2, "gf")):
for rid in ids:
note = '中文, "quoted"\nsecond line'
self.db.add(BizCompareDiff(id=rid, run_id="run", metric_id="sheet", seq=seq,
kind="unchanged", key_json={"id": rid},
before_json={"id": rid, "note": note},
after_json={"id": rid, "note": note}))
self.db.commit()
def tearDown(self):
self.db.close()
self.engine.dispose()
def test_cursor_handles_tied_sequence_numbers_across_chunks(self):
statements = []
def record(_conn, _cursor, statement, _parameters, _context, _executemany):
if "ORDER BY biz_compare_diff.seq" in statement:
statements.append(statement)
event.listen(self.engine, "before_cursor_execute", record)
with patch("netx_api.biz_state.compare_service._DIFF_CHUNK", 2):
iterator = _iter_sheet_diffs(self.db, "run", "sheet")
self.assertEqual(statements, [])
self.assertEqual(next(iterator)["key"]["id"], "a")
self.assertEqual(next(iterator)["key"]["id"], "b")
self.assertEqual(len(statements), 1, "do not load the next chunk early")
self.assertEqual([d["key"]["id"] for d in iterator], list("cdefg"))
self.assertEqual(len(statements), 4)
self.assertTrue(all("biz_compare_diff.seq >" in q and "biz_compare_diff.id >" in q
for q in statements[1:]))
def test_export_csv_escapes_unicode_quotes_commas_and_newlines(self):
with patch("netx_api.biz_state.compare_service._DIFF_CHUNK", 2):
archive_bytes = export_run_zip(self.db, "run")
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
payload = archive.read("tables/sheet.csv")
self.assertTrue(payload.startswith(b"\xef\xbb\xbf"))
rows = list(csv.DictReader(io.StringIO(payload.decode("utf-8-sig"))))
self.assertEqual([r["id"] for r in rows], list("abcdefg"))
self.assertTrue(all(r["note__pre"] == '中文, "quoted"\nsecond line' for r in rows))
self.assertTrue(all(r["note__pre"] == r["note__post"] for r in rows))
def test_other_sheets_do_not_leak_into_export(self):
self.db.add(BizCompareDiff(id="other", run_id="run", metric_id="another", seq=0,
kind="added", key_json={"id": "other"}, after_json={"note": "other"}))
self.db.commit()
with patch("netx_api.biz_state.compare_service._DIFF_CHUNK", 2):
self.assertEqual([d["key"]["id"] for d in _iter_sheet_diffs(self.db, "run", "sheet")], list("abcdefg"))
def test_legacy_inline_and_empty_exports(self):
self.db.query(BizCompareDiff).delete()
inline = {"kind": "added", "key": {"id": "legacy"}, "after": {"note": "value"}}
self.run.summary_json = {"sheets": [{**self.sheet, "diffs": [inline]}]}
self.db.commit()
self.assertEqual(list(_iter_sheet_diffs(self.db, "run", "sheet")), [inline])
self.run.summary_json = {"sheets": [self.sheet]}
self.db.commit()
with zipfile.ZipFile(io.BytesIO(export_run_zip(self.db, "run"))) as archive:
self.assertEqual(archive.read("tables/sheet.csv").decode("utf-8-sig"), "kind,id,note__pre,note__post\n")