"""``pyflowx.registry`` 模块的单元测试.""" from __future__ import annotations from typing import Any import pytest import pyflowx as px from pyflowx.registry import _REGISTRY, FnRegistry, get_fn, has_fn, register_fn @pytest.fixture def clear_registry() -> Any: """每个测试前后清空 registry, 但保留 _ops 模块自动注册的函数. _ops 模块在首次导入时注册函数, 模块不会重新执行, 因此清空后无法重新注册. 保存原始状态并在 teardown 时恢复, 确保 TestOpsModules 等不使用此 fixture 的 测试仍能看到 _ops 的注册. """ saved = dict(_REGISTRY) _REGISTRY.clear() yield _REGISTRY.clear() _REGISTRY.update(saved) class TestRegisterFn: """``register_fn`` 装饰器测试.""" @pytest.fixture(autouse=True) def _clear(self, clear_registry: Any) -> None: """每个测试前后清空 registry.""" def test_register_with_explicit_name(self) -> None: """使用显式名称注册函数.""" @register_fn("custom_name") def _func(x: int) -> int: return x * 2 assert has_fn("custom_name") assert get_fn("custom_name")(5) == 10 def test_register_without_parentheses(self) -> None: """无括号使用 ``@register_fn`` 时以 ``__name__`` 注册.""" @register_fn def my_func(x: int) -> int: return x + 1 assert has_fn("my_func") assert get_fn("my_func")(10) == 11 def test_register_with_none_uses_func_name(self) -> None: """``@register_fn(None)`` 等价于使用 ``__name__``.""" @register_fn(None) def auto_named(x: int) -> int: return x assert has_fn("auto_named") def test_register_preserves_function(self) -> None: """装饰后函数仍可直接调用.""" @register_fn("calc") def _calc(a: int, b: int) -> int: return a + b assert _calc(2, 3) == 5 def test_duplicate_registration_raises(self) -> None: """重复注册同名函数抛出 ValueError.""" with pytest.raises(ValueError, match="已注册"): @register_fn("dup") def _first() -> None: pass @register_fn("dup") def _second() -> None: pass def test_duplicate_registration_without_parentheses(self) -> None: """无括号模式下重复注册也抛出 ValueError.""" with pytest.raises(ValueError, match="已注册"): @register_fn def shared_name() -> None: pass @register_fn def shared_name() -> None: # noqa: F811 pass class TestGetFn: """``get_fn`` 函数测试.""" @pytest.fixture(autouse=True) def _clear(self, clear_registry: Any) -> None: """每个测试前后清空 registry.""" def test_get_registered_function(self) -> None: """获取已注册的函数.""" @register_fn("target") def _target(x: int) -> int: return x * 3 retrieved = get_fn("target") assert retrieved is _target def test_get_unregistered_raises_keyerror(self) -> None: """获取未注册的函数抛出 KeyError.""" with pytest.raises(KeyError, match="not_found"): get_fn("not_found") class TestHasFn: """``has_fn`` 函数测试.""" @pytest.fixture(autouse=True) def _clear(self, clear_registry: Any) -> None: """每个测试前后清空 registry.""" def test_has_registered(self) -> None: """已注册返回 True.""" @register_fn("exists") def _exists() -> None: pass assert has_fn("exists") is True def test_has_not_registered(self) -> None: """未注册返回 False.""" assert has_fn("nope") is False class TestFnRegistry: """``FnRegistry`` 类测试.""" @pytest.fixture(autouse=True) def _clear(self, clear_registry: Any) -> None: """每个测试前后清空 registry.""" def test_register_method(self) -> None: """``FnRegistry.register`` 等价于 ``register_fn``.""" @FnRegistry.register("via_class") def _func(x: int) -> int: return x assert FnRegistry.has("via_class") assert FnRegistry.get("via_class")(7) == 7 def test_register_method_without_name(self) -> None: """``FnRegistry.register(None)`` 使用函数名.""" @FnRegistry.register() def class_auto(x: int) -> int: return x assert FnRegistry.has("class_auto") def test_get_method(self) -> None: """``FnRegistry.get`` 获取已注册函数.""" @register_fn("klass") def _klass() -> str: return "hello" assert FnRegistry.get("klass")() == "hello" def test_has_method_false(self) -> None: """``FnRegistry.has`` 未注册返回 False.""" assert FnRegistry.has("missing") is False def test_clear_method(self) -> None: """``FnRegistry.clear`` 清空注册表.""" @register_fn("temp") def _temp() -> None: pass assert FnRegistry.has("temp") FnRegistry.clear() assert not FnRegistry.has("temp") def test_names_method(self) -> None: """``FnRegistry.names`` 返回所有注册名.""" @register_fn("alpha") def _alpha() -> None: pass @register_fn("beta") def _beta() -> None: pass names = FnRegistry.names() assert "alpha" in names assert "beta" in names def test_names_returns_copy(self) -> None: """``names`` 返回列表副本, 修改不影响内部状态.""" @register_fn("orig") def _orig() -> None: pass names = FnRegistry.names() names.append("fake") assert not FnRegistry.has("fake") class TestOpsModules: """``_ops`` 子模块导入后函数自动注册的集成测试. 这些测试不使用 ``_clear_registry`` fixture, 因为 ``_ops`` 模块在首次导入时 注册函数, 清空后将无法重新注册 (模块不会再次执行). """ def test_all_ops_functions_registered(self) -> None: """导入 ``_ops`` 后所有函数应已注册.""" import inspect from pyflowx.cli._ops import dev, files, media, system for module in (files, dev, media, system): for name in module.__all__: obj = getattr(module, name) if not inspect.isfunction(obj): continue assert px.has_fn(name), f"{module.__name__}.{name} 未注册" def test_total_function_count(self) -> None: """注册函数总数 = 18+15+18+11 = 62.""" from pyflowx.cli._ops import dev, files, media, system # noqa: F401 all_names = px.FnRegistry.names() assert len(all_names) == 62 def test_specific_functions_callable(self) -> None: """关键注册函数可调用.""" from pyflowx.cli._ops import dev, files, media, system assert px.get_fn("get_file_timestamp") is files.get_file_timestamp assert px.get_fn("bump_file_version") is dev.bump_file_version assert px.get_fn("pdf_merge") is media.pdf_merge assert px.get_fn("ssh_copy_id") is system.ssh_copy_id