Files
ecm_sync_system/tests/unit/test_summary_report.py
T
2026-04-08 09:28:14 +08:00

172 lines
5.9 KiB
Python

from __future__ import annotations
import logging
from io import StringIO
import pytest
from sync_state_machine.pipeline.summary_report import print_pipeline_summary
from sync_state_machine.logging import clear_scope_display_names, get_scope_logger, set_scope_display_name
class _FakeBindingManager:
async def get_all_records(self, node_type: str) -> list[dict]:
return []
class _FakeDataSource:
def get_action_summary(self) -> dict:
return {}
class _FakeCollection:
def filter(self, *, node_type: str) -> list:
return []
@pytest.mark.asyncio
async def test_pipeline_summary_omits_diff_schemas_column() -> None:
stream = StringIO()
logger = logging.getLogger("tests.summary_report")
logger.handlers.clear()
logger.setLevel(logging.INFO)
logger.propagate = False
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)
try:
await print_pipeline_summary(
logger=logger,
node_types=["project"],
binding_manager=_FakeBindingManager(),
local_datasource=_FakeDataSource(),
remote_datasource=_FakeDataSource(),
local_collection=_FakeCollection(),
remote_collection=_FakeCollection(),
consistency_by_type={
"project": {
"checked_pairs": 1,
"mismatch_count": 1,
"fields": [
{
"field_name": "actual_production_date",
"mismatch_count": 1,
"total_count": 1,
"schema_refs": ["ProjectResponseBase", "ProjectBaseInfoUpdate"],
"samples": [{"local": None, "remote": "2025-11-14"}],
}
],
}
},
stats={
"loaded": 1,
"synced": 0,
"failed": 0,
"post_check_passed": True,
},
)
finally:
logger.removeHandler(handler)
output = stream.getvalue()
assert "diff_schemas" not in output
assert "ProjectResponseBase" not in output
assert "ProjectBaseInfoUpdate" not in output
assert "field mismatch/total samples" in output
assert "actual_production_date" in output
assert "None/'2025-11-14'" in output
@pytest.mark.asyncio
async def test_pipeline_summary_only_prefixes_headers_for_scoped_logger() -> None:
clear_scope_display_names()
set_scope_display_name("project_id:p1", "project:Alpha")
stream = StringIO()
base_logger = logging.getLogger("sync_state_machine.tests.summary_report_scoped")
base_logger.handlers.clear()
base_logger.setLevel(logging.INFO)
base_logger.propagate = False
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(message)s"))
base_logger.addHandler(handler)
try:
scoped_logger = get_scope_logger("project_id:p1", "tests.summary_report_scoped")
await print_pipeline_summary(
logger=scoped_logger,
node_types=["project"],
binding_manager=_FakeBindingManager(),
local_datasource=_FakeDataSource(),
remote_datasource=_FakeDataSource(),
local_collection=_FakeCollection(),
remote_collection=_FakeCollection(),
consistency_by_type={
"project": {
"checked_pairs": 1,
"mismatch_count": 0,
"fields": [],
}
},
stats={
"loaded": 1,
"synced": 0,
"failed": 0,
"post_check_passed": True,
},
)
finally:
base_logger.removeHandler(handler)
clear_scope_display_names()
lines = [line for line in stream.getvalue().splitlines() if line.strip()]
assert any(line.startswith("[project:Alpha] 📊 Local Summary") for line in lines)
assert any(line == "Node Type Bound/Rec/Load Create(S/F/T) Update(S/F/T) Delete(S/F/T) Consistent(S/F/T) " for line in lines)
assert not any(line.startswith("[project:Alpha] Node Type") for line in lines)
assert not any(line.startswith("[project:Alpha] TOTAL") for line in lines)
def test_scope_logger_prefixes_messages() -> None:
clear_scope_display_names()
stream = StringIO()
base_logger = logging.getLogger("sync_state_machine.tests.scope_logger")
base_logger.handlers.clear()
base_logger.setLevel(logging.INFO)
base_logger.propagate = False
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(message)s"))
base_logger.addHandler(handler)
try:
scoped_logger = get_scope_logger("project_id:p1", "tests.scope_logger")
scoped_logger.info("Strategy start: project")
finally:
base_logger.removeHandler(handler)
assert stream.getvalue().strip() == "[project_id:p1] Strategy start: project"
def test_scope_logger_uses_registered_display_name() -> None:
clear_scope_display_names()
set_scope_display_name("project_id:p1", "project:Alpha")
stream = StringIO()
base_logger = logging.getLogger("sync_state_machine.tests.scope_logger_display")
base_logger.handlers.clear()
base_logger.setLevel(logging.INFO)
base_logger.propagate = False
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(message)s"))
base_logger.addHandler(handler)
try:
scoped_logger = get_scope_logger("project_id:p1", "tests.scope_logger_display")
scoped_logger.info("Strategy start: project")
finally:
base_logger.removeHandler(handler)
clear_scope_display_names()
assert stream.getvalue().strip() == "[project:Alpha] Strategy start: project"