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

260 lines
9.9 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 -*-
"""CLIBuilder 单元测试
覆盖7 种 Flow、4 种处理模式、时间窗口、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 --flow ... --processing-mode ..."""
config = TaskConfigSchema(tasks=["ODS_MEMBER"])
cmd = builder.build_command(config, ETL_PATH)
assert cmd[:3] == ["python", "-m", "cli.main"]
assert "--flow" 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"], flow=flow_id)
cmd = builder.build_command(config, ETL_PATH)
idx = cmd.index("--flow")
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("--flow")
assert cmd[idx + 1] == "api_ods_dwd"
# ---------------------------------------------------------------------------
# 4 种处理模式
# ---------------------------------------------------------------------------
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