diff --git a/src/telemetry_window_demo/io.py b/src/telemetry_window_demo/io.py index 614ae4e..902ed0f 100644 --- a/src/telemetry_window_demo/io.py +++ b/src/telemetry_window_demo/io.py @@ -248,9 +248,9 @@ def _require_text_columns( def write_table(frame: pd.DataFrame, path: str | Path) -> Path: output_path = Path(path) - output_path.parent.mkdir(parents=True, exist_ok=True) - - export = frame.copy() + _ensure_output_directory(output_path.parent) + + export = frame.copy() for column in export.columns: dtype = export[column].dtype if pd.api.types.is_datetime64_any_dtype(dtype) or isinstance( @@ -265,15 +265,27 @@ def write_table(frame: pd.DataFrame, path: str | Path) -> Path: def write_json(payload: dict[str, Any], path: str | Path) -> Path: output_path = Path(path) - output_path.parent.mkdir(parents=True, exist_ok=True) + _ensure_output_directory(output_path.parent) output_path.write_text( json.dumps(payload, indent=2) + "\n", encoding="utf-8", ) return output_path - - -def format_timestamp(value: Any) -> str: + + +def ensure_output_directory(path: str | Path) -> Path: + output_dir = Path(path) + _ensure_output_directory(output_dir) + return output_dir + + +def _ensure_output_directory(output_dir: Path) -> None: + if output_dir.exists() and not output_dir.is_dir(): + raise ValueError(f"Output directory path is not a directory: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + + +def format_timestamp(value: Any) -> str: if pd.isna(value): return "" timestamp = pd.Timestamp(value) diff --git a/src/telemetry_window_demo/visualize.py b/src/telemetry_window_demo/visualize.py index a9d968f..0503e29 100644 --- a/src/telemetry_window_demo/visualize.py +++ b/src/telemetry_window_demo/visualize.py @@ -5,6 +5,8 @@ import matplotlib import pandas as pd +from .io import ensure_output_directory + matplotlib.use("Agg") import matplotlib.pyplot as plt @@ -14,8 +16,7 @@ def plot_outputs( alerts: pd.DataFrame, output_dir: str | Path, ) -> list[Path]: - target_dir = Path(output_dir) - target_dir.mkdir(parents=True, exist_ok=True) + target_dir = ensure_output_directory(output_dir) plt.style.use("seaborn-v0_8-whitegrid") paths = [ diff --git a/tests/test_cli_errors.py b/tests/test_cli_errors.py index 02cb04b..6c2d762 100644 --- a/tests/test_cli_errors.py +++ b/tests/test_cli_errors.py @@ -72,6 +72,36 @@ def test_main_reports_directory_input_without_traceback(tmp_path, capsys) -> Non assert "Traceback" not in stderr +def test_main_reports_file_output_dir_without_traceback(tmp_path, capsys) -> None: + input_path = tmp_path / "events.csv" + input_path.write_text( + "timestamp,event_type,source,target,status\n" + "2026-03-10T10:00:00Z,login_success,user_a,auth,ok\n", + encoding="utf-8", + ) + output_path = tmp_path / "processed" + output_path.write_text("not a directory\n", encoding="utf-8") + config_path = tmp_path / "file-output-dir.yaml" + config_path.write_text( + yaml.safe_dump( + { + "input_path": str(input_path), + "output_dir": str(output_path), + } + ), + encoding="utf-8", + ) + + with pytest.raises(SystemExit) as excinfo: + main(["run", "--config", str(config_path)]) + + assert excinfo.value.code == 1 + stderr = capsys.readouterr().err + assert stderr.startswith("error: ") + assert "Output directory path is not a directory" in stderr + assert "Traceback" not in stderr + + def test_main_reports_bad_summarize_timestamp_column_without_traceback( tmp_path, capsys, @@ -155,3 +185,40 @@ def test_main_reports_missing_default_alert_table_without_traceback(tmp_path, ca assert stderr.startswith("error: ") assert "Alert table not found" in stderr assert "Traceback" not in stderr + + +def test_main_reports_file_plot_output_dir_without_traceback(tmp_path, capsys) -> None: + features_path = tmp_path / "features.csv" + features_path.write_text( + "window_start,window_end,event_count,error_rate\n" + "2026-03-10T10:00:00Z,2026-03-10T10:01:00Z,10,0.25\n", + encoding="utf-8", + ) + alerts_path = tmp_path / "alerts.csv" + alerts_path.write_text( + "alert_time,window_start,window_end,rule_name,severity\n" + "2026-03-10T10:01:00Z,2026-03-10T10:00:00Z," + "2026-03-10T10:01:00Z,high_error_rate,medium\n", + encoding="utf-8", + ) + output_path = tmp_path / "plots" + output_path.write_text("not a directory\n", encoding="utf-8") + + with pytest.raises(SystemExit) as excinfo: + main( + [ + "plot", + "--features", + str(features_path), + "--alerts", + str(alerts_path), + "--output-dir", + str(output_path), + ] + ) + + assert excinfo.value.code == 1 + stderr = capsys.readouterr().err + assert stderr.startswith("error: ") + assert "Output directory path is not a directory" in stderr + assert "Traceback" not in stderr