testsAndMisc/python_pkg/code_tutor/tests/test_signature_block.py
Krzysztof kuhy Rudnicki e3214c813c
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Clear the seven over-cap code_tutor test modules
Partitioned at the section banners each file already used, into ten new
modules named for what they cover. python_pkg/code_tutor is now entirely
under the cap, from thirteen violations.

The splitter copies the whole original header into each new file and
lets ruff's F401 pass prune it - 59 imports removed correctly, versus a
hand-written import block failing on every earlier test split today.
That does not cover module-level helpers: _make_verifier sat between two
banners and was left behind by its callers, so it moved to conftest.py,
taking test_verifier.py's shadowing copy of _make_live_mock with it.

No patch string changed. Pass count is 217 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:06:38 +02:00

225 lines
6.9 KiB
Python

"""Tests for signature-block extraction and import hints."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
from python_pkg.code_tutor._challenge import (
_collect_and_rate_tests,
_validate_tests_against_real,
)
from python_pkg.code_tutor._challenge_support import (
_extract_signature_block,
_import_hint,
)
from python_pkg.code_tutor.tests.conftest import _item, _make_live_mock
# ---------------------------------------------------------------------------
# _extract_signature_block
# ---------------------------------------------------------------------------
def test_extract_signature_block_with_docstring(tmp_path: Path) -> None:
source = 'def fn(x):\n """Do thing."""\n return x\n'
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
result = _extract_signature_block(_item(file="mod.py"), str(tmp_path))
assert "def fn" in result
assert '"""Do thing."""' in result
def test_extract_signature_block_without_docstring(tmp_path: Path) -> None:
source = "def fn(x):\n return x\n"
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
result = _extract_signature_block(_item(file="mod.py"), str(tmp_path))
assert "def fn" in result
assert "return" not in result
def test_extract_signature_block_oserror(tmp_path: Path) -> None:
result = _extract_signature_block(_item(file="missing.py"), str(tmp_path))
assert "def fn(...):" in result
def test_extract_signature_block_no_match(tmp_path: Path) -> None:
source = "def other(): pass\n"
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
result = _extract_signature_block(_item(file="mod.py", name="fn"), str(tmp_path))
assert "def fn(...):" in result
def test_extract_signature_block_syntax_error(tmp_path: Path) -> None:
(tmp_path / "mod.py").write_text("def (broken):\n", encoding="utf-8")
result = _extract_signature_block(_item(file="mod.py"), str(tmp_path))
assert "def fn(...):" in result
# ---------------------------------------------------------------------------
# Tests for _import_hint
# ---------------------------------------------------------------------------
def test_import_hint_relative_to_project(tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
result = _import_hint(
_item(file="src/mod.py", name="fn"),
str(tmp_path),
tmp_path,
)
assert result == "from src.mod import fn"
def test_import_hint_value_error(tmp_path: Path) -> None:
# project_root is different from codebase_path
other_root = tmp_path / "other"
other_root.mkdir()
result = _import_hint(
_item(file="src/mod.py", name="fn"),
str(tmp_path),
other_root,
)
assert result == "from mod import fn"
# ---------------------------------------------------------------------------
# _validate_tests_against_real
# ---------------------------------------------------------------------------
def test_validate_tests_syntax_error() -> None:
mock_console = MagicMock()
result = _validate_tests_against_real(
"def (broken):", "import fn", Path("/proj"), mock_console
)
assert result is False
def test_validate_tests_passes() -> None:
mock_console = MagicMock()
with patch("python_pkg.code_tutor._challenge._pytest_clean", return_value=True):
result = _validate_tests_against_real(
"def test_fn():\n assert True\n",
"# import",
Path("/proj"),
mock_console,
)
assert result is True
def test_validate_tests_fails() -> None:
mock_console = MagicMock()
with patch("python_pkg.code_tutor._challenge._pytest_clean", return_value=False):
result = _validate_tests_against_real(
"def test_fn():\n assert False\n",
"# import",
Path("/proj"),
mock_console,
)
assert result is False
# ---------------------------------------------------------------------------
# _collect_and_rate_tests
# ---------------------------------------------------------------------------
def test_collect_and_rate_tests_skip() -> None:
mock_backend = MagicMock()
mock_console = MagicMock()
live_mock = _make_live_mock()
with patch("python_pkg.code_tutor._verdict.Live", return_value=live_mock):
result = _collect_and_rate_tests(
"def fn():",
"explanation",
mock_backend,
mock_console,
lambda _: "skip",
)
assert result is None
def test_collect_and_rate_tests_pass_first() -> None:
mock_backend = MagicMock()
mock_console = MagicMock()
live_mock = _make_live_mock()
def fake_stream(system: str, user: str, on_token: object) -> str:
assert callable(on_token)
on_token('{"verdict": "PASS", "gap": ""}')
return ""
mock_backend.stream.side_effect = fake_stream
lines_iter = iter(["test code line", "END"])
with patch("python_pkg.code_tutor._verdict.Live", return_value=live_mock):
result = _collect_and_rate_tests(
"def fn():",
"explanation",
mock_backend,
mock_console,
lambda _: next(lines_iter),
)
assert result == "test code line"
def test_collect_and_rate_tests_fail_twice() -> None:
mock_backend = MagicMock()
mock_console = MagicMock()
live_mock = _make_live_mock()
mock_backend.stream.return_value = '{"verdict": "FAIL", "gap": "bad"}'
attempt_inputs = [
# attempt 1: provide code, then END
"test code",
"END",
# attempt 2: provide code, then END
"test code again",
"END",
]
inputs_iter = iter(attempt_inputs)
with patch("python_pkg.code_tutor._verdict.Live", return_value=live_mock):
result = _collect_and_rate_tests(
"def fn():",
"explanation",
mock_backend,
mock_console,
lambda _: next(inputs_iter),
)
assert result is None
def test_collect_and_rate_tests_fail_then_pass() -> None:
mock_backend = MagicMock()
mock_console = MagicMock()
live_mock = _make_live_mock()
call_count = [0]
def fake_stream(system: str, user: str, on_token: object) -> str:
assert callable(on_token)
call_count[0] += 1
verdict = (
'{"verdict": "FAIL", "gap": "not enough"}'
if call_count[0] == 1
else '{"verdict": "PASS", "gap": ""}'
)
on_token(verdict)
return ""
mock_backend.stream.side_effect = fake_stream
inputs = iter(["bad test", "END", "good test", "END"])
with patch("python_pkg.code_tutor._verdict.Live", return_value=live_mock):
result = _collect_and_rate_tests(
"def fn():",
"explanation",
mock_backend,
mock_console,
lambda _: next(inputs),
)
assert result == "good test"