Files
mlx-knife/tests/conftest.py
T
2025-08-12 23:00:55 +02:00

147 lines
5.2 KiB
Python

"""
Pytest configuration and shared fixtures for MLX Knife tests.
"""
import os
import tempfile
import shutil
import pytest
import subprocess
import signal
import time
from pathlib import Path
from typing import Generator, List
import psutil
@pytest.fixture
def temp_cache_dir() -> Generator[Path, None, None]:
"""Create a temporary cache directory for isolated testing."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_path = Path(temp_dir) / "test_cache"
cache_path.mkdir()
# Set HF_HOME to our temp directory
old_hf_home = os.environ.get("HF_HOME")
os.environ["HF_HOME"] = str(cache_path)
try:
yield cache_path
finally:
# Restore original HF_HOME
if old_hf_home:
os.environ["HF_HOME"] = old_hf_home
elif "HF_HOME" in os.environ:
del os.environ["HF_HOME"]
@pytest.fixture
def mlx_knife_process():
"""Factory fixture to create and manage mlx_knife subprocess."""
processes: List[subprocess.Popen] = []
def _create_process(args: List[str], **kwargs) -> subprocess.Popen:
"""Create a new mlx_knife process and track it."""
full_args = ["python", "-m", "mlx_knife.cli"] + args
proc = subprocess.Popen(
full_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
**kwargs
)
processes.append(proc)
return proc
yield _create_process
# Cleanup: Kill all created processes
for proc in processes:
if proc.poll() is None: # Process still running
try:
proc.terminate()
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
@pytest.fixture
def process_monitor():
"""Monitor processes for zombie detection."""
def _get_process_tree(pid: int) -> List[psutil.Process]:
"""Get all child processes of a given PID."""
try:
parent = psutil.Process(pid)
return parent.children(recursive=True)
except psutil.NoSuchProcess:
return []
def _wait_for_process_cleanup(pid: int, timeout: float = 5.0) -> bool:
"""Wait for all child processes to terminate."""
start_time = time.time()
while time.time() - start_time < timeout:
children = _get_process_tree(pid)
if not children:
return True
time.sleep(0.1)
return False
return {
"get_process_tree": _get_process_tree,
"wait_for_cleanup": _wait_for_process_cleanup
}
@pytest.fixture
def mock_model_cache(temp_cache_dir):
"""Create mock model cache structures for testing."""
def _create_mock_model(
model_name: str,
healthy: bool = True,
corruption_type: str = None
) -> Path:
"""Create a mock model in the cache directory."""
# Convert model name to cache directory format
cache_name = model_name.replace("/", "--")
model_dir = temp_cache_dir / f"models--{cache_name}" / "snapshots" / "main"
model_dir.mkdir(parents=True, exist_ok=True)
if healthy and not corruption_type:
# Create healthy model files
(model_dir / "config.json").write_text('{"model_type": "test"}')
(model_dir / "tokenizer.json").write_text('{"version": "1.0"}')
(model_dir / "model.safetensors").write_bytes(b"fake_model_data" * 100)
elif corruption_type:
_create_corrupted_model(model_dir, corruption_type)
return model_dir
def _create_corrupted_model(model_dir: Path, corruption_type: str):
"""Create various types of corrupted models."""
if corruption_type == "missing_snapshot":
# Remove snapshots directory
shutil.rmtree(model_dir.parent.parent)
elif corruption_type == "missing_config":
(model_dir / "tokenizer.json").write_text('{"version": "1.0"}')
(model_dir / "model.safetensors").write_bytes(b"fake_model_data")
# config.json is missing
elif corruption_type == "lfs_pointer":
(model_dir / "config.json").write_text('{"model_type": "test"}')
(model_dir / "tokenizer.json").write_text('{"version": "1.0"}')
# Create LFS pointer file instead of actual data
(model_dir / "model.safetensors").write_text(
"version https://git-lfs.github.com/spec/v1\n"
"oid sha256:abc123\n"
"size 1000000\n"
)
elif corruption_type == "truncated_safetensors":
(model_dir / "config.json").write_text('{"model_type": "test"}')
(model_dir / "tokenizer.json").write_text('{"version": "1.0"}')
# Create truncated/corrupted safetensors
(model_dir / "model.safetensors").write_bytes(b"corrupted")
elif corruption_type == "missing_tokenizer":
(model_dir / "config.json").write_text('{"model_type": "test"}')
(model_dir / "model.safetensors").write_bytes(b"fake_model_data")
# tokenizer.json is missing
return _create_mock_model