在前后端开发联调前 的提交20260223
This commit is contained in:
263
tests/test_property_7_timer_completeness.py
Normal file
263
tests/test_property_7_timer_completeness.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user