From 9f79621bd674f02b47539bc68bc32063c5d05ec2 Mon Sep 17 00:00:00 2001 From: stacknil Date: Sat, 16 May 2026 17:17:02 +0800 Subject: [PATCH] Validate output table file errors --- src/telemetry_window_demo/io.py | 18 ++++++++++++++++-- tests/test_cli_errors.py | 18 ++++++++++++++++++ tests/test_io.py | 20 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/telemetry_window_demo/io.py b/src/telemetry_window_demo/io.py index 559ab53..a9310ef 100644 --- a/src/telemetry_window_demo/io.py +++ b/src/telemetry_window_demo/io.py @@ -93,7 +93,7 @@ def load_events(path: str | Path) -> pd.DataFrame: def load_feature_table(path: str | Path) -> pd.DataFrame: table_path = Path(path) - frame = pd.read_csv(table_path) + frame = _read_csv_table(table_path, table_name="feature table") _require_columns(frame, FEATURE_TABLE_REQUIRED_COLUMNS, source=str(table_path)) _parse_datetime_columns( frame, @@ -105,7 +105,7 @@ def load_feature_table(path: str | Path) -> pd.DataFrame: def load_alert_table(path: str | Path) -> pd.DataFrame: table_path = Path(path) - frame = pd.read_csv(table_path) + frame = _read_csv_table(table_path, table_name="alert table") _require_columns(frame, ALERT_TABLE_REQUIRED_COLUMNS, source=str(table_path)) _parse_datetime_columns( frame, @@ -115,6 +115,20 @@ def load_alert_table(path: str | Path) -> pd.DataFrame: return frame +def _read_csv_table(table_path: Path, *, table_name: str) -> pd.DataFrame: + if not table_path.exists(): + display_name = table_name[:1].upper() + table_name[1:] + raise FileNotFoundError(f"{display_name} not found: {table_path}") + try: + return pd.read_csv(table_path) + except ( + pd.errors.EmptyDataError, + pd.errors.ParserError, + UnicodeDecodeError, + ) as exc: + raise ValueError(f"Invalid {table_name} CSV in {table_path}: {exc}") from exc + + def _require_columns( frame: pd.DataFrame, required_columns: tuple[str, ...], diff --git a/tests/test_cli_errors.py b/tests/test_cli_errors.py index 90a6a05..8176a09 100644 --- a/tests/test_cli_errors.py +++ b/tests/test_cli_errors.py @@ -66,3 +66,21 @@ def test_main_reports_bad_plot_feature_table_without_traceback(tmp_path, capsys) assert stderr.startswith("error: ") assert "event_count" in stderr assert "Traceback" not in stderr + + +def test_main_reports_missing_default_alert_table_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", + ) + + with pytest.raises(SystemExit) as excinfo: + main(["plot", "--features", str(features_path)]) + + assert excinfo.value.code == 1 + stderr = capsys.readouterr().err + assert stderr.startswith("error: ") + assert "Alert table not found" in stderr + assert "Traceback" not in stderr diff --git a/tests/test_io.py b/tests/test_io.py index b17c184..ae520ca 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -176,6 +176,13 @@ def test_load_feature_table_requires_plot_columns(tmp_path) -> None: assert "event_count" in message +def test_load_feature_table_reports_missing_file(tmp_path) -> None: + path = tmp_path / "missing-features.csv" + + with pytest.raises(FileNotFoundError, match="Feature table not found"): + load_feature_table(path) + + def test_load_feature_table_rejects_invalid_window_timestamp(tmp_path) -> None: path = tmp_path / "features.csv" path.write_text( @@ -192,6 +199,19 @@ def test_load_feature_table_rejects_invalid_window_timestamp(tmp_path) -> None: assert "window_start" in message +def test_load_alert_table_rejects_invalid_csv(tmp_path) -> None: + path = tmp_path / "alerts.csv" + 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", + ) + + with pytest.raises(ValueError, match="Invalid alert table CSV"): + load_alert_table(path) + + def test_load_alert_table_requires_plot_columns(tmp_path) -> None: path = tmp_path / "alerts.csv" path.write_text(