196 lines
6.7 KiB
Python
196 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Mock file system for testing without actual file I/O.
|
|
"""
|
|
|
|
import json
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, Any, List
|
|
from datetime import datetime
|
|
|
|
|
|
@dataclass
|
|
class MockFile:
|
|
"""Represents a mock file in memory."""
|
|
content: bytes = b""
|
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
modified_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
is_locked: bool = False
|
|
lock_holder: Optional[str] = None
|
|
|
|
|
|
class MockFileSystem:
|
|
"""
|
|
In-memory file system for testing.
|
|
|
|
Simulates file operations without touching the actual file system.
|
|
Thread-safe for concurrent access testing.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._files: Dict[str, MockFile] = {}
|
|
self._locks: Dict[str, threading.Lock] = {}
|
|
self._operations_log: List[Dict] = []
|
|
self._lock = threading.Lock()
|
|
|
|
def _get_lock(self, path: str) -> threading.Lock:
|
|
"""Get or create a lock for a path."""
|
|
with self._lock:
|
|
if path not in self._locks:
|
|
self._locks[path] = threading.Lock()
|
|
return self._locks[path]
|
|
|
|
def _log_operation(self, operation: str, path: str, **kwargs) -> None:
|
|
"""Log a file operation for test assertions."""
|
|
self._operations_log.append({
|
|
"operation": operation,
|
|
"path": path,
|
|
"timestamp": datetime.now().isoformat(),
|
|
**kwargs
|
|
})
|
|
|
|
def exists(self, path: Path) -> bool:
|
|
"""Check if file exists."""
|
|
return str(path) in self._files
|
|
|
|
def read_text(self, path: Path) -> str:
|
|
"""Read file as text."""
|
|
path_str = str(path)
|
|
if path_str not in self._files:
|
|
raise FileNotFoundError(f"File not found: {path}")
|
|
self._log_operation("read_text", path_str)
|
|
return self._files[path_str].content.decode('utf-8')
|
|
|
|
def read_bytes(self, path: Path) -> bytes:
|
|
"""Read file as bytes."""
|
|
path_str = str(path)
|
|
if path_str not in self._files:
|
|
raise FileNotFoundError(f"File not found: {path}")
|
|
self._log_operation("read_bytes", path_str)
|
|
return self._files[path_str].content
|
|
|
|
def write_text(self, path: Path, content: str) -> None:
|
|
"""Write text to file."""
|
|
path_str = str(path)
|
|
with self._get_lock(path_str):
|
|
if path_str in self._files:
|
|
self._files[path_str].content = content.encode('utf-8')
|
|
self._files[path_str].modified_at = datetime.now().isoformat()
|
|
else:
|
|
self._files[path_str] = MockFile(content=content.encode('utf-8'))
|
|
self._log_operation("write_text", path_str, size=len(content))
|
|
|
|
def write_bytes(self, path: Path, content: bytes) -> None:
|
|
"""Write bytes to file."""
|
|
path_str = str(path)
|
|
with self._get_lock(path_str):
|
|
if path_str in self._files:
|
|
self._files[path_str].content = content
|
|
self._files[path_str].modified_at = datetime.now().isoformat()
|
|
else:
|
|
self._files[path_str] = MockFile(content=content)
|
|
self._log_operation("write_bytes", path_str, size=len(content))
|
|
|
|
def delete(self, path: Path) -> None:
|
|
"""Delete file."""
|
|
path_str = str(path)
|
|
with self._get_lock(path_str):
|
|
if path_str in self._files:
|
|
del self._files[path_str]
|
|
self._log_operation("delete", path_str)
|
|
|
|
def mkdir(self, path: Path, parents: bool = False, exist_ok: bool = False) -> None:
|
|
"""Create directory (no-op in mock, just logs)."""
|
|
self._log_operation("mkdir", str(path), parents=parents, exist_ok=exist_ok)
|
|
|
|
def read_json(self, path: Path) -> Any:
|
|
"""Read and parse JSON file."""
|
|
content = self.read_text(path)
|
|
return json.loads(content)
|
|
|
|
def write_json(self, path: Path, data: Any, indent: int = 2) -> None:
|
|
"""Write data as JSON file."""
|
|
content = json.dumps(data, indent=indent)
|
|
self.write_text(path, content)
|
|
|
|
def acquire_lock(self, path: Path, timeout: float = 30.0) -> bool:
|
|
"""Acquire file lock."""
|
|
path_str = str(path)
|
|
with self._get_lock(path_str):
|
|
if path_str in self._files:
|
|
if self._files[path_str].is_locked:
|
|
return False
|
|
self._files[path_str].is_locked = True
|
|
self._files[path_str].lock_holder = f"thread-{threading.current_thread().ident}"
|
|
self._log_operation("acquire_lock", path_str, timeout=timeout)
|
|
return True
|
|
|
|
def release_lock(self, path: Path) -> None:
|
|
"""Release file lock."""
|
|
path_str = str(path)
|
|
with self._get_lock(path_str):
|
|
if path_str in self._files:
|
|
self._files[path_str].is_locked = False
|
|
self._files[path_str].lock_holder = None
|
|
self._log_operation("release_lock", path_str)
|
|
|
|
def is_locked(self, path: Path) -> bool:
|
|
"""Check if file is locked."""
|
|
path_str = str(path)
|
|
if path_str in self._files:
|
|
return self._files[path_str].is_locked
|
|
return False
|
|
|
|
def get_operations(self) -> List[Dict]:
|
|
"""Get logged operations for assertions."""
|
|
return self._operations_log.copy()
|
|
|
|
def clear_operations(self) -> None:
|
|
"""Clear operation log."""
|
|
self._operations_log.clear()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the entire file system."""
|
|
with self._lock:
|
|
self._files.clear()
|
|
self._operations_log.clear()
|
|
self._locks.clear()
|
|
|
|
def list_files(self, pattern: str = "*") -> List[str]:
|
|
"""List files matching pattern."""
|
|
import fnmatch
|
|
return [p for p in self._files.keys() if fnmatch.fnmatch(p, pattern)]
|
|
|
|
|
|
class MockFileLock:
|
|
"""
|
|
Mock file lock context manager.
|
|
|
|
Simulates file locking without actual file system calls.
|
|
"""
|
|
|
|
def __init__(self, fs: MockFileSystem, path: Path, timeout: float = 30.0):
|
|
self.fs = fs
|
|
self.path = path
|
|
self.timeout = timeout
|
|
self._acquired = False
|
|
|
|
def __enter__(self):
|
|
"""Acquire lock."""
|
|
import time
|
|
start = time.time()
|
|
while time.time() - start < self.timeout:
|
|
if self.fs.acquire_lock(self.path, timeout=0.1):
|
|
self._acquired = True
|
|
return self
|
|
time.sleep(0.05)
|
|
raise TimeoutError(f"Could not acquire lock on {self.path} within {self.timeout}s")
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Release lock."""
|
|
if self._acquired:
|
|
self.fs.release_lock(self.path)
|
|
return False
|