在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
"""CLIBuilder 单元测试
覆盖7 种 Flow、3 种处理模式、时间窗口、store_id 自动注入、extra_args 等。
"""
import pytest
from app.schemas.tasks import TaskConfigSchema
from app.services.cli_builder import CLIBuilder, VALID_FLOWS, VALID_PROCESSING_MODES
@pytest.fixture
def builder() -> CLIBuilder:
return CLIBuilder()
ETL_PATH = "/fake/etl/project"
# ---------------------------------------------------------------------------
# 基本命令结构
# ---------------------------------------------------------------------------
class TestBasicCommand:
def test_minimal_command(self, builder: CLIBuilder):
"""最小配置应生成 python -m cli.main --pipeline ... --processing-mode ..."""
config = TaskConfigSchema(tasks=["ODS_MEMBER"])
cmd = builder.build_command(config, ETL_PATH)
assert cmd[:3] == ["python", "-m", "cli.main"]
assert "--pipeline" in cmd
assert "--processing-mode" in cmd
def test_custom_python_executable(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"])
cmd = builder.build_command(config, ETL_PATH, python_executable="python3")
assert cmd[0] == "python3"
def test_tasks_joined_by_comma(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER", "ODS_PAYMENT", "ODS_REFUND"])
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--tasks")
assert cmd[idx + 1] == "ODS_MEMBER,ODS_PAYMENT,ODS_REFUND"
def test_empty_tasks_no_tasks_arg(self, builder: CLIBuilder):
"""空任务列表不应生成 --tasks 参数"""
config = TaskConfigSchema(tasks=[])
cmd = builder.build_command(config, ETL_PATH)
assert "--tasks" not in cmd
# ---------------------------------------------------------------------------
# 7 种 Flow
# ---------------------------------------------------------------------------
class TestFlows:
@pytest.mark.parametrize("flow_id", sorted(VALID_FLOWS))
def test_all_flows_accepted(self, builder: CLIBuilder, flow_id: str):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], pipeline=flow_id)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--pipeline")
assert cmd[idx + 1] == flow_id
def test_default_flow_is_api_ods_dwd(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"])
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--pipeline")
assert cmd[idx + 1] == "api_ods_dwd"
# ---------------------------------------------------------------------------
# 3 种处理模式
# ---------------------------------------------------------------------------
class TestProcessingModes:
@pytest.mark.parametrize("mode", sorted(VALID_PROCESSING_MODES))
def test_all_modes_accepted(self, builder: CLIBuilder, mode: str):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], processing_mode=mode)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--processing-mode")
assert cmd[idx + 1] == mode
def test_fetch_before_verify_only_in_verify_mode(self, builder: CLIBuilder):
"""--fetch-before-verify 仅在 verify_only 模式下生效"""
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
processing_mode="verify_only",
fetch_before_verify=True,
)
cmd = builder.build_command(config, ETL_PATH)
assert "--fetch-before-verify" in cmd
def test_fetch_before_verify_ignored_in_increment_mode(self, builder: CLIBuilder):
"""increment_only 模式下 fetch_before_verify=True 不应生成参数"""
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
processing_mode="increment_only",
fetch_before_verify=True,
)
cmd = builder.build_command(config, ETL_PATH)
assert "--fetch-before-verify" not in cmd
# ---------------------------------------------------------------------------
# 时间窗口
# ---------------------------------------------------------------------------
class TestTimeWindow:
def test_lookback_mode_generates_lookback_args(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
window_mode="lookback",
lookback_hours=48,
overlap_seconds=1200,
)
cmd = builder.build_command(config, ETL_PATH)
idx_lb = cmd.index("--lookback-hours")
assert cmd[idx_lb + 1] == "48"
idx_ol = cmd.index("--overlap-seconds")
assert cmd[idx_ol + 1] == "1200"
# lookback 模式不应生成 --window-start / --window-end
assert "--window-start" not in cmd
assert "--window-end" not in cmd
def test_custom_mode_generates_window_args(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
window_mode="custom",
window_start="2026-01-01",
window_end="2026-01-31",
)
cmd = builder.build_command(config, ETL_PATH)
idx_s = cmd.index("--window-start")
assert cmd[idx_s + 1] == "2026-01-01"
idx_e = cmd.index("--window-end")
assert cmd[idx_e + 1] == "2026-01-31"
# custom 模式不应生成 --lookback-hours / --overlap-seconds
assert "--lookback-hours" not in cmd
assert "--overlap-seconds" not in cmd
def test_window_split_with_days(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
window_split="day",
window_split_days=10,
)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--window-split")
assert cmd[idx + 1] == "day"
idx_d = cmd.index("--window-split-days")
assert cmd[idx_d + 1] == "10"
def test_window_split_none_not_generated(self, builder: CLIBuilder):
"""window_split='none' 不应生成 --window-split 参数"""
config = TaskConfigSchema(tasks=["ODS_MEMBER"], window_split="none")
cmd = builder.build_command(config, ETL_PATH)
assert "--window-split" not in cmd
# ---------------------------------------------------------------------------
# store_id 自动注入
# ---------------------------------------------------------------------------
class TestStoreId:
def test_store_id_injected(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], store_id=42)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--store-id")
assert cmd[idx + 1] == "42"
def test_store_id_none_not_generated(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], store_id=None)
cmd = builder.build_command(config, ETL_PATH)
assert "--store-id" not in cmd
# ---------------------------------------------------------------------------
# dry_run
# ---------------------------------------------------------------------------
class TestDryRun:
def test_dry_run_flag(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], dry_run=True)
cmd = builder.build_command(config, ETL_PATH)
assert "--dry-run" in cmd
def test_no_dry_run_flag(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"], dry_run=False)
cmd = builder.build_command(config, ETL_PATH)
assert "--dry-run" not in cmd
# ---------------------------------------------------------------------------
# extra_args
# ---------------------------------------------------------------------------
class TestExtraArgs:
def test_supported_value_arg(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"pg_dsn": "postgresql://localhost/test"},
)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--pg-dsn")
assert cmd[idx + 1] == "postgresql://localhost/test"
def test_supported_bool_arg(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"force_window_override": True},
)
cmd = builder.build_command(config, ETL_PATH)
assert "--force-window-override" in cmd
def test_unsupported_arg_ignored(self, builder: CLIBuilder):
"""不在 CLI_SUPPORTED_ARGS 中的键应被忽略"""
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"unknown_param": "value"},
)
cmd = builder.build_command(config, ETL_PATH)
assert "--unknown-param" not in cmd
def test_none_value_ignored(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"pg_dsn": None},
)
cmd = builder.build_command(config, ETL_PATH)
assert "--pg-dsn" not in cmd
def test_false_bool_arg_not_generated(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"force_window_override": False},
)
cmd = builder.build_command(config, ETL_PATH)
assert "--force-window-override" not in cmd
# ---------------------------------------------------------------------------
# build_command_string
# ---------------------------------------------------------------------------
class TestBuildCommandString:
def test_returns_string(self, builder: CLIBuilder):
config = TaskConfigSchema(tasks=["ODS_MEMBER"])
result = builder.build_command_string(config, ETL_PATH)
assert isinstance(result, str)
assert "python -m cli.main" in result
def test_quotes_args_with_spaces(self, builder: CLIBuilder):
config = TaskConfigSchema(
tasks=["ODS_MEMBER"],
extra_args={"pg_dsn": "host=localhost dbname=test"},
)
result = builder.build_command_string(config, ETL_PATH)
# 包含空格的值应被引号包裹
assert '"host=localhost dbname=test"' in result