mirror of
https://github.com/hanzoui/pyisolate.git
synced 2026-10-30 20:37:17 +00:00
213 lines
6.5 KiB
Python
213 lines
6.5 KiB
Python
"""Tests for ExtensionManagerConfig and ExtensionConfig validation.
|
|
|
|
These tests verify configuration validation without spawning processes.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from pyisolate import ExtensionConfig, ExtensionManagerConfig
|
|
from pyisolate._internal.rpc_protocol import ProxiedSingleton
|
|
|
|
|
|
class TestExtensionManagerConfig:
|
|
"""Tests for ExtensionManagerConfig TypedDict."""
|
|
|
|
def test_minimal_config(self, tmp_path: Path):
|
|
"""Minimal config requires only venv_root_path."""
|
|
config: ExtensionManagerConfig = {
|
|
"venv_root_path": str(tmp_path / "venvs"),
|
|
}
|
|
|
|
assert "venv_root_path" in config
|
|
|
|
def test_venv_root_path_is_string(self, tmp_path: Path):
|
|
"""venv_root_path must be a string path."""
|
|
config: ExtensionManagerConfig = {
|
|
"venv_root_path": str(tmp_path / "venvs"),
|
|
}
|
|
|
|
assert isinstance(config["venv_root_path"], str)
|
|
|
|
|
|
class TestExtensionConfigValidation:
|
|
"""Tests for ExtensionConfig field validation."""
|
|
|
|
def test_name_must_be_nonempty(self):
|
|
"""Extension name should not be empty."""
|
|
config: ExtensionConfig = {
|
|
"name": "", # Invalid but TypedDict doesn't enforce
|
|
"module_path": "/path/to/ext",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
# The config is syntactically valid, but semantically
|
|
# empty name would cause issues. Runtime validation needed.
|
|
assert config["name"] == ""
|
|
|
|
def test_module_path_can_be_relative(self):
|
|
"""Module path can be relative."""
|
|
config: ExtensionConfig = {
|
|
"name": "myext",
|
|
"module_path": "./extensions/myext",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["module_path"] == "./extensions/myext"
|
|
|
|
def test_module_path_can_be_absolute(self):
|
|
"""Module path can be absolute."""
|
|
config: ExtensionConfig = {
|
|
"name": "myext",
|
|
"module_path": "/app/extensions/myext",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["module_path"] == "/app/extensions/myext"
|
|
|
|
def test_dependencies_format(self):
|
|
"""Dependencies are pip requirement specifiers."""
|
|
config: ExtensionConfig = {
|
|
"name": "myext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [
|
|
"numpy>=1.20",
|
|
"pillow==10.0.0",
|
|
"package[extra]>=2.0,<3.0",
|
|
],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert len(config["dependencies"]) == 3
|
|
assert "numpy>=1.20" in config["dependencies"]
|
|
|
|
def test_apis_are_singleton_types(self):
|
|
"""APIs list contains ProxiedSingleton subclasses."""
|
|
|
|
class MyService(ProxiedSingleton):
|
|
pass
|
|
|
|
config: ExtensionConfig = {
|
|
"name": "myext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [MyService],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert MyService in config["apis"]
|
|
assert issubclass(config["apis"][0], ProxiedSingleton)
|
|
|
|
def test_share_torch_implies_requirements(self):
|
|
"""share_torch=True has implications for tensor handling."""
|
|
config: ExtensionConfig = {
|
|
"name": "ml_ext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": True,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["share_torch"] is True
|
|
# share_cuda_ipc can be independently configured
|
|
assert config["share_cuda_ipc"] is False
|
|
|
|
def test_share_cuda_ipc_requires_share_torch(self):
|
|
"""share_cuda_ipc only makes sense with share_torch."""
|
|
# This is a semantic constraint, not enforced by TypedDict
|
|
config: ExtensionConfig = {
|
|
"name": "gpu_ext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": True, # Required for cuda_ipc to work
|
|
"share_cuda_ipc": True,
|
|
}
|
|
|
|
assert config["share_torch"] is True
|
|
assert config["share_cuda_ipc"] is True
|
|
|
|
|
|
class TestConfigDefaults:
|
|
"""Tests documenting expected config defaults."""
|
|
|
|
def test_isolated_defaults_true(self):
|
|
"""When creating configs, isolated typically defaults True."""
|
|
# This documents expected behavior for config factories
|
|
default_isolated = True
|
|
|
|
config: ExtensionConfig = {
|
|
"name": "ext",
|
|
"module_path": "/path",
|
|
"isolated": default_isolated,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["isolated"] is True
|
|
|
|
def test_share_torch_defaults_false(self):
|
|
"""share_torch defaults to False for safety."""
|
|
default_share_torch = False
|
|
|
|
config: ExtensionConfig = {
|
|
"name": "ext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": default_share_torch,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["share_torch"] is False
|
|
|
|
def test_dependencies_defaults_empty(self):
|
|
"""dependencies defaults to empty list."""
|
|
config: ExtensionConfig = {
|
|
"name": "ext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["dependencies"] == []
|
|
|
|
def test_apis_defaults_empty(self):
|
|
"""apis defaults to empty list."""
|
|
config: ExtensionConfig = {
|
|
"name": "ext",
|
|
"module_path": "/path",
|
|
"isolated": True,
|
|
"dependencies": [],
|
|
"apis": [],
|
|
"share_torch": False,
|
|
"share_cuda_ipc": False,
|
|
}
|
|
|
|
assert config["apis"] == []
|