Files
stack/tests/perf/test_hooks.py
kert 7696c1c6da feat: add perf module — OTel pipeline telemetry with Grafana dashboards
Add `perf` as the 12th skinny package with full OpenTelemetry
instrumentation for pipeline traces, Prometheus metrics, and
Loki-correlated logs.

- src/perf/: TracerProvider, MeterProvider, LoggerProvider bridge,
  PipelineCollector, psutil system metrics, FileExporter fallback,
  FastAPI middleware, auto-file Gitea issues on step/test failure
- infra/otel/: OTel Collector fan-out (traces→Jaeger, metrics→Prometheus,
  logs→Loki), all services migrated from jaeger:4317 to collector
- infra/grafana/dashboards/: 8-panel pipeline performance dashboard
- Pipeline runner auto-instruments all 189 steps with zero-cost
  MockTracer/MockSpan no-ops when telemetry is disabled
- 82 new tests, 12,055 total passing

Closes #203, closes #204, closes #205, closes #206, closes #207,
closes #208, closes #209, closes #210, closes #211, closes #212,
closes #213
2026-03-24 21:37:41 -04:00

175 lines
5.7 KiB
Python

"""Tests for perf.hooks — Gitea issue filing on failure/skip."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
class TestOnStepFailure:
"""on_step_failure builds correct issue title and body."""
@patch("perf.hooks._file_issue")
def test_files_issue_on_exception(self, mock_file):
mock_file.return_value = {"number": 999}
from perf.hooks import on_step_failure
exc = ValueError("missing column: person_id")
on_step_failure("core.encounter", exc, pipeline="readmissions")
mock_file.assert_called_once()
title, body = mock_file.call_args[0][:2]
assert "core.encounter" in title
assert "readmissions" in title
assert "failed" in title
assert "ValueError" in body
assert "missing column: person_id" in body
assert "Traceback" in body
@patch("perf.hooks._file_issue")
def test_labels_include_bug_and_pipeline(self, mock_file):
mock_file.return_value = {"number": 1}
from perf.hooks import on_step_failure
on_step_failure("x.y", RuntimeError("boom"), pipeline="core")
labels = mock_file.call_args[1].get("labels") or mock_file.call_args[0][2]
assert "bug" in labels
assert "pipeline" in labels
class TestOnStepSkip:
"""on_step_skip builds correct issue for skipped steps."""
@patch("perf.hooks._file_issue")
def test_files_issue_on_skip(self, mock_file):
mock_file.return_value = {"number": 100}
from perf.hooks import on_step_skip
on_step_skip("readmissions._int_enc", pipeline="readmissions")
mock_file.assert_called_once()
title = mock_file.call_args[0][0]
assert "skipped" in title
assert "readmissions._int_enc" in title
class TestPytestHook:
"""pytest_runtest_logreport files issues in CI."""
@patch("perf.hooks._file_test_issue")
def test_ignores_setup_phase(self, mock_file):
from perf.hooks import pytest_runtest_logreport
report = MagicMock()
report.when = "setup"
pytest_runtest_logreport(report)
mock_file.assert_not_called()
@patch("perf.hooks._file_test_issue")
def test_ignores_without_env_var(self, mock_file, monkeypatch):
monkeypatch.delenv("STACK_FILE_TEST_ISSUES", raising=False)
from perf.hooks import pytest_runtest_logreport
report = MagicMock()
report.when = "call"
report.failed = True
pytest_runtest_logreport(report)
mock_file.assert_not_called()
@patch("perf.hooks._file_test_issue")
def test_files_on_failure_in_ci(self, mock_file, monkeypatch):
monkeypatch.setenv("STACK_FILE_TEST_ISSUES", "true")
from perf.hooks import pytest_runtest_logreport
report = MagicMock()
report.when = "call"
report.failed = True
report.skipped = False
pytest_runtest_logreport(report)
mock_file.assert_called_once_with(report, "failed")
@patch("perf.hooks._file_test_issue")
def test_files_on_skip_in_ci(self, mock_file, monkeypatch):
monkeypatch.setenv("STACK_FILE_TEST_ISSUES", "true")
from perf.hooks import pytest_runtest_logreport
report = MagicMock()
report.when = "call"
report.failed = False
report.skipped = True
pytest_runtest_logreport(report)
mock_file.assert_called_once_with(report, "skipped")
class TestFileTestIssue:
"""_file_test_issue builds correct markdown body."""
@patch("perf.hooks._file_issue")
def test_body_includes_nodeid(self, mock_file):
mock_file.return_value = {"number": 50}
from perf.hooks import _file_test_issue
report = MagicMock()
report.nodeid = "tests/aco/test_core.py::test_encounter"
report.longrepr = "AssertionError: ..."
report.longreprtext = "AssertionError: expected 10 got 0"
_file_test_issue(report, "failed")
title, body = mock_file.call_args[0][:2]
assert "test_core.py::test_encounter" in title
assert "failed" in title
assert "AssertionError" in body
@patch("perf.hooks._file_issue")
def test_failed_gets_bug_label(self, mock_file):
mock_file.return_value = {"number": 51}
from perf.hooks import _file_test_issue
report = MagicMock()
report.nodeid = "tests/foo.py::test_bar"
report.longrepr = None
_file_test_issue(report, "failed")
labels = mock_file.call_args[1].get("labels") or mock_file.call_args[0][2]
assert "bug" in labels
assert "test" in labels
@patch("perf.hooks._file_issue")
def test_skipped_no_bug_label(self, mock_file):
mock_file.return_value = {"number": 52}
from perf.hooks import _file_test_issue
report = MagicMock()
report.nodeid = "tests/foo.py::test_baz"
report.longrepr = None
_file_test_issue(report, "skipped")
labels = mock_file.call_args[1].get("labels") or mock_file.call_args[0][2]
assert "bug" not in labels
assert "test" in labels
class TestRunnerIntegration:
"""Pipeline runner calls on_step_failure when a step raises."""
@patch("perf.hooks.on_step_failure")
def test_runner_fires_hook_on_failure(self, mock_hook):
import polars as pl
from aco.pipe.runner import run_pipeline
def bad_step(input_layer__data):
raise RuntimeError("data corrupted")
data = pl.DataFrame({"id": [1]})
try:
run_pipeline([("ns.bad", bad_step)], lambda ref: data)
except RuntimeError:
pass
mock_hook.assert_called_once()
args = mock_hook.call_args
assert args[0][0] == "ns.bad"
assert isinstance(args[0][1], RuntimeError)