"""统一数值格式化测试。 **背景**:`config/report.yml: layout.decimals` 长期是摆设 —— `ratio` 与 `money` 从未被读取,各报告模块各自硬编码小数位(7 处), 前端也把百分比写死 2 位。结果是「改了配置不生效」。 本测试钉住「配置必须真的驱动输出」。 """ from __future__ import annotations import pytest from hdiv.report.format import NumFmt # --------------------------------------------------------------------------- # 语义:百分比小数位 = ratio - 2 # --------------------------------------------------------------------------- @pytest.mark.parametrize( "ratio,expect", [(2, "6%"), (4, "6.17%"), (6, "6.1715%"), (8, "6.171491%")], ) def test_percent_decimals_derive_from_ratio(ratio: int, expect: str) -> None: """比率保留 ratio 位后乘 100,恰好少两位 —— 这是与用户确认的语义。""" f = NumFmt(ratio=ratio) assert f.pct(0.06171491) == expect assert f.percent == max(0, ratio - 2) def test_ratio_str_keeps_ratio_decimals() -> None: assert NumFmt(ratio=4).ratio_str(0.06171491) == "0.0617" assert NumFmt(ratio=6).ratio_str(0.06171491) == "0.061715" def test_ratio_below_two_does_not_go_negative() -> None: """ratio=1 时百分比不能出现负小数位(会抛异常)。""" f = NumFmt(ratio=1) assert f.percent == 0 assert f.pct(0.0617) == "6%" def test_missing_values_render_as_dash() -> None: f = NumFmt() for v in (None, float("nan")): assert f.pct(v) == "—" assert f.ratio_str(v) == "—" assert f.yi(v) == "—" def test_non_numeric_falls_back_to_str() -> None: """传进来已格式化的字符串不应崩,也不应二次加工。""" f = NumFmt() assert f.pct("已格式化") == "已格式化" def test_pp_and_plus_signs() -> None: f = NumFmt(ratio=4) assert f.pct_pp(0.1211) == "+12.11pp" assert f.pct_pp(-0.0324) == "-3.24pp" assert f.pct(0.0401, plus=True) == "+4.01%" def test_by_unit_dispatch() -> None: f = NumFmt(ratio=4, money=2) assert f.by_unit(0.0617, "pct") == "6.17%" assert f.by_unit(123456789.0, "money") == "1.23亿" assert f.by_unit(16.72, "years") == "17" assert f.by_unit(0.0617, "ratio") == "0.0617" assert f.by_unit(2838, "int") == "2,838" # --------------------------------------------------------------------------- # 配置必须真的驱动输出 # --------------------------------------------------------------------------- def test_from_config_reads_report_yml() -> None: from hdiv.core.config import load_config cfg = load_config("report") f = NumFmt.from_config(cfg) d = cfg.layout.decimals assert (f.ratio, f.money, f.price) == (d.ratio, d.money, d.price) assert f.percent == max(0, d.ratio - 2) def test_from_config_survives_broken_config() -> None: """配置不可用时回落默认值,而不是让报告生成崩掉。""" class Boom: @property def layout(self): raise RuntimeError("配置坏了") f = NumFmt.from_config(Boom()) assert f.ratio == 4 def test_renderer_uses_config_not_hardcoded_defaults() -> None: """回归:渲染器的 fmt_pct 曾默认 2 位且从不读 decimals.ratio。""" import inspect from hdiv.report.renderer import Renderer src = inspect.getsource(Renderer.fmt_pct) assert "NumFmt" in src or "self.fmt" in src, "fmt_pct 应走统一格式化器" assert ":.2f}%" not in src, "fmt_pct 不应再硬编码 2 位" def test_no_hardcoded_percent_format_in_report_modules() -> None: """五个报告模块都不应再有硬编码的百分比精度。""" from pathlib import Path root = Path(__file__).resolve().parents[1] / "src" / "hdiv" / "report" offenders = [] for name in ("profile_report", "backtest_report", "universe_report", "sensitivity_report", "walkforward_report"): text = (root / f"{name}.py").read_text(encoding="utf-8") for i, line in enumerate(text.splitlines(), 1): if ":.2f}%" in line or ":.2f}pp" in line: offenders.append(f"{name}:{i}") assert not offenders, f"仍硬编码百分比精度:{offenders}" def test_frontend_precision_endpoint_exists() -> None: """前端也必须由配置驱动(曾把百分比写死 2 位)。""" from hdiv.web.server import ROUTES assert any(pat.match("/api/config/display") for _m, pat, _f in ROUTES), \ "缺少 /api/config/display,前端无法获知配置的显示精度" def test_no_hardcoded_percent_anywhere_in_src() -> None: """整个 src/ 都不应再有硬编码的百分比精度(含接口层与 CLI 输出)。 曾散落 20 余处,其中「成交理由里的股息率」是用户直接看到的那种。 唯一允许的例外是 format.py 自身的文档说明。 """ from pathlib import Path root = Path(__file__).resolve().parents[1] / "src" / "hdiv" offenders = [] for f in root.rglob("*.py"): if f.name == "format.py": continue for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1): if ":.2f}%" in line or "100:.2f" in line: offenders.append(f"{f.relative_to(root)}:{i}") assert not offenders, f"仍硬编码百分比精度:{offenders}" def test_frontend_uses_config_precision() -> None: from pathlib import Path js = (Path(__file__).resolve().parents[1] / "web" / "app.js").read_text(encoding="utf-8") assert "FMT" in js and "config/display" in js, "前端未接入配置驱动精度" assert "FMT.percent" in js, "百分比应使用配置的 percent"