"""Normalization tests — synthetic fixtures only (_synthetic).""" import math import sys import types from pathlib import Path import pandas as pd import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parent)) from fixtures_synth import synthetic_daily, SYNTH # noqa: E402 from worker.normalize import normalize_frame, build_manifest_entry, \ compute_object_hash, coverage_summary, NormalizeError # noqa: E402 def test_synthetic_marker_present(): assert SYNTH["_synthetic"] is True df = synthetic_daily() assert df.attrs["synthetic"] is True def test_normalize_keeps_raw_extra_fields_and_precision(): df = synthetic_daily() out = normalize_frame(df, symbol="SH#600000") assert "_synthetic" not in out.columns # canonical columns present for col in ("date", "open", "high", "low", "close", "volume"): assert col in out.columns # raw extra fields preserved assert "amount" in out.columns and "turnover" in out.columns # precision kept, no rounding drift assert out["close"].iloc[0] == df["close"].iloc[0] assert out["volume"].iloc[3] == df["volume"].iloc[3] # symbol never substituted across instruments assert out["symbol"].iloc[5] == "SH#600000" assert out["date"].notna().all() def test_normalize_rejects_missing_ohlcv(): df = synthetic_daily() df.loc[4, "close"] = math.nan with pytest.raises(NormalizeError): normalize_frame(df, symbol="SH#600000") def test_normalize_rejects_bad_dates(): df = synthetic_daily() df["date"] = df["date"].astype(object) df.loc[2, "date"] = "not-a-date" with pytest.raises(NormalizeError): normalize_frame(df, symbol="SH#600000") def test_normalize_sorts_and_dedups_dates(): df = synthetic_daily() df = pd.concat([df.iloc[5:], df.iloc[:5]]).reset_index(drop=True) out = normalize_frame(df, symbol="SH#600000") assert list(out["date"]) == sorted(out["date"]) assert len(out) == 20 def test_object_hash_stable_and_content_sensitive(): df = synthetic_daily() h1 = compute_object_hash(df) h2 = compute_object_hash(normalize_frame(df, symbol="SH#600000")) assert h1 == h2 and len(h1) == 64 df2 = synthetic_daily(base=11.0) assert compute_object_hash(df2) != h1 def test_coverage_summary_and_manifest_entry(): df = synthetic_daily(days=10, extra_fields=False) cov = coverage_summary(df) assert cov["actual_start"] == df["date"].iloc[0] entry = build_manifest_entry( df, instrument={"symbol": "SH#600000", "market": "cn", "asset_type": "stock"}, provider="akshare", endpoint="stock_zh_a_hist", params={"period": "daily"}, adjustment="none", requested_start="2024-01-01", requested_end="2024-12-31", raw_object_hash="rawhash", warnings=[], schema_version="1", normalization_version="1", path="objects/ab/cd.json", fetched_at="2026-09-16T00:00:00Z", akshare_version="1.18.95", ) assert entry["immutable"] is True assert entry["row_count"] == 10 assert entry["adjustment"] == "none" assert entry["actual_start"] <= entry["actual_end"] assert "potential symbol substitution" not in entry["warnings"] for internal in ("path",): assert internal in entry # server strips internal-only fields downstream