在前后端开发联调前 的提交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,263 @@
# -*- coding: utf-8 -*-
"""
Feature: dataflow-field-completion, Property 7: 计时器记录完整性
**Validates: Requirements 15.2**
对于任意 ETL 步骤序列,计时器输出应包含每个步骤的名称、开始时间、结束时间和耗时,
且耗时等于结束时间减去开始时间。
测试策略:
- 使用 hypothesis 生成随机步骤名称列表1-10 个步骤)
- 每个步骤可选包含 0-5 个子步骤
- 验证属性:
1. 每个步骤的 to_dict() 输出包含 name、start_time、end_time、elapsed_ms
2. elapsed_ms ≈ (end_time - start_time) 的毫秒数(允许 ±50ms 误差)
3. 所有步骤名称都出现在 timer.steps 中
4. timer.to_dict() 的 steps 数量等于实际添加的步骤数
5. 子步骤的 elapsed_ms 也满足上述一致性
"""
from __future__ import annotations
import sys
import time
from datetime import datetime
from pathlib import Path
from hypothesis import given, settings, HealthCheck
import hypothesis.strategies as st
# ── 将 ETL 模块加入 sys.path ──
_ETL_ROOT = (
Path(__file__).resolve().parent.parent
/ "apps" / "etl" / "connectors" / "feiqiu"
)
if str(_ETL_ROOT) not in sys.path:
sys.path.insert(0, str(_ETL_ROOT))
from utils.timer import EtlTimer
# ══════════════════════════════════════════════════════════════════
# Hypothesis 策略
# ══════════════════════════════════════════════════════════════════
# 步骤名称ASCII 字母 + 数字 + 下划线,模拟真实 ETL 任务名
_step_name = st.text(
alphabet=st.sampled_from("ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"),
min_size=3,
max_size=20,
)
# 子步骤名称
_sub_step_name = st.text(
alphabet=st.sampled_from("abcdefghijklmnopqrstuvwxyz_0123456789"),
min_size=2,
max_size=15,
)
@st.composite
def _step_spec(draw):
"""生成一个步骤规格:(步骤名, [子步骤名列表])"""
name = draw(_step_name)
# 0-5 个子步骤,名称唯一
sub_names = draw(
st.lists(_sub_step_name, min_size=0, max_size=5, unique=True)
)
return (name, sub_names)
# 生成 1-10 个步骤,步骤名唯一
_steps_strategy = st.lists(
_step_spec(),
min_size=1,
max_size=10,
).filter(
# 确保步骤名唯一
lambda specs: len(set(s[0] for s in specs)) == len(specs)
)
# 允许的时间误差毫秒——perf_counter 与 datetime.now 之间存在微小差异
_TOLERANCE_MS = 50.0
# ══════════════════════════════════════════════════════════════════
# 辅助函数
# ══════════════════════════════════════════════════════════════════
def _run_timer(step_specs: list[tuple[str, list[str]]]) -> EtlTimer:
"""按给定步骤规格运行计时器,返回完成后的 timer 实例"""
timer = EtlTimer()
timer.start()
for step_name, sub_names in step_specs:
timer.start_step(step_name)
for sub_name in sub_names:
timer.start_sub_step(step_name, sub_name)
# 极短暂停,确保 start/end 时间有差异
time.sleep(0.001)
timer.stop_sub_step(step_name, sub_name)
time.sleep(0.001)
timer.stop_step(step_name)
timer.finish(write_report=False)
return timer
def _parse_iso(iso_str: str) -> datetime:
"""解析 ISO 格式时间字符串"""
return datetime.fromisoformat(iso_str)
# ══════════════════════════════════════════════════════════════════
# Property 7a: to_dict() 输出包含必要字段
# ══════════════════════════════════════════════════════════════════
@given(step_specs=_steps_strategy)
@settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_step_dict_contains_required_fields(step_specs):
"""
**Validates: Requirements 15.2**
每个步骤的 to_dict() 输出必须包含 name、start_time、end_time、elapsed_ms 字段。
"""
timer = _run_timer(step_specs)
for step in timer.steps:
d = step.to_dict()
for key in ("name", "start_time", "end_time", "elapsed_ms"):
assert key in d, f"步骤 {step.name} 的 to_dict() 缺少字段: {key}"
# start_time 和 end_time 不为 None
assert d["start_time"] is not None, f"步骤 {step.name} 的 start_time 为 None"
assert d["end_time"] is not None, f"步骤 {step.name} 的 end_time 为 None"
# ══════════════════════════════════════════════════════════════════
# Property 7b: elapsed_ms ≈ (end_time - start_time)
# ══════════════════════════════════════════════════════════════════
@given(step_specs=_steps_strategy)
@settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_step_elapsed_ms_consistent_with_timestamps(step_specs):
"""
**Validates: Requirements 15.2**
每个步骤的 elapsed_ms 应近似等于 (end_time - start_time) 的毫秒数。
允许 ±50ms 误差perf_counter 与 datetime.now 的微小差异)。
"""
timer = _run_timer(step_specs)
for step in timer.steps:
d = step.to_dict()
start_dt = _parse_iso(d["start_time"])
end_dt = _parse_iso(d["end_time"])
wall_ms = (end_dt - start_dt).total_seconds() * 1000
elapsed_ms = d["elapsed_ms"]
diff = abs(elapsed_ms - wall_ms)
assert diff <= _TOLERANCE_MS, (
f"步骤 {step.name}: elapsed_ms={elapsed_ms:.3f}"
f"wall_clock_ms={wall_ms:.3f} 差异 {diff:.3f}ms 超过容差 {_TOLERANCE_MS}ms"
)
# ══════════════════════════════════════════════════════════════════
# Property 7c: 所有步骤名称都出现在 timer.steps 中
# ══════════════════════════════════════════════════════════════════
@given(step_specs=_steps_strategy)
@settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_all_step_names_present(step_specs):
"""
**Validates: Requirements 15.2**
所有添加的步骤名称都必须出现在 timer.steps 列表中。
"""
timer = _run_timer(step_specs)
recorded_names = {s.name for s in timer.steps}
for step_name, _ in step_specs:
assert step_name in recorded_names, (
f"步骤 {step_name!r} 未出现在 timer.steps 中"
)
# ══════════════════════════════════════════════════════════════════
# Property 7d: timer.to_dict() 的 steps 数量等于实际步骤数
# ══════════════════════════════════════════════════════════════════
@given(step_specs=_steps_strategy)
@settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_timer_dict_step_count_matches(step_specs):
"""
**Validates: Requirements 15.2**
timer.to_dict() 输出的 steps 数量应等于实际添加的步骤数。
"""
timer = _run_timer(step_specs)
timer_dict = timer.to_dict()
assert len(timer_dict["steps"]) == len(step_specs), (
f"to_dict() steps 数量 {len(timer_dict['steps'])} "
f"!= 实际步骤数 {len(step_specs)}"
)
# ══════════════════════════════════════════════════════════════════
# Property 7e: 子步骤的 elapsed_ms 也满足一致性
# ══════════════════════════════════════════════════════════════════
@given(step_specs=_steps_strategy)
@settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_sub_step_elapsed_ms_consistent(step_specs):
"""
**Validates: Requirements 15.2**
子步骤的 elapsed_ms 也应近似等于 (end_time - start_time) 的毫秒数,
且 to_dict() 输出包含必要字段。
"""
timer = _run_timer(step_specs)
for step in timer.steps:
step_dict = step.to_dict()
for child_dict in step_dict["children"]:
# 必要字段检查
for key in ("name", "start_time", "end_time", "elapsed_ms"):
assert key in child_dict, (
f"子步骤 {child_dict.get('name', '?')} 的 to_dict() 缺少字段: {key}"
)
if child_dict["end_time"] is None:
continue
start_dt = _parse_iso(child_dict["start_time"])
end_dt = _parse_iso(child_dict["end_time"])
wall_ms = (end_dt - start_dt).total_seconds() * 1000
elapsed_ms = child_dict["elapsed_ms"]
diff = abs(elapsed_ms - wall_ms)
assert diff <= _TOLERANCE_MS, (
f"子步骤 {child_dict['name']}: elapsed_ms={elapsed_ms:.3f}"
f"wall_clock_ms={wall_ms:.3f} 差异 {diff:.3f}ms 超过容差 {_TOLERANCE_MS}ms"
)