mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 01:50:46 +08:00
87 lines
4.5 KiB
Python
87 lines
4.5 KiB
Python
"""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")
|