mirror of
https://github.com/hanzoui/pyisolate.git
synced 2026-10-30 20:37:17 +00:00
Drop Python 3.9, add bubblewrap to workflows, apply ruff formatting. Was running `ruff` and not the additional `ruff-format`. Installed pre-commit hooks to prevent any future non-compliant commits.
240 lines
7.9 KiB
Python
240 lines
7.9 KiB
Python
"""Tests for IsolationAdapter protocol compliance.
|
|
|
|
These tests verify that adapters implementing IsolationAdapter
|
|
behave correctly according to the protocol contract. They test
|
|
at the boundary (adapter interface), not internal implementation.
|
|
|
|
The MockHostAdapter from fixtures serves as the reference implementation
|
|
and is used to demonstrate expected behavior for each protocol method.
|
|
"""
|
|
|
|
from pyisolate._internal.rpc_protocol import ProxiedSingleton
|
|
from pyisolate._internal.serialization_registry import SerializerRegistry
|
|
from pyisolate.interfaces import IsolationAdapter
|
|
|
|
from .fixtures.test_adapter import MockHostAdapter, MockRegistry, MockTestData
|
|
|
|
|
|
class TestAdapterIdentifier:
|
|
"""Tests for the identifier property."""
|
|
|
|
def test_adapter_has_identifier(self):
|
|
"""Adapter must have a non-empty identifier."""
|
|
adapter = MockHostAdapter()
|
|
assert adapter.identifier
|
|
assert isinstance(adapter.identifier, str)
|
|
|
|
def test_identifier_is_lowercase(self):
|
|
"""Identifier should be lowercase for consistency."""
|
|
adapter = MockHostAdapter()
|
|
assert adapter.identifier == adapter.identifier.lower()
|
|
|
|
def test_identifier_no_spaces(self):
|
|
"""Identifier should not contain spaces."""
|
|
adapter = MockHostAdapter()
|
|
assert " " not in adapter.identifier
|
|
|
|
|
|
class TestAdapterPathConfig:
|
|
"""Tests for get_path_config method."""
|
|
|
|
def test_get_path_config_returns_dict(self):
|
|
"""get_path_config must return dict with required keys."""
|
|
adapter = MockHostAdapter()
|
|
config = adapter.get_path_config("/some/module/__init__.py")
|
|
|
|
assert isinstance(config, dict)
|
|
assert "preferred_root" in config
|
|
assert "additional_paths" in config
|
|
|
|
def test_preferred_root_is_string(self):
|
|
"""preferred_root must be a string path."""
|
|
adapter = MockHostAdapter("/tmp/myapp")
|
|
config = adapter.get_path_config("/tmp/myapp/ext/__init__.py")
|
|
|
|
assert isinstance(config["preferred_root"], str)
|
|
assert config["preferred_root"] == "/tmp/myapp"
|
|
|
|
def test_additional_paths_is_list(self):
|
|
"""additional_paths must be a list of strings."""
|
|
adapter = MockHostAdapter()
|
|
config = adapter.get_path_config("/some/path")
|
|
|
|
assert isinstance(config["additional_paths"], list)
|
|
for path in config["additional_paths"]:
|
|
assert isinstance(path, str)
|
|
|
|
|
|
class TestAdapterSerializers:
|
|
"""Tests for register_serializers method."""
|
|
|
|
def test_register_serializers_accepts_registry(self):
|
|
"""register_serializers must accept SerializerRegistryProtocol."""
|
|
adapter = MockHostAdapter()
|
|
registry = SerializerRegistry.get_instance()
|
|
registry.clear() # Start fresh
|
|
|
|
# Should not raise
|
|
adapter.register_serializers(registry)
|
|
|
|
def test_registered_serializer_is_callable(self):
|
|
"""Registered serializers must be callable."""
|
|
adapter = MockHostAdapter()
|
|
registry = SerializerRegistry.get_instance()
|
|
registry.clear()
|
|
|
|
adapter.register_serializers(registry)
|
|
|
|
serializer = registry.get_serializer("MockTestData")
|
|
assert serializer is not None
|
|
assert callable(serializer)
|
|
|
|
def test_serializer_produces_json_compatible(self):
|
|
"""Serializer output must be JSON-compatible."""
|
|
import json
|
|
|
|
adapter = MockHostAdapter()
|
|
registry = SerializerRegistry.get_instance()
|
|
registry.clear()
|
|
|
|
adapter.register_serializers(registry)
|
|
|
|
serializer = registry.get_serializer("MockTestData")
|
|
test_obj = MockTestData("hello")
|
|
result = serializer(test_obj)
|
|
|
|
# Must be JSON serializable
|
|
json_str = json.dumps(result)
|
|
assert json_str
|
|
|
|
|
|
class TestAdapterRpcServices:
|
|
"""Tests for provide_rpc_services method."""
|
|
|
|
def test_provide_rpc_services_returns_list(self):
|
|
"""provide_rpc_services must return a list."""
|
|
adapter = MockHostAdapter()
|
|
services = adapter.provide_rpc_services()
|
|
|
|
assert isinstance(services, list)
|
|
|
|
def test_services_are_proxied_singleton_subclasses(self):
|
|
"""Each service must be a ProxiedSingleton subclass."""
|
|
adapter = MockHostAdapter()
|
|
services = adapter.provide_rpc_services()
|
|
|
|
for svc in services:
|
|
assert isinstance(svc, type), f"{svc} is not a class"
|
|
assert issubclass(svc, ProxiedSingleton), f"{svc} is not a ProxiedSingleton"
|
|
|
|
def test_services_are_instantiable(self):
|
|
"""Each service class must be instantiable with no args."""
|
|
adapter = MockHostAdapter()
|
|
services = adapter.provide_rpc_services()
|
|
|
|
for svc_cls in services:
|
|
# Should not raise
|
|
instance = svc_cls()
|
|
assert instance is not None
|
|
|
|
|
|
class TestAdapterApiRegistration:
|
|
"""Tests for handle_api_registration method."""
|
|
|
|
def test_handle_api_registration_accepts_args(self):
|
|
"""handle_api_registration must accept api and rpc args."""
|
|
adapter = MockHostAdapter()
|
|
|
|
# Create mock api and rpc
|
|
api = MockRegistry()
|
|
rpc = None # Could be a mock, but we just test it accepts the arg
|
|
|
|
# Should not raise
|
|
adapter.handle_api_registration(api, rpc)
|
|
|
|
|
|
class TestAdapterProtocolCompliance:
|
|
"""Tests that verify full protocol compliance."""
|
|
|
|
def test_adapter_implements_protocol(self):
|
|
"""MockHostAdapter must implement IsolationAdapter protocol."""
|
|
adapter = MockHostAdapter()
|
|
|
|
# Protocol check (structural typing)
|
|
assert isinstance(adapter, IsolationAdapter)
|
|
|
|
def test_adapter_is_runtime_checkable(self):
|
|
"""IsolationAdapter must be runtime checkable."""
|
|
# This verifies the @runtime_checkable decorator is present
|
|
adapter = MockHostAdapter()
|
|
assert isinstance(adapter, IsolationAdapter)
|
|
|
|
|
|
class TestMockRegistryBehavior:
|
|
"""Tests for MockRegistry ProxiedSingleton example."""
|
|
|
|
def test_registry_register_returns_id(self):
|
|
"""register() must return a string ID."""
|
|
registry = MockRegistry()
|
|
obj_id = registry.register({"key": "value"})
|
|
|
|
assert isinstance(obj_id, str)
|
|
assert obj_id.startswith("obj_")
|
|
|
|
def test_registry_get_retrieves_object(self):
|
|
"""get() must retrieve the registered object."""
|
|
registry = MockRegistry()
|
|
original = {"key": "value"}
|
|
obj_id = registry.register(original)
|
|
|
|
retrieved = registry.get(obj_id)
|
|
assert retrieved == original
|
|
|
|
def test_registry_get_unknown_returns_none(self):
|
|
"""get() with unknown ID must return None."""
|
|
registry = MockRegistry()
|
|
|
|
result = registry.get("nonexistent")
|
|
assert result is None
|
|
|
|
def test_registry_clear_removes_all(self):
|
|
"""clear() must remove all stored objects."""
|
|
registry = MockRegistry()
|
|
registry.register("obj1")
|
|
registry.register("obj2")
|
|
|
|
registry.clear()
|
|
|
|
assert registry.get("obj_0") is None
|
|
assert registry.get("obj_1") is None
|
|
|
|
|
|
class TestTestDataSerialization:
|
|
"""Tests for TestData custom type serialization."""
|
|
|
|
def test_testdata_equality(self):
|
|
"""TestData equality must compare values."""
|
|
a = MockTestData("hello")
|
|
b = MockTestData("hello")
|
|
c = MockTestData("world")
|
|
|
|
assert a == b
|
|
assert a != c
|
|
|
|
def test_testdata_serialization_roundtrip(self):
|
|
"""TestData must survive serialization roundtrip."""
|
|
adapter = MockHostAdapter()
|
|
registry = SerializerRegistry.get_instance()
|
|
registry.clear()
|
|
|
|
adapter.register_serializers(registry)
|
|
|
|
original = MockTestData("test_value")
|
|
serializer = registry.get_serializer("MockTestData")
|
|
deserializer = registry.get_deserializer("MockTestData")
|
|
|
|
serialized = serializer(original)
|
|
deserialized = deserializer(serialized)
|
|
|
|
assert deserialized == original
|