fix(config): add specific exception handling for path operations

Replaces generic Exception handling with specific PermissionError and OSError
handling in __post_init__ and ensure_runtime_dirs(). Provides clear diagnostic
messages to distinguish permission issues from other filesystem errors.

Solution-ID: SOL-1735385400008
Issue-ID: ISS-1766921318981-8
Task-ID: T1
This commit is contained in:
catlog22
2025-12-29 19:34:27 +08:00
parent 3289562be7
commit 60fbb4177c
2 changed files with 125 additions and 2 deletions

View File

@@ -122,8 +122,21 @@ class Config:
self.data_dir = self.data_dir.expanduser().resolve()
self.venv_path = self.venv_path.expanduser().resolve()
self.data_dir.mkdir(parents=True, exist_ok=True)
except PermissionError as exc:
raise ConfigError(
f"Permission denied initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) "
f"[{type(exc).__name__}]: {exc}"
) from exc
except OSError as exc:
raise ConfigError(
f"Filesystem error initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) "
f"[{type(exc).__name__}]: {exc}"
) from exc
except Exception as exc:
raise ConfigError(f"Failed to initialize data_dir at {self.data_dir}: {exc}") from exc
raise ConfigError(
f"Unexpected error initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) "
f"[{type(exc).__name__}]: {exc}"
) from exc
@cached_property
def cache_dir(self) -> Path:
@@ -145,8 +158,18 @@ class Config:
for directory in (self.cache_dir, self.index_dir):
try:
directory.mkdir(parents=True, exist_ok=True)
except PermissionError as exc:
raise ConfigError(
f"Permission denied creating directory {directory} [{type(exc).__name__}]: {exc}"
) from exc
except OSError as exc:
raise ConfigError(
f"Filesystem error creating directory {directory} [{type(exc).__name__}]: {exc}"
) from exc
except Exception as exc:
raise ConfigError(f"Failed to create directory {directory}: {exc}") from exc
raise ConfigError(
f"Unexpected error creating directory {directory} [{type(exc).__name__}]: {exc}"
) from exc
def language_for_path(self, path: str | Path) -> str | None:
"""Infer a supported language ID from a file path."""

View File

@@ -118,6 +118,57 @@ class TestConfig:
config = Config(data_dir=data_dir)
assert data_dir.exists()
def test_post_init_permission_error_includes_path_and_cause(self, monkeypatch):
"""PermissionError during __post_init__ should raise ConfigError with context."""
with tempfile.TemporaryDirectory() as tmpdir:
data_dir = Path(tmpdir) / "blocked"
venv_path = Path(tmpdir) / "venv"
expected_data_dir = data_dir.expanduser().resolve()
real_mkdir = Path.mkdir
def guarded_mkdir(self, *args, **kwargs):
if self == expected_data_dir:
raise PermissionError("Permission denied")
return real_mkdir(self, *args, **kwargs)
monkeypatch.setattr(Path, "mkdir", guarded_mkdir)
with pytest.raises(ConfigError) as excinfo:
Config(data_dir=data_dir, venv_path=venv_path)
message = str(excinfo.value)
assert str(expected_data_dir) in message
assert "permission" in message.lower()
assert "PermissionError" in message
assert isinstance(excinfo.value.__cause__, PermissionError)
def test_post_init_os_error_includes_path_and_cause(self, monkeypatch):
"""OSError during __post_init__ should raise ConfigError with context."""
with tempfile.TemporaryDirectory() as tmpdir:
data_dir = Path(tmpdir) / "invalid"
venv_path = Path(tmpdir) / "venv"
expected_data_dir = data_dir.expanduser().resolve()
real_mkdir = Path.mkdir
def guarded_mkdir(self, *args, **kwargs):
if self == expected_data_dir:
raise OSError("Invalid path")
return real_mkdir(self, *args, **kwargs)
monkeypatch.setattr(Path, "mkdir", guarded_mkdir)
with pytest.raises(ConfigError) as excinfo:
Config(data_dir=data_dir, venv_path=venv_path)
message = str(excinfo.value)
assert str(expected_data_dir) in message
assert "permission" not in message.lower()
assert "filesystem" in message.lower()
assert "OSError" in message
assert isinstance(excinfo.value.__cause__, OSError)
def test_supported_languages(self):
"""Test default supported languages."""
with tempfile.TemporaryDirectory() as tmpdir:
@@ -158,6 +209,55 @@ class TestConfig:
assert config.cache_dir.exists()
assert config.index_dir.exists()
def test_ensure_runtime_dirs_permission_error_includes_path_and_cause(self, monkeypatch):
"""PermissionError during ensure_runtime_dirs should raise ConfigError with context."""
with tempfile.TemporaryDirectory() as tmpdir:
config = Config(data_dir=Path(tmpdir))
target_dir = config.cache_dir
real_mkdir = Path.mkdir
def guarded_mkdir(self, *args, **kwargs):
if self == target_dir:
raise PermissionError("Permission denied")
return real_mkdir(self, *args, **kwargs)
monkeypatch.setattr(Path, "mkdir", guarded_mkdir)
with pytest.raises(ConfigError) as excinfo:
config.ensure_runtime_dirs()
message = str(excinfo.value)
assert str(target_dir) in message
assert "permission" in message.lower()
assert "PermissionError" in message
assert isinstance(excinfo.value.__cause__, PermissionError)
def test_ensure_runtime_dirs_os_error_includes_path_and_cause(self, monkeypatch):
"""OSError during ensure_runtime_dirs should raise ConfigError with context."""
with tempfile.TemporaryDirectory() as tmpdir:
config = Config(data_dir=Path(tmpdir))
target_dir = config.cache_dir
real_mkdir = Path.mkdir
def guarded_mkdir(self, *args, **kwargs):
if self == target_dir:
raise OSError("Invalid path")
return real_mkdir(self, *args, **kwargs)
monkeypatch.setattr(Path, "mkdir", guarded_mkdir)
with pytest.raises(ConfigError) as excinfo:
config.ensure_runtime_dirs()
message = str(excinfo.value)
assert str(target_dir) in message
assert "permission" not in message.lower()
assert "filesystem" in message.lower()
assert "OSError" in message
assert isinstance(excinfo.value.__cause__, OSError)
def test_language_for_path_python(self):
"""Test language detection for Python files."""
with tempfile.TemporaryDirectory() as tmpdir: