"""测试 AstrbotPaths 路径类的综合测试.""" from __future__ import annotations import os import tempfile from pathlib import Path from typing import TYPE_CHECKING import pytest from astrbot.base.paths import AstrbotPaths if TYPE_CHECKING: from collections.abc import Generator @pytest.fixture def temp_root(monkeypatch: pytest.MonkeyPatch) -> Generator[Path]: """创建一个临时根目录用于测试.""" with tempfile.TemporaryDirectory() as tmpdir: temp_path = Path(tmpdir) monkeypatch.setenv("ASTRBOT_ROOT", str(temp_path)) # 清除类变量和实例缓存 AstrbotPaths._instances.clear() # 重新加载环境变量 from dotenv import load_dotenv load_dotenv(override=True) AstrbotPaths.astrbot_root = temp_path yield temp_path # 清理 AstrbotPaths._instances.clear() @pytest.fixture def paths_instance(temp_root: Path) -> AstrbotPaths: """创建一个 AstrbotPaths 实例用于测试.""" return AstrbotPaths.getPaths("test-module") class TestAstrbotPathsInit: """测试 AstrbotPaths 初始化.""" def test_init_creates_root_directory(self, temp_root: Path) -> None: """测试初始化时创建根目录.""" # 删除根目录以测试自动创建 if temp_root.exists(): import shutil shutil.rmtree(temp_root) AstrbotPaths("test-init") assert temp_root.exists() assert temp_root.is_dir() def test_init_with_name(self, temp_root: Path) -> None: """测试使用名称初始化.""" paths = AstrbotPaths("my-module") assert paths.name == "my-module" def test_astrbot_root_from_env( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """测试从环境变量读取根目录.""" custom_root = tmp_path / "custom_root" custom_root.mkdir(parents=True, exist_ok=True) # 清除实例缓存 AstrbotPaths._instances.clear() # 直接设置环境变量(在 load_dotenv 之前) monkeypatch.setenv("ASTRBOT_ROOT", str(custom_root)) # 直接更新 astrbot_root(模拟 load_dotenv 的效果但使用我们设置的环境变量) AstrbotPaths.astrbot_root = Path( os.getenv("ASTRBOT_ROOT", Path.home() / ".astrbot") ).absolute() assert AstrbotPaths.astrbot_root == custom_root.absolute() def test_astrbot_root_default( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """测试默认根目录.""" # 清除环境变量 monkeypatch.delenv("ASTRBOT_ROOT", raising=False) # 清除任何可能存在的 .env 文件影响 monkeypatch.setattr("os.environ", {**os.environ}) # 清除实例缓存 AstrbotPaths._instances.clear() # 重新计算根目录 AstrbotPaths.astrbot_root = Path( os.getenv("ASTRBOT_ROOT", Path.home() / ".astrbot") ).absolute() expected = (Path.home() / ".astrbot").absolute() assert AstrbotPaths.astrbot_root == expected class TestGetPaths: """测试 getPaths 单例模式.""" def test_get_paths_returns_same_instance(self, temp_root: Path) -> None: """测试多次调用返回同一个实例.""" paths1 = AstrbotPaths.getPaths("test-module") paths2 = AstrbotPaths.getPaths("test-module") assert paths1 is paths2 def test_get_paths_different_names(self, temp_root: Path) -> None: """测试不同名称返回不同实例.""" paths1 = AstrbotPaths.getPaths("module-a") paths2 = AstrbotPaths.getPaths("module-b") assert paths1 is not paths2 assert paths1.name == "module-a" assert paths2.name == "module-b" def test_get_paths_normalizes_name(self, temp_root: Path) -> None: """测试名称规范化.""" # PEP 503 规范化: 转小写, 替换 -, _, . paths1 = AstrbotPaths.getPaths("Test_Module") paths2 = AstrbotPaths.getPaths("test-module") paths3 = AstrbotPaths.getPaths("TEST.MODULE") # 所有这些名称应该被规范化为相同的名称 assert paths1 is paths2 assert paths2 is paths3 class TestProperties: """测试所有属性访问器.""" def test_root_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None: """测试 root 属性.""" assert paths_instance.root == temp_root assert paths_instance.root.exists() def test_root_property_when_not_exists( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """测试 root 属性当根目录不存在时.""" non_existent = tmp_path / "non_existent_path" # 确保目录不存在 if non_existent.exists(): import shutil shutil.rmtree(non_existent) # 清除实例缓存 AstrbotPaths._instances.clear() # 设置不存在的路径 AstrbotPaths.astrbot_root = non_existent # __init__ 会创建根目录,所以 getPaths 会使根目录存在 # 我们测试的是在 __init__ 创建目录之前访问 root 属性的行为 # 但由于 getPaths 总是调用 __init__,目录总是会被创建 # 所以这个测试应该验证即使最初不存在,getPaths 之后也会存在 paths = AstrbotPaths.getPaths("test") # getPaths 调用 __init__,__init__ 会创建根目录 # 所以 root 应该返回 astrbot_root(现在已存在) assert paths.root == non_existent assert non_existent.exists() def test_root_property_fallback_to_cwd( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """测试 root 属性在根目录被删除后回退到 cwd/.astrbot.""" import shutil # 创建并设置一个根目录 temp_root = tmp_path / "test_root" temp_root.mkdir(parents=True, exist_ok=True) # 清除实例缓存 AstrbotPaths._instances.clear() AstrbotPaths.astrbot_root = temp_root # 创建实例 paths = AstrbotPaths.getPaths("test-fallback") # 删除根目录(模拟被外部删除的情况) shutil.rmtree(temp_root) # 现在访问 root 应该回退到 cwd/.astrbot expected = Path.cwd() / ".astrbot" assert paths.root == expected def test_home_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None: """测试 home 属性.""" home_path = paths_instance.home expected = temp_root / "home" / paths_instance.name assert home_path == expected assert home_path.exists() assert home_path.is_dir() def test_config_property( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 config 属性.""" config_path = paths_instance.config expected = temp_root / "config" / paths_instance.name assert config_path == expected assert config_path.exists() assert config_path.is_dir() def test_data_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None: """测试 data 属性.""" data_path = paths_instance.data expected = temp_root / "data" / paths_instance.name assert data_path == expected assert data_path.exists() assert data_path.is_dir() def test_log_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None: """测试 log 属性.""" log_path = paths_instance.log expected = temp_root / "logs" / paths_instance.name assert log_path == expected assert log_path.exists() assert log_path.is_dir() def test_temp_property(self, paths_instance: AstrbotPaths, temp_root: Path) -> None: """测试 temp 属性.""" temp_path = paths_instance.temp expected = temp_root / "temp" / paths_instance.name assert temp_path == expected assert temp_path.exists() assert temp_path.is_dir() def test_plugins_property( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 plugins 属性.""" plugins_path = paths_instance.plugins expected = temp_root / "plugins" / paths_instance.name assert plugins_path == expected assert plugins_path.exists() assert plugins_path.is_dir() def test_properties_create_nested_directories( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试属性访问时创建嵌套目录.""" # 清空目录 import shutil if temp_root.exists(): for item in temp_root.iterdir(): if item.is_dir(): shutil.rmtree(item) else: item.unlink() # 访问所有属性 _ = paths_instance.home _ = paths_instance.config _ = paths_instance.data _ = paths_instance.log _ = paths_instance.temp _ = paths_instance.plugins # 验证所有目录都已创建 assert (temp_root / "home" / paths_instance.name).exists() assert (temp_root / "config" / paths_instance.name).exists() assert (temp_root / "data" / paths_instance.name).exists() assert (temp_root / "logs" / paths_instance.name).exists() assert (temp_root / "temp" / paths_instance.name).exists() assert (temp_root / "plugins" / paths_instance.name).exists() class TestIsRoot: """测试 is_root 类方法.""" def test_is_root_with_marker_file(self, temp_root: Path) -> None: """测试带有标记文件的根目录识别.""" marker_file = temp_root / ".astrbot" marker_file.touch() assert AstrbotPaths.is_root(temp_root) is True def test_is_root_without_marker_file(self, temp_root: Path) -> None: """测试没有标记文件的目录.""" marker_file = temp_root / ".astrbot" if marker_file.exists(): marker_file.unlink() assert AstrbotPaths.is_root(temp_root) is False def test_is_root_with_non_existent_path(self) -> None: """测试不存在的路径.""" non_existent = Path("/definitely/not/exist/path") assert AstrbotPaths.is_root(non_existent) is False def test_is_root_with_file_not_directory(self, temp_root: Path) -> None: """测试路径是文件而非目录.""" test_file = temp_root / "test.txt" test_file.touch() assert AstrbotPaths.is_root(test_file) is False class TestReload: """测试 reload 方法.""" def test_reload_updates_root( self, temp_root: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """测试 reload 更新根目录.""" paths = AstrbotPaths.getPaths("test-reload") # 修改环境变量 new_root = temp_root / "new_root" new_root.mkdir(parents=True, exist_ok=True) monkeypatch.setenv("ASTRBOT_ROOT", str(new_root)) # 重新加载 paths.reload() # 验证根目录已更新 assert AstrbotPaths.astrbot_root == new_root.absolute() def test_reload_clears_old_env( self, temp_root: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """测试 reload 在环境变量被删除后使用默认值.""" paths = AstrbotPaths.getPaths("test-reload-default") # 删除环境变量 monkeypatch.delenv("ASTRBOT_ROOT", raising=False) # 重新加载 paths.reload() # 应该使用默认值 (Path.home() / ".astrbot").absolute() # 由于 .env 文件可能存在,实际结果可能不变 # 所以我们只验证 reload 没有抛出异常 assert AstrbotPaths.astrbot_root is not None assert isinstance(AstrbotPaths.astrbot_root, Path) class TestChdir: """测试 chdir 上下文管理器.""" def test_chdir_changes_directory( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 chdir 切换目录.""" original_cwd = Path.cwd() # 创建目标目录 target_path = temp_root / "home" target_path.mkdir(parents=True, exist_ok=True) with paths_instance.chdir("home") as target_dir: current_cwd = Path.cwd() expected_dir = temp_root / "home" assert current_cwd == expected_dir assert target_dir == expected_dir # 验证已恢复原目录 assert Path.cwd() == original_cwd def test_chdir_restores_on_exception( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 chdir 在异常时恢复原目录.""" original_cwd = Path.cwd() # 创建目标目录 target_path = temp_root / "home" target_path.mkdir(parents=True, exist_ok=True) with pytest.raises(ValueError): with paths_instance.chdir("home"): raise ValueError("Test exception") # 验证已恢复原目录 assert Path.cwd() == original_cwd def test_chdir_with_different_subdirectories( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 chdir 使用不同的子目录.""" original_cwd = Path.cwd() # 创建测试目录 test_dir = temp_root / "test_subdir" test_dir.mkdir(parents=True, exist_ok=True) with paths_instance.chdir("test_subdir") as target_dir: assert Path.cwd() == test_dir assert target_dir == test_dir assert Path.cwd() == original_cwd class TestAchdir: """测试 achdir 异步上下文管理器.""" @pytest.mark.asyncio async def test_achdir_changes_directory( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 achdir 异步切换目录.""" original_cwd = Path.cwd() # 创建目标目录 target_path = temp_root / "home" target_path.mkdir(parents=True, exist_ok=True) async with paths_instance.achdir("home") as target_dir: current_cwd = Path.cwd() expected_dir = temp_root / "home" assert current_cwd == expected_dir assert target_dir == expected_dir # 验证已恢复原目录 assert Path.cwd() == original_cwd @pytest.mark.asyncio async def test_achdir_restores_on_exception( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 achdir 在异常时恢复原目录.""" original_cwd = Path.cwd() # 创建目标目录 target_path = temp_root / "home" target_path.mkdir(parents=True, exist_ok=True) with pytest.raises(ValueError): async with paths_instance.achdir("home"): raise ValueError("Test exception") # 验证已恢复原目录 assert Path.cwd() == original_cwd @pytest.mark.asyncio async def test_achdir_with_different_subdirectories( self, paths_instance: AstrbotPaths, temp_root: Path ) -> None: """测试 achdir 使用不同的子目录.""" original_cwd = Path.cwd() # 创建测试目录 test_dir = temp_root / "async_test_subdir" test_dir.mkdir(parents=True, exist_ok=True) async with paths_instance.achdir("async_test_subdir") as target_dir: assert Path.cwd() == test_dir assert target_dir == test_dir assert Path.cwd() == original_cwd class TestIntegration: """集成测试.""" def test_multiple_modules_isolated(self, temp_root: Path) -> None: """测试多个模块之间的隔离.""" module_a = AstrbotPaths.getPaths("module-a") module_b = AstrbotPaths.getPaths("module-b") # 访问各自的 home 目录 home_a = module_a.home home_b = module_b.home # 验证目录不同 assert home_a != home_b assert home_a == temp_root / "home" / "module-a" assert home_b == temp_root / "home" / "module-b" # 验证都存在 assert home_a.exists() assert home_b.exists() def test_full_workflow(self, temp_root: Path) -> None: """测试完整工作流.""" # 创建一个模块 module = AstrbotPaths.getPaths("my-plugin") # 创建各种文件 config_file = module.config / "settings.json" config_file.write_text('{"key": "value"}') data_file = module.data / "data.txt" data_file.write_text("some data") log_file = module.log / "app.log" log_file.write_text("log entry") # 验证文件存在 assert config_file.exists() assert data_file.exists() assert log_file.exists() # 验证内容 assert config_file.read_text() == '{"key": "value"}' assert data_file.read_text() == "some data" assert log_file.read_text() == "log entry" def test_singleton_pattern_thread_safe(self, temp_root: Path) -> None: """测试单例模式的基本行为(注意:不是真正的线程安全测试).""" instances = [] for _ in range(10): instances.append(AstrbotPaths.getPaths("singleton-test")) # 所有实例应该是同一个对象 first = instances[0] for instance in instances[1:]: assert instance is first