Files
Neo-ZQYY/apps/backend/tests/test_task_registry_properties.py

300 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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