Files
xiaobai-review/tests/test_data_gateway.py
T

185 lines
7.2 KiB
Python

from __future__ import annotations
import ast
import unittest
from datetime import datetime, timedelta
from pathlib import Path
from backend.data import (
DataPolicyError,
DataQualityError,
DataSourcePolicy,
QualityEvidence,
build_data_gateway,
)
from backend.data.quality import market_timezone
class DataGatewayTests(unittest.TestCase):
def test_policy_allows_registered_calculation_source(self) -> None:
policy = DataSourcePolicy.load()
contract = policy.assert_allowed(
"market.stock_daily", "tushare", "calculation"
)
self.assertEqual(contract.primary, "tushare")
def test_policy_rejects_public_web_source_for_calculation(self) -> None:
policy = DataSourcePolicy.load()
with self.assertRaises(DataPolicyError):
policy.assert_allowed(
"observation.realtime_indices", "eastmoney", "calculation"
)
def test_policy_rejects_blocked_dataset(self) -> None:
policy = DataSourcePolicy.load()
with self.assertRaises(DataPolicyError):
policy.assert_allowed("market.level2", "unresolved", "display")
def test_gateway_uses_live_token_supplier_and_shared_ifind(self) -> None:
token = {"value": "first"}
gateway = build_data_gateway(
{"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
lambda: token["value"],
)
self.assertEqual(gateway.tushare().token, "first")
token["value"] = "second"
self.assertEqual(gateway.tushare().token, "second")
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
source = (
Path(__file__).resolve().parents[1]
/ "backend"
/ "features"
/ "market"
/ "service.py"
).read_text(encoding="utf-8")
self.assertEqual(source.count("TushareClient(self.token)"), 1)
self.assertIn("return gateway.tushare()", source)
def test_provider_construction_has_unique_declared_owners(self) -> None:
root = Path(__file__).resolve().parents[1]
owners = {
"EastmoneyChartClient": {"backend/data/gateway.py"},
"IfindHttpClient": {"backend/data/gateway.py"},
"IfindProvider": {"backend/data/gateway.py"},
"MarketChartClient": {"backend/data/gateway.py"},
"TushareClient": {"backend/features/market/service.py"},
"TushareProvider": {"backend/data/gateway.py"},
"WebRealtimeAggregator": {"backend/data/gateway.py"},
}
found = {name: set() for name in owners}
for path in (root / "backend").rglob("*.py"):
relative = path.relative_to(root).as_posix()
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
if name in found:
found[name].add(relative)
self.assertEqual(found, owners)
provider_source = (root / "backend/data/providers/tushare.py").read_text(
encoding="utf-8"
)
self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_source)
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
timezone = market_timezone()
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
gateway = build_data_gateway({})
report = gateway.require_quality(
QualityEvidence(
dataset_id="market.stock_daily",
provider_id="tushare",
data_time="2026-07-29",
observed_at=now,
actual_count=5000,
expected_count=5000,
adjustment="current-unadjusted",
units={
"open": "CNY/share", "high": "CNY/share", "low": "CNY/share",
"close": "CNY/share", "pct_chg": "percent",
"volume_shares": "share", "amount_yuan": "CNY",
},
),
"calculation",
now,
)
self.assertTrue(report.accepted)
self.assertEqual(report.coverage_ratio, 1.0)
def test_quality_gate_rejects_stale_dynamic_auction(self) -> None:
timezone = market_timezone()
now = datetime(2026, 7, 29, 9, 24, tzinfo=timezone)
gateway = build_data_gateway({})
with self.assertRaises(DataQualityError):
gateway.require_quality(
QualityEvidence(
dataset_id="market.auction_dynamic",
provider_id="ifind",
data_time=now - timedelta(seconds=30),
observed_at=now - timedelta(seconds=29),
units={
"price": "CNY/share", "volume_shares": "share",
"amount_yuan": "CNY", "pre_close": "CNY/share",
"turnover_rate_pct": "percent", "volume_ratio": "ratio",
"float_share": "share",
},
),
"calculation",
now,
)
def test_quality_gate_rejects_low_coverage_and_wrong_adjustment(self) -> None:
timezone = market_timezone()
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
gateway = build_data_gateway({})
report = gateway.quality.evaluate(
QualityEvidence(
dataset_id="market.stock_daily",
provider_id="tushare",
data_time="2026-07-29",
observed_at=now,
actual_count=4000,
expected_count=5000,
adjustment="forward1",
),
"calculation",
now,
)
self.assertFalse(report.accepted)
self.assertTrue(any("Coverage" in issue for issue in report.issues))
self.assertTrue(any("Adjustment" in issue for issue in report.issues))
def test_quality_gate_enforces_financial_point_in_time(self) -> None:
timezone = market_timezone()
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
gateway = build_data_gateway({})
report = gateway.quality.evaluate(
QualityEvidence(
dataset_id="market.fundamentals",
provider_id="tushare",
data_time="2026-06-30",
observed_at=now,
available_at="2026-08-15",
),
"calculation",
now,
)
self.assertFalse(report.accepted)
self.assertTrue(any("not available" in issue for issue in report.issues))
def test_provider_chain_never_silently_promotes_display_fallback(self) -> None:
gateway = build_data_gateway({})
self.assertEqual(
gateway.provider_chain("chart.intraday", "display"),
("ifind", "eastmoney"),
)
with self.assertRaises(RuntimeError):
gateway.provider_chain("chart.intraday", "calculation")
if __name__ == "__main__":
unittest.main()