在前后端开发联调前 的提交20260223

This commit is contained in:
Neo
2026-02-23 23:02:20 +08:00
parent 254ccb1e77
commit fafc95e64c
1142 changed files with 10366960 additions and 36957 deletions

View File

@@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
"""EtlTimer 单元测试"""
from __future__ import annotations
import os
import time
from pathlib import Path
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
from utils.timer import EtlTimer, StepRecord, _fmt_ms
# ── _fmt_ms 格式化 ───────────────────────────────────────────
class TestFmtMs:
def test_sub_second(self):
assert _fmt_ms(123.4) == "123.4ms"
def test_seconds(self):
assert _fmt_ms(2500) == "2.50s"
def test_minutes(self):
result = _fmt_ms(90_000) # 90 秒
assert result.startswith("1m")
def test_hours(self):
result = _fmt_ms(3_700_000) # ~61.7 分钟
assert result.startswith("1h")
# ── StepRecord ───────────────────────────────────────────────
class TestStepRecord:
def test_elapsed_seconds(self):
rec = StepRecord(name="test", start_time=None, elapsed_ms=1500.0)
assert rec.elapsed_seconds == 1.5
def test_to_dict_without_end(self):
from datetime import datetime
now = datetime.now(ZoneInfo("Asia/Shanghai"))
rec = StepRecord(name="s1", start_time=now, elapsed_ms=100.0)
d = rec.to_dict()
assert d["name"] == "s1"
assert d["end_time"] is None
assert d["elapsed_ms"] == 100.0
assert d["children"] == []
def test_to_dict_with_children(self):
from datetime import datetime
tz = ZoneInfo("Asia/Shanghai")
now = datetime.now(tz)
parent = StepRecord(name="p", start_time=now, elapsed_ms=200.0)
child = StepRecord(name="c", start_time=now, end_time=now, elapsed_ms=50.0)
parent.children.append(child)
d = parent.to_dict()
assert len(d["children"]) == 1
assert d["children"][0]["name"] == "c"
# ── EtlTimer 核心流程 ────────────────────────────────────────
class TestEtlTimer:
def test_start_stop_step(self):
timer = EtlTimer()
timer.start()
timer.start_step("STEP_A")
time.sleep(0.01) # 确保有可测量的耗时
rec = timer.stop_step("STEP_A")
assert rec.name == "STEP_A"
assert rec.end_time is not None
assert rec.elapsed_ms > 0
def test_sub_steps(self):
timer = EtlTimer()
timer.start()
timer.start_step("PARENT")
timer.start_sub_step("PARENT", "child_1")
time.sleep(0.01)
timer.stop_sub_step("PARENT", "child_1")
timer.start_sub_step("PARENT", "child_2")
timer.stop_sub_step("PARENT", "child_2")
timer.stop_step("PARENT")
parent = timer.get_step("PARENT")
assert parent is not None
assert len(parent.children) == 2
assert parent.children[0].name == "child_1"
assert parent.children[0].elapsed_ms > 0
def test_stop_unknown_step_raises(self):
timer = EtlTimer()
with pytest.raises(KeyError, match="未找到步骤"):
timer.stop_step("NONEXISTENT")
def test_start_sub_step_unknown_parent_raises(self):
timer = EtlTimer()
with pytest.raises(KeyError, match="未找到父步骤"):
timer.start_sub_step("NONEXISTENT", "child")
def test_stop_sub_step_unknown_raises(self):
timer = EtlTimer()
timer.start()
timer.start_step("P")
with pytest.raises(KeyError, match="未找到子步骤"):
timer.stop_sub_step("P", "no_such_child")
def test_multiple_steps(self):
timer = EtlTimer()
timer.start()
for name in ["ODS_LOAD", "DWD_LOAD", "DWS_AGG"]:
timer.start_step(name)
timer.stop_step(name)
assert len(timer.steps) == 3
assert timer.steps[0].name == "ODS_LOAD"
assert timer.steps[2].name == "DWS_AGG"
def test_to_dict(self):
timer = EtlTimer()
timer.start()
timer.start_step("S1")
timer.stop_step("S1")
report = timer.finish(write_report=False)
d = timer.to_dict()
assert d["overall_start"] is not None
assert d["overall_end"] is not None
assert d["overall_elapsed_ms"] >= 0
assert len(d["steps"]) == 1
def test_overall_elapsed(self):
timer = EtlTimer()
timer.start()
time.sleep(0.02)
timer.finish(write_report=False)
assert timer.overall_elapsed_ms >= 15 # 至少 15ms留余量
def test_finish_returns_markdown(self):
timer = EtlTimer()
timer.start()
timer.start_step("TEST_STEP")
timer.stop_step("TEST_STEP")
md = timer.finish(write_report=False)
assert "# ETL 执行计时报告" in md
assert "TEST_STEP" in md
assert "步骤汇总" in md
def test_markdown_contains_sub_steps(self):
timer = EtlTimer()
timer.start()
timer.start_step("MAIN")
timer.start_sub_step("MAIN", "sub_a")
timer.stop_sub_step("MAIN", "sub_a")
timer.stop_step("MAIN")
md = timer.finish(write_report=False)
assert "步骤详情" in md
assert "sub_a" in md
def test_write_report_requires_env(self):
"""ETL_REPORT_ROOT 未设置时应抛出 KeyError"""
timer = EtlTimer()
timer.start()
timer.start_step("X")
timer.stop_step("X")
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(KeyError, match="ETL_REPORT_ROOT"):
timer.finish(write_report=True)
def test_write_report_creates_file(self, tmp_path: Path):
"""设置 ETL_REPORT_ROOT 后应生成 .md 文件"""
timer = EtlTimer()
timer.start()
timer.start_step("Y")
timer.stop_step("Y")
with patch.dict(os.environ, {"ETL_REPORT_ROOT": str(tmp_path)}):
timer.finish(write_report=True)
md_files = list(tmp_path.glob("etl_timing_*.md"))
assert len(md_files) == 1
content = md_files[0].read_text(encoding="utf-8")
assert "# ETL 执行计时报告" in content
assert "Y" in content
def test_elapsed_equals_end_minus_start(self):
"""Property 7 核心验证:耗时 ≈ 结束时间 - 开始时间"""
timer = EtlTimer()
timer.start()
timer.start_step("VERIFY")
time.sleep(0.05)
rec = timer.stop_step("VERIFY")
# 用 datetime 差值计算的毫秒数
dt_diff_ms = (rec.end_time - rec.start_time).total_seconds() * 1000
# perf_counter 计算的毫秒数
# 两者应在合理误差范围内±50ms考虑系统调度抖动
assert abs(rec.elapsed_ms - dt_diff_ms) < 50