# -*- coding: utf-8 -*- """ Feature: dataflow-field-completion, Property 3: TABLE_MAP 覆盖完整性 **Validates: Requirements 7.2, 8.2** 对于任意在 TABLE_MAP 中注册的 DWD 表,该表的所有非 SCD2 列要么在 FACT_MAPPINGS 中有显式映射,要么在对应 ODS 表中存在同名列(自动映射)。 本测试聚焦以下可静态验证的属性: 1. TABLE_MAP 所有条目的 ODS 源表非空 2. C 类表在 TABLE_MAP 中注册 3. C 类表在 FACT_MAPPINGS 中有条目 4. C 类表映射字段数量与期望一致 5. TABLE_MAP 与 FACT_MAPPINGS 交叉一致性 6. hypothesis 属性测试:随机 TABLE_MAP 条目结构验证 """ from __future__ import annotations import sys 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 tasks.dwd.dwd_load_task import DwdLoadTask # ── C 类表定义 ── C_CLASS_TABLES = [ "dwd.dwd_goods_stock_summary", "dwd.dwd_goods_stock_movement", ] # ── C 类表期望映射字段 ── # goods_stock_summary → dwd_goods_stock_summary(14 个字段) _GOODS_STOCK_SUMMARY_EXPECTED_COLS = { "site_goods_id", "goods_name", "goods_unit", "goods_category_id", "goods_category_second_id", "category_name", "range_start_stock", "range_end_stock", "range_in", "range_out", "range_sale", "range_sale_money", "range_inventory", "current_stock", } # goods_stock_movements → dwd_goods_stock_movement(19 个字段) _GOODS_STOCK_MOVEMENT_EXPECTED_COLS = { "site_goods_stock_id", "tenant_id", "site_id", "site_goods_id", "goods_name", "goods_category_id", "goods_second_category_id", "unit", "price", "stock_type", "change_num", "start_num", "end_num", "change_num_a", "start_num_a", "end_num_a", "remark", "operator_name", "create_time", } # ── 收集所有 TABLE_MAP 条目 ── _ALL_TABLE_MAP_ENTRIES = list(DwdLoadTask.TABLE_MAP.items()) # ══════════════════════════════════════════════════════════════════ # Property 3.1: TABLE_MAP 所有条目的 ODS 源表非空 # ══════════════════════════════════════════════════════════════════ def test_all_table_map_entries_have_ods_source(): """TABLE_MAP 中每个 DWD 表都有非空的 ODS 源表名。""" for dwd_table, ods_table in DwdLoadTask.TABLE_MAP.items(): assert ods_table and isinstance(ods_table, str) and ods_table.strip(), \ f"{dwd_table}: TABLE_MAP 中的 ODS 源表为空或无效" # ══════════════════════════════════════════════════════════════════ # Property 3.2: C 类表在 TABLE_MAP 中注册 # ══════════════════════════════════════════════════════════════════ def test_c_class_tables_registered_in_table_map(): """ **Validates: Requirements 7.2, 8.2** C 类表(dwd_goods_stock_summary、dwd_goods_stock_movement) 必须在 TABLE_MAP 中注册。 """ for table in C_CLASS_TABLES: assert table in DwdLoadTask.TABLE_MAP, \ f"C 类表 {table} 未在 TABLE_MAP 中注册" ods_table = DwdLoadTask.TABLE_MAP[table] assert ods_table and isinstance(ods_table, str), \ f"C 类表 {table}: TABLE_MAP 中的 ODS 源表无效" # ══════════════════════════════════════════════════════════════════ # Property 3.3: C 类表在 FACT_MAPPINGS 中有条目 # ══════════════════════════════════════════════════════════════════ def test_c_class_tables_have_fact_mappings(): """ **Validates: Requirements 7.2, 8.2** C 类表必须在 FACT_MAPPINGS 中有至少一个映射条目。 """ for table in C_CLASS_TABLES: entries = DwdLoadTask.FACT_MAPPINGS.get(table, []) assert len(entries) > 0, \ f"C 类表 {table} 在 FACT_MAPPINGS 中无条目" # ══════════════════════════════════════════════════════════════════ # Property 3.4: goods_stock_summary 14 个字段全覆盖 # ══════════════════════════════════════════════════════════════════ def test_goods_stock_summary_mapping_coverage(): """ **Validates: Requirements 7.2** dwd.dwd_goods_stock_summary 的 FACT_MAPPINGS 应覆盖全部 14 个期望字段。 """ table = "dwd.dwd_goods_stock_summary" entries = DwdLoadTask.FACT_MAPPINGS.get(table, []) actual_cols = {e[0].lower() for e in entries} # 验证数量 assert len(entries) == 14, \ f"{table}: 期望 14 个映射条目,实际 {len(entries)} 个" # 验证字段覆盖 missing = _GOODS_STOCK_SUMMARY_EXPECTED_COLS - actual_cols assert not missing, \ f"{table}: 缺少映射字段 {missing}" extra = actual_cols - _GOODS_STOCK_SUMMARY_EXPECTED_COLS assert not extra, \ f"{table}: 存在多余映射字段 {extra}" # ══════════════════════════════════════════════════════════════════ # Property 3.5: goods_stock_movement 19 个字段全覆盖 # ══════════════════════════════════════════════════════════════════ def test_goods_stock_movement_mapping_coverage(): """ **Validates: Requirements 8.2** dwd.dwd_goods_stock_movement 的 FACT_MAPPINGS 应覆盖全部 19 个期望字段。 """ table = "dwd.dwd_goods_stock_movement" entries = DwdLoadTask.FACT_MAPPINGS.get(table, []) actual_cols = {e[0].lower() for e in entries} # 验证数量 assert len(entries) == 19, \ f"{table}: 期望 19 个映射条目,实际 {len(entries)} 个" # 验证字段覆盖 missing = _GOODS_STOCK_MOVEMENT_EXPECTED_COLS - actual_cols assert not missing, \ f"{table}: 缺少映射字段 {missing}" extra = actual_cols - _GOODS_STOCK_MOVEMENT_EXPECTED_COLS assert not extra, \ f"{table}: 存在多余映射字段 {extra}" # ══════════════════════════════════════════════════════════════════ # Property 3.6: FACT_MAPPINGS 是 TABLE_MAP 的子集 # ══════════════════════════════════════════════════════════════════ def test_fact_mappings_subset_of_table_map(): """FACT_MAPPINGS 中的所有 DWD 表都必须在 TABLE_MAP 中注册。""" for table in DwdLoadTask.FACT_MAPPINGS: assert table in DwdLoadTask.TABLE_MAP, \ f"FACT_MAPPINGS 中的 {table} 未在 TABLE_MAP 中注册" # ══════════════════════════════════════════════════════════════════ # Hypothesis 属性测试:随机 TABLE_MAP 条目结构验证 # ══════════════════════════════════════════════════════════════════ _table_map_entry_strategy = st.sampled_from(_ALL_TABLE_MAP_ENTRIES) @given(entry=_table_map_entry_strategy) @settings(max_examples=200, suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_random_table_map_entry_valid(entry): """ **Validates: Requirements 7.2, 8.2** 对于任意随机选取的 TABLE_MAP 条目,验证: - DWD 表名为 "dwd." 前缀的非空字符串 - ODS 源表名为 "ods." 前缀的非空字符串 - 如果该表在 FACT_MAPPINGS 中有条目,每个条目都是合法的三元组 """ dwd_table, ods_table = entry # DWD 表名格式验证 assert isinstance(dwd_table, str) and dwd_table.startswith("dwd."), \ f"TABLE_MAP key {dwd_table!r} 不以 'dwd.' 开头" # ODS 源表名格式验证 assert isinstance(ods_table, str) and ods_table.startswith("ods."), \ f"TABLE_MAP[{dwd_table}] = {ods_table!r} 不以 'ods.' 开头" # 如果有 FACT_MAPPINGS 条目,验证结构 entries = DwdLoadTask.FACT_MAPPINGS.get(dwd_table, []) for i, e in enumerate(entries): assert isinstance(e, (tuple, list)) and len(e) == 3, \ f"{dwd_table}[{i}]: FACT_MAPPINGS 条目应为三元组" dwd_col, ods_expr, cast_type = e assert isinstance(dwd_col, str) and dwd_col.strip(), \ f"{dwd_table}[{i}]: dwd_col 为空" assert isinstance(ods_expr, str) and ods_expr.strip(), \ f"{dwd_table}[{i}]: ods_expr 为空" assert cast_type is None or isinstance(cast_type, str), \ f"{dwd_table}[{i}].{dwd_col}: cast_type 类型无效"