Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion omop_alchemy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,19 @@ def get_cdm_context() -> tuple[OmopAlchemyConfig, ResolvedResource]:

The resource is taken from tools.omop_alchemy.default_resource when set;
otherwise falls back to the canonical CDM_DB resource name.

Raises
------
RuntimeError
If no oa-configurator stack config file exists yet.
"""
stack = load_stack_config()
try:
stack = load_stack_config()
except FileNotFoundError as exc:
raise RuntimeError(
"No omop-alchemy configuration found. "
"Run `omop-config configure omop_alchemy` to set it up."
) from exc
pkg_config = OmopAlchemyConfig.from_stack(stack)
tool = stack.tools.get(OmopAlchemyConfig.tool_name)
resource_name = (tool.default_resource if tool else None) or OmopAlchemyConfig.CDM_DB.semantic_name
Expand Down
10 changes: 10 additions & 0 deletions omop_alchemy/maintenance/cli_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ def create_database_backup(
raise RuntimeError(
"Database backup failed via `pg_dump`." + (f" {stderr}" if stderr else "")
) from exc
except FileNotFoundError as exc:
raise RuntimeError(
f"`pg_dump` executable not found at {tool_path!r}. "
"It may have been removed from PATH after being resolved."
) from exc

return BackupResult(
file_path=str(resolved_output_path),
Expand Down Expand Up @@ -123,6 +128,11 @@ def restore_database_backup(
raise RuntimeError(
"Database restore failed." + (f" {stderr}" if stderr else "")
) from exc
except FileNotFoundError as exc:
raise RuntimeError(
f"Restore executable not found at {tool_path!r}. "
"It may have been removed from PATH after being resolved."
) from exc

return BackupResult(
file_path=str(resolved_input_path),
Expand Down
74 changes: 74 additions & 0 deletions tests/test_cli_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Tests for missing-config error handling.

get_cdm_context() converts a missing oa-configurator stack file into a clear
RuntimeError at the source (config.py), rather than handle_error() pattern-
matching on the raw FileNotFoundError type. This keeps handle_error's
FileNotFoundError-shaped catch from also swallowing unrelated missing-file
errors raised elsewhere inside a command body (e.g. a missing pg_dump binary).
"""

import pytest
import typer

from omop_alchemy.config import get_cdm_context
from omop_alchemy.maintenance._cli_utils import handle_error
from omop_alchemy.maintenance.ui import console


@pytest.fixture(autouse=True)
def _wide_console():
"""Rich wraps/truncates Panel text at the console's default 80-column
width, which is narrower than this message under pytest's captured,
non-tty output. Widen it so assertions can match the full text."""
original_width = console.width
console.width = 200
yield
console.width = original_width


def _raise_file_not_found():
raise FileNotFoundError("Config file not found: /home/cava/.config/omop/config.toml")


def test_get_cdm_context_raises_runtime_error_when_config_missing(monkeypatch):
monkeypatch.setattr("omop_alchemy.config.load_stack_config", _raise_file_not_found)
with pytest.raises(RuntimeError, match="omop-config configure omop_alchemy"):
get_cdm_context()


def test_get_cdm_context_runtime_error_does_not_leak_raw_path(monkeypatch):
"""The friendly message should not surface the raw exception text."""
monkeypatch.setattr("omop_alchemy.config.load_stack_config", _raise_file_not_found)
with pytest.raises(RuntimeError) as exc_info:
get_cdm_context()
assert "/home/cava/.config/omop/config.toml" not in str(exc_info.value)


def test_get_cdm_context_runtime_error_chains_original_exception(monkeypatch):
monkeypatch.setattr("omop_alchemy.config.load_stack_config", _raise_file_not_found)
with pytest.raises(RuntimeError) as exc_info:
get_cdm_context()
assert isinstance(exc_info.value.__cause__, FileNotFoundError)


def test_handle_error_runtime_error_exits_with_code_1_and_prints_hint(capsys):
"""The RuntimeError from get_cdm_context is handled cleanly, same as any other."""
with pytest.raises(typer.Exit) as exc_info:
handle_error(
RuntimeError(
"No omop-alchemy configuration found. "
"Run `omop-config configure omop_alchemy` to set it up."
)
)
assert exc_info.value.exit_code == 1
captured = capsys.readouterr()
assert "omop-config configure omop_alchemy" in captured.out


def test_handle_error_does_not_special_case_file_not_found():
"""A stray FileNotFoundError from elsewhere in a command body must not be
mislabeled as a missing-configuration error — it should propagate as-is."""
exc = FileNotFoundError("pg_dump: No such file or directory")
with pytest.raises(FileNotFoundError) as exc_info:
handle_error(exc)
assert exc_info.value is exc
Loading