# -*- 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