mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 08:31:03 +01:00
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:
parent
ac9b0b7e97
commit
adc3f72b4b
@ -3,12 +3,14 @@
|
|||||||
"scope": [
|
"scope": [
|
||||||
"python_pkg/sql_drills (new package, fixtures, tests, README)",
|
"python_pkg/sql_drills (new package, fixtures, tests, README)",
|
||||||
"meta/pyproject.toml: register the docker pytest marker",
|
"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": [
|
"changes": [
|
||||||
"CLI start/stop/list/show/check over one long-lived tmpfs container",
|
"CLI start/stop/list/show/check over one long-lived tmpfs container",
|
||||||
"Judge-style normaliser and line diff, unit-tested without Docker",
|
"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": [
|
"verification": [
|
||||||
{
|
{
|
||||||
@ -20,11 +22,17 @@
|
|||||||
"command": "python3 -m python_pkg.sql_drills check <id> <reference> for all 12 drills",
|
"command": "python3 -m python_pkg.sql_drills check <id> <reference> for all 12 drills",
|
||||||
"result": "pass",
|
"result": "pass",
|
||||||
"evidence": "every drill prints PASS; wrong query prints a line diff, syntax error exits 2"
|
"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": [
|
"risks": [
|
||||||
"Local MySQL image must be pulled once; start fails with a clear error otherwise",
|
"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": [
|
"rollback": [
|
||||||
"git revert the commit; run python3 -m python_pkg.sql_drills stop first",
|
"git revert the commit; run python3 -m python_pkg.sql_drills stop first",
|
||||||
|
|||||||
@ -3,12 +3,32 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from python_pkg.sql_drills import _check, _drills, _engine
|
from python_pkg.sql_drills import _check, _drills, _engine
|
||||||
|
|
||||||
_EXIT_CODES = {"pass": 0, "fail": 1, "error": 2}
|
_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:
|
def _out(line: str) -> None:
|
||||||
@ -44,8 +64,10 @@ def _dispatch(args: argparse.Namespace) -> int:
|
|||||||
for drill in _drills.list_drills():
|
for drill in _drills.list_drills():
|
||||||
_out(f"{drill.id} {_drills.title_of(drill)}")
|
_out(f"{drill.id} {_drills.title_of(drill)}")
|
||||||
elif args.cmd == "show":
|
elif args.cmd == "show":
|
||||||
|
_heartbeat()
|
||||||
sys.stdout.write(_drills.render_show(_drills.get_drill(args.drill)))
|
sys.stdout.write(_drills.render_show(_drills.get_drill(args.drill)))
|
||||||
else:
|
else:
|
||||||
|
_heartbeat()
|
||||||
result = _check.run_check(_drills.get_drill(args.drill), args.sql_file)
|
result = _check.run_check(_drills.get_drill(args.drill), args.sql_file)
|
||||||
_out(result.message)
|
_out(result.message)
|
||||||
return _EXIT_CODES[result.status]
|
return _EXIT_CODES[result.status]
|
||||||
|
|||||||
14
python_pkg/sql_drills/tests/conftest.py
Normal file
14
python_pkg/sql_drills/tests/conftest.py
Normal 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))
|
||||||
71
python_pkg/sql_drills/tests/test_focus_hook.py
Normal file
71
python_pkg/sql_drills/tests/test_focus_hook.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user