sql_drills: optional focus heartbeat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYcmik6APnHfc9gEVy
This commit is contained in:
Krzysztof Rudnicki 2026-11-02 08:24:46 +01:00
parent ac9b0b7e97
commit adc3f72b4b
4 changed files with 118 additions and 3 deletions

View File

@ -3,12 +3,14 @@
"scope": [
"python_pkg/sql_drills (new package, fixtures, tests, README)",
"meta/pyproject.toml: register the docker pytest marker",
"Non-goal: bumping the pinned mysql image; copying third-party problems"
"Non-goal: bumping the pinned mysql image; copying third-party problems",
"2026-11-02: show/check send an optional focus-tracker heartbeat (__main__.py, tests/conftest.py, tests/test_focus_hook.py)"
],
"changes": [
"CLI start/stop/list/show/check over one long-lived tmpfs container",
"Judge-style normaliser and line diff, unit-tested without Docker",
"12 original drills with live-verified reference solutions kept in tests only"
"12 original drills with live-verified reference solutions kept in tests only",
"_heartbeat(): soft import of focus_project; absent -> no-op, tracker error -> logged, result and exit code unchanged"
],
"verification": [
{
@ -20,11 +22,17 @@
"command": "python3 -m python_pkg.sql_drills check <id> <reference> for all 12 drills",
"result": "pass",
"evidence": "every drill prints PASS; wrong query prints a line diff, syntax error exits 2"
},
{
"command": "python3 -m pytest -q python_pkg/sql_drills/tests --cov=python_pkg.sql_drills --cov-branch",
"result": "pass",
"evidence": "61 passed, 15 live tests skipped (no container), 100% branch coverage; show 01 wrote one sql_drills heartbeat row to a scratch FOCUS_PROJECT_DATA"
}
],
"risks": [
"Local MySQL image must be pulled once; start fails with a clear error otherwise",
"A runaway query is cut off client-side after 30 s but may keep running in the container until stop"
"A runaway query is cut off client-side after 30 s but may keep running in the container until stop",
"A hanging tracker would delay show/check; the real one is a local file append"
],
"rollback": [
"git revert the commit; run python3 -m python_pkg.sql_drills stop first",

View File

@ -3,12 +3,32 @@
from __future__ import annotations
import argparse
import importlib
import logging
from pathlib import Path
import sys
from python_pkg.sql_drills import _check, _drills, _engine
_EXIT_CODES = {"pass": 0, "fail": 1, "error": 2}
_LOG = logging.getLogger(__name__)
def _heartbeat() -> None:
"""Tell the optional focus tracker that practice is happening.
Soft dependency: absent module -> silent no-op; a tracker error is logged
(file or registry errors) and never changes the drill's result or
exit code.
"""
try:
tracker = importlib.import_module("focus_project")
except ImportError:
return
try:
tracker.record_activity("sql_drills", "heartbeat")
except OSError, ValueError:
_LOG.exception("focus tracker heartbeat failed")
def _out(line: str) -> None:
@ -44,8 +64,10 @@ def _dispatch(args: argparse.Namespace) -> int:
for drill in _drills.list_drills():
_out(f"{drill.id} {_drills.title_of(drill)}")
elif args.cmd == "show":
_heartbeat()
sys.stdout.write(_drills.render_show(_drills.get_drill(args.drill)))
else:
_heartbeat()
result = _check.run_check(_drills.get_drill(args.drill), args.sql_file)
_out(result.message)
return _EXIT_CODES[result.status]

View File

@ -0,0 +1,14 @@
"""Keep the optional focus tracker out of every test unless a test fakes it."""
from __future__ import annotations
import sys
from typing import Any, cast
import pytest
@pytest.fixture(autouse=True)
def _no_real_focus_tracker(monkeypatch: pytest.MonkeyPatch) -> None:
"""Make ``import focus_project`` fail, so no test writes live activity."""
monkeypatch.setitem(sys.modules, "focus_project", cast("Any", None))

View File

@ -0,0 +1,71 @@
"""The soft-optional focus-tracker heartbeat on ``show`` and ``check``."""
from __future__ import annotations
import logging
import sys
import types
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import patch
import pytest
from python_pkg.sql_drills import __main__ as cli
from python_pkg.sql_drills import _check
if TYPE_CHECKING:
from pathlib import Path
def _fake_tracker(
monkeypatch: pytest.MonkeyPatch, *, fail: bool = False
) -> list[tuple[Any, ...]]:
calls: list[tuple[Any, ...]] = []
module = cast("Any", types.ModuleType("focus_project"))
def record_activity(*args: str) -> None:
if fail:
msg = "tracker down"
raise OSError(msg)
calls.append(args)
module.record_activity = record_activity
monkeypatch.setitem(sys.modules, "focus_project", module)
return calls
def test_show_sends_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None:
calls = _fake_tracker(monkeypatch)
assert cli.main(["show", "01"]) == 0
assert calls == [("sql_drills", "heartbeat")]
def test_check_sends_heartbeat(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
calls = _fake_tracker(monkeypatch)
result = _check.CheckResult("fail", "MSG")
with patch.object(_check, "run_check", return_value=result):
assert cli.main(["check", "01", str(tmp_path / "q.sql")]) == 1
assert calls == [("sql_drills", "heartbeat")]
def test_list_sends_no_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None:
calls = _fake_tracker(monkeypatch)
assert cli.main(["list"]) == 0
assert calls == []
def test_missing_tracker_changes_nothing(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["show", "01"]) == 0
assert "CREATE TABLE" in capsys.readouterr().out
def test_failing_tracker_is_logged_not_raised(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
capsys: pytest.CaptureFixture[str],
) -> None:
_fake_tracker(monkeypatch, fail=True)
with caplog.at_level(logging.ERROR):
assert cli.main(["show", "01"]) == 0
assert "heartbeat" in caplog.text
assert "CREATE TABLE" in capsys.readouterr().out