"""src/wov_app/config.py 的模块级测试(数据 → 测试过程 → 验证结果)。 被测模块:`src/wov_app/config.py`(环境变量读取与路径推导),可独立调用。 由于该模块在**导入时**锁定路径常量,隔离用例在子进程中运行(真实导入路径 + 干净环境),其余用例校验默认值与类型。 注意:config 的路径常量在进程内已固化,因此"环境变量覆盖"必须用子进程验证, 否则测的是缓存值而非真实行为(这也是规则要求"可独立运行、不依赖外部配置" 的具体体现)。 """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import pytest from wov_app import config # 仓库根目录(config.WORKSPACE_ROOT 应该指向它)。 WORKSPACE = Path(__file__).resolve().parents[3] # 与路径相关的环境变量:子进程验证前必须清除,否则会继承外层测试隔离值。 _PATH_ENV_KEYS = ( "WOV_DATA_DIR", "WOV_DB_PATH", "WOV_STORAGE_DIR", "WOV_SCHEDULER_INTERVAL_SECONDS", "WOV_BATCH_INTERVAL_SECONDS", "WOV_CLEANUP_INTERVAL_SECONDS", "WOV_CLEANUP_GRACE_SECONDS", "WOV_BATCH_ENABLED", "WOV_CLEANUP_ENABLED", ) def _run_in_subprocess(code: str, env: dict[str, str] | None = None) -> dict: """在干净子进程中导入 config 并返回指定变量(真实进程隔离)。 先清除全部相关环境变量,再用 env 注入本次用例的值,保证测到的是 config 的真实解析逻辑而不是外层测试运行环境。 """ script = ( "import json\n" "from wov_app import config\n" f"{code}\n" "print(json.dumps(result))\n" ) clean_env = {k: v for k, v in __import__("os").environ.items() if k not in _PATH_ENV_KEYS} clean_env.update(env or {}) result = subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, cwd=str(WORKSPACE), env=clean_env, ) assert result.returncode == 0, result.stderr return json.loads(result.stdout.strip().splitlines()[-1]) # --------------------------------------------------------------------------- # 默认值(当前进程内已导入的常量) # --------------------------------------------------------------------------- def test_workspace_root_points_to_repo_root() -> None: """WORKSPACE_ROOT 指向仓库根(用于推导其余路径)。""" # 数据:模块常量。 # 测试过程与验证结果 assert config.WORKSPACE_ROOT == WORKSPACE assert (config.WORKSPACE_ROOT / "pyproject.toml").is_file() def test_default_paths_are_under_data_dir() -> None: """无环境变量时默认路径为 <仓库根>/data 下的推导值(子进程验证真实默认)。""" # 数据:清空全部相关环境变量。 # 测试过程 result = _run_in_subprocess( "result = {'root': str(config.WORKSPACE_ROOT), 'data': str(config.DATA_DIR), " "'db': str(config.DB_PATH), 'storage': str(config.STORAGE_DIR), " "'batch': config.BATCH_ENABLED, 'cleanup': config.CLEANUP_ENABLED}" ) # 验证结果:默认 data 目录在仓库根下,db/storage 由它推导,开关默认开。 assert result["root"] == str(WORKSPACE) assert result["data"] == str(WORKSPACE / "data") assert result["db"] == str(WORKSPACE / "data" / "wov.db") assert result["storage"] == str(WORKSPACE / "data" / "storage") assert result["batch"] is True assert result["cleanup"] is True def test_numeric_settings_are_floats() -> None: """数值型配置被解析为 float(避免字符串参与算术)。""" # 数据:模块常量。 # 测试过程与验证结果 for value in ( config.SCHEDULER_INTERVAL_SECONDS, config.BATCH_INTERVAL_SECONDS, config.CLEANUP_INTERVAL_SECONDS, config.CLEANUP_GRACE_SECONDS, ): assert isinstance(value, float) assert value > 0 def test_boolean_settings_are_bool() -> None: """布尔型开关被解析为 bool,默认全部开启(1)。""" # 数据:模块常量(测试运行环境由隔离层设为 0,故仅校验类型)。 # 测试过程与验证结果 for value in (config.BATCH_ENABLED, config.CLEANUP_ENABLED): assert isinstance(value, bool) # --------------------------------------------------------------------------- # 环境变量覆盖(子进程真实导入) # --------------------------------------------------------------------------- def test_data_dir_env_override_changes_all_derived_paths(tmp_path: Path) -> None: """WOV_DATA_DIR 覆盖后,DB_PATH 与 STORAGE_DIR 随之推导(路径联动)。""" # 数据:自定义数据目录。 custom = tmp_path / "custom-data" # 测试过程 result = _run_in_subprocess( "result = {'data': str(config.DATA_DIR), 'db': str(config.DB_PATH), " "'storage': str(config.STORAGE_DIR)}", env={"WOV_DATA_DIR": str(custom)}, ) # 验证结果 assert result["data"] == str(custom) assert result["db"] == str(custom / "wov.db") assert result["storage"] == str(custom / "storage") def test_explicit_db_and_storage_env_take_precedence(tmp_path: Path) -> None: """WOV_DB_PATH / WOV_STORAGE_DIR 可独立覆盖(不跟随 DATA_DIR)。""" # 数据:分别指定的数据库与存储路径。 db_path = tmp_path / "x" / "custom.db" storage = tmp_path / "y" / "store" # 测试过程 result = _run_in_subprocess( "result = {'db': str(config.DB_PATH), 'storage': str(config.STORAGE_DIR)}", env={"WOV_DB_PATH": str(db_path), "WOV_STORAGE_DIR": str(storage)}, ) # 验证结果 assert result["db"] == str(db_path) assert result["storage"] == str(storage) def test_boolean_env_parsing() -> None: """开关型环境变量:'1' 为 True,'0' 为 False。""" # 数据:批量与清理开关分别置 1 与 0。 # 测试过程 result = _run_in_subprocess( "result = {'batch': config.BATCH_ENABLED, 'cleanup': config.CLEANUP_ENABLED}", env={"WOV_BATCH_ENABLED": "1", "WOV_CLEANUP_ENABLED": "0"}, ) # 验证结果 assert result["batch"] is True assert result["cleanup"] is False def test_interval_env_parsing() -> None: """轮询间隔环境变量被解析为对应浮点值。""" # 数据:指定的调度与批量间隔。 # 测试过程 result = _run_in_subprocess( "result = {'sched': config.SCHEDULER_INTERVAL_SECONDS, " "'batch': config.BATCH_INTERVAL_SECONDS}", env={"WOV_SCHEDULER_INTERVAL_SECONDS": "2.5", "WOV_BATCH_INTERVAL_SECONDS": "0.25"}, ) # 验证结果 assert result["sched"] == 2.5 assert result["batch"] == 0.25 def test_paths_are_pathlib_objects(tmp_path: Path) -> None: """路径配置是 pathlib.Path(跨平台,不写死 Windows 盘符)。""" # 数据:自定义数据目录。 # 测试过程 result = _run_in_subprocess( "result = {'is_path': isinstance(config.DATA_DIR, __import__('pathlib').Path)}", env={"WOV_DATA_DIR": str(tmp_path / "d")}, ) # 验证结果 assert result["is_path"] is True