在准备环境前提交次全部更改。
This commit is contained in:
299
apps/backend/tests/test_task_registry_properties.py
Normal file
299
apps/backend/tests/test_task_registry_properties.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""任务注册表分组属性测试(Property-Based Testing)。
|
||||
|
||||
Property 4: 对于 Task_Registry 中的任务集合,分组结果中每个任务应出现在
|
||||
且仅出现在其所属业务域的分组中。
|
||||
|
||||
Validates: Requirements 2.1
|
||||
|
||||
测试策略:
|
||||
1. 直接测试 get_tasks_grouped_by_domain 函数:
|
||||
- 每个任务出现在且仅出现在其 domain 对应的分组中
|
||||
- 分组中的任务总数等于全部任务数(不多不少)
|
||||
- 每个分组的 key 等于该分组内所有任务的 domain
|
||||
2. 通过 API 端点测试(TestClient + mock auth):
|
||||
- 返回的 groups 中每个任务的 domain 与其所在分组 key 一致
|
||||
- 所有任务都出现在结果中
|
||||
3. 随机子集验证:
|
||||
- 随机选取任务子集,验证分组逻辑的一致性
|
||||
- 随机选取 domain,验证该 domain 下的任务都正确
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-registry")
|
||||
|
||||
from hypothesis import given, settings, assume
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from app.services.task_registry import (
|
||||
get_all_tasks,
|
||||
get_tasks_grouped_by_domain,
|
||||
TaskDefinition,
|
||||
)
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
from app.auth.dependencies import get_current_user, CurrentUser
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 辅助
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ALL_TASKS = get_all_tasks()
|
||||
ALL_CODES = [t.code for t in ALL_TASKS]
|
||||
ALL_DOMAINS = list({t.domain for t in ALL_TASKS})
|
||||
|
||||
|
||||
def _mock_user() -> CurrentUser:
|
||||
return CurrentUser(user_id=1, site_id=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.1: 分组完整性 — 每个任务出现在且仅出现在其 domain 分组中
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(data=st.data())
|
||||
def test_every_task_in_exactly_its_domain_group(data):
|
||||
"""Property 4.1: 每个任务出现在且仅出现在其所属业务域的分组中。
|
||||
|
||||
从全量任务中随机选取一个任务,验证它只出现在对应 domain 的分组里,
|
||||
且不出现在其他任何分组中。
|
||||
"""
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
# 随机选取一个任务
|
||||
task = data.draw(st.sampled_from(ALL_TASKS))
|
||||
|
||||
# 该任务必须出现在其 domain 分组中
|
||||
assert task.domain in grouped, (
|
||||
f"任务 {task.code} 的 domain '{task.domain}' 不在分组 keys 中"
|
||||
)
|
||||
domain_codes = [t.code for t in grouped[task.domain]]
|
||||
assert task.code in domain_codes, (
|
||||
f"任务 {task.code} 未出现在其 domain '{task.domain}' 的分组中"
|
||||
)
|
||||
|
||||
# 该任务不应出现在其他任何分组中
|
||||
for other_domain, other_tasks in grouped.items():
|
||||
if other_domain == task.domain:
|
||||
continue
|
||||
other_codes = [t.code for t in other_tasks]
|
||||
assert task.code not in other_codes, (
|
||||
f"任务 {task.code}(domain={task.domain})错误地出现在 "
|
||||
f"domain '{other_domain}' 的分组中"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.2: 分组总数守恒 — 分组中的任务总数等于全部任务数
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(data=st.data())
|
||||
def test_grouped_total_equals_all_tasks(data):
|
||||
"""Property 4.2: 分组中的任务总数等于全部任务数(不多不少)。
|
||||
|
||||
随机选取若干 domain 进行局部验证,同时验证全局总数守恒。
|
||||
"""
|
||||
all_tasks = get_all_tasks()
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
|
||||
# 全局守恒:分组内任务总数 == 全量任务数
|
||||
grouped_total = sum(len(tasks) for tasks in grouped.values())
|
||||
assert grouped_total == len(all_tasks), (
|
||||
f"分组总数 {grouped_total} != 全量任务数 {len(all_tasks)}"
|
||||
)
|
||||
|
||||
# 随机选取一个 domain,验证该 domain 下的任务数量正确
|
||||
domain = data.draw(st.sampled_from(ALL_DOMAINS))
|
||||
expected_count = sum(1 for t in all_tasks if t.domain == domain)
|
||||
actual_count = len(grouped[domain])
|
||||
assert actual_count == expected_count, (
|
||||
f"domain '{domain}' 分组内任务数 {actual_count} != 预期 {expected_count}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.3: 分组 key 一致性 — 每个分组的 key 等于组内所有任务的 domain
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(data=st.data())
|
||||
def test_group_key_matches_task_domains(data):
|
||||
"""Property 4.3: 每个分组的 key 等于该分组内所有任务的 domain。
|
||||
|
||||
随机选取一个 domain 分组,验证组内每个任务的 domain 字段都等于分组 key。
|
||||
"""
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
domain = data.draw(st.sampled_from(list(grouped.keys())))
|
||||
|
||||
for task in grouped[domain]:
|
||||
assert task.domain == domain, (
|
||||
f"分组 '{domain}' 中的任务 {task.code} 的 domain 为 "
|
||||
f"'{task.domain}',与分组 key 不一致"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.4: 任务 code 全局唯一 — 分组后不应出现重复 code
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(data=st.data())
|
||||
def test_no_duplicate_codes_across_groups(data):
|
||||
"""Property 4.4: 分组后所有任务的 code 全局唯一,无重复。
|
||||
|
||||
随机选取若干 domain 的任务合并,验证 code 不重复。
|
||||
"""
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
|
||||
# 收集所有分组中的 code
|
||||
all_codes_in_groups = []
|
||||
for tasks in grouped.values():
|
||||
all_codes_in_groups.extend(t.code for t in tasks)
|
||||
|
||||
assert len(all_codes_in_groups) == len(set(all_codes_in_groups)), (
|
||||
"分组中存在重复的任务 code"
|
||||
)
|
||||
|
||||
# 随机选取两个不同 domain,验证它们的任务 code 无交集
|
||||
if len(ALL_DOMAINS) >= 2:
|
||||
domains = data.draw(
|
||||
st.lists(st.sampled_from(ALL_DOMAINS), min_size=2, max_size=2, unique=True)
|
||||
)
|
||||
codes_a = {t.code for t in grouped[domains[0]]}
|
||||
codes_b = {t.code for t in grouped[domains[1]]}
|
||||
overlap = codes_a & codes_b
|
||||
assert not overlap, (
|
||||
f"domain '{domains[0]}' 和 '{domains[1]}' 存在重叠任务 code: {overlap}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.5: 随机子集分组一致性 — 子集中的任务分组结果与全量一致
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
indices=st.lists(
|
||||
st.integers(min_value=0, max_value=len(ALL_TASKS) - 1),
|
||||
min_size=1,
|
||||
max_size=min(20, len(ALL_TASKS)),
|
||||
unique=True,
|
||||
)
|
||||
)
|
||||
def test_subset_grouping_consistency(indices):
|
||||
"""Property 4.5: 随机选取任务子集,验证每个任务在全量分组中的归属正确。
|
||||
|
||||
对于随机选取的任务子集,每个任务在 get_tasks_grouped_by_domain() 的结果中
|
||||
都应出现在其 domain 对应的分组里。
|
||||
"""
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
subset = [ALL_TASKS[i] for i in indices]
|
||||
|
||||
for task in subset:
|
||||
# 任务的 domain 必须是分组的 key 之一
|
||||
assert task.domain in grouped
|
||||
# 任务必须在对应分组中
|
||||
group_codes = {t.code for t in grouped[task.domain]}
|
||||
assert task.code in group_codes, (
|
||||
f"任务 {task.code} 未出现在 domain '{task.domain}' 的分组中"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.6: API 端点分组正确性 — GET /api/tasks/registry 返回一致的分组
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(data=st.data())
|
||||
def test_api_registry_grouping_correctness(data):
|
||||
"""Property 4.6: API 端点返回的分组中,每个任务的 domain 与分组 key 一致,
|
||||
且所有任务都出现在结果中。
|
||||
"""
|
||||
app.dependency_overrides[get_current_user] = _mock_user
|
||||
try:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/tasks/registry")
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
groups = body["groups"]
|
||||
|
||||
# 收集 API 返回的所有任务 code
|
||||
api_codes: set[str] = set()
|
||||
for domain_key, task_list in groups.items():
|
||||
for task_item in task_list:
|
||||
# 每个任务的 domain 必须等于分组 key
|
||||
assert task_item["domain"] == domain_key, (
|
||||
f"API 返回的任务 {task_item['code']}(domain={task_item['domain']})"
|
||||
f"出现在分组 '{domain_key}' 中,不一致"
|
||||
)
|
||||
api_codes.add(task_item["code"])
|
||||
|
||||
# 所有任务都应出现在 API 结果中
|
||||
all_codes_set = {t.code for t in get_all_tasks()}
|
||||
assert api_codes == all_codes_set, (
|
||||
f"API 返回的任务集合与全量任务不一致。"
|
||||
f"缺失: {all_codes_set - api_codes},"
|
||||
f"多余: {api_codes - all_codes_set}"
|
||||
)
|
||||
|
||||
# 随机选取一个 domain,验证该 domain 下的任务数量与服务层一致
|
||||
if groups:
|
||||
domain = data.draw(st.sampled_from(list(groups.keys())))
|
||||
expected = get_tasks_grouped_by_domain()
|
||||
assert len(groups[domain]) == len(expected[domain]), (
|
||||
f"API 返回的 domain '{domain}' 任务数 {len(groups[domain])} "
|
||||
f"!= 服务层 {len(expected[domain])}"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_current_user, None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 4.7: 随机 domain 过滤验证
|
||||
# Validates: Requirements 2.1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(domain=st.sampled_from(ALL_DOMAINS))
|
||||
def test_random_domain_tasks_all_correct(domain):
|
||||
"""Property 4.7: 随机选取一个 domain,验证该 domain 下的所有任务都正确归属。
|
||||
|
||||
对于选定的 domain:
|
||||
- 分组中的每个任务的 domain 字段都等于选定的 domain
|
||||
- 全量任务中所有属于该 domain 的任务都出现在分组中
|
||||
"""
|
||||
grouped = get_tasks_grouped_by_domain()
|
||||
all_tasks = get_all_tasks()
|
||||
|
||||
# 分组中该 domain 的任务
|
||||
group_tasks = grouped.get(domain, [])
|
||||
|
||||
# 全量任务中属于该 domain 的任务
|
||||
expected_tasks = [t for t in all_tasks if t.domain == domain]
|
||||
|
||||
# 数量一致
|
||||
assert len(group_tasks) == len(expected_tasks), (
|
||||
f"domain '{domain}': 分组中 {len(group_tasks)} 个任务,"
|
||||
f"预期 {len(expected_tasks)} 个"
|
||||
)
|
||||
|
||||
# code 集合一致
|
||||
group_codes = {t.code for t in group_tasks}
|
||||
expected_codes = {t.code for t in expected_tasks}
|
||||
assert group_codes == expected_codes, (
|
||||
f"domain '{domain}': 分组 codes {group_codes} != 预期 {expected_codes}"
|
||||
)
|
||||
|
||||
# 每个任务的 domain 字段都正确
|
||||
for task in group_tasks:
|
||||
assert task.domain == domain
|
||||
Reference in New Issue
Block a user