mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:47:01 +01:00
New python_pkg/code_tutor package that turns an unfamiliar codebase into an interactive lesson: analyse code into items (_analyzer), order them dependency-first (_plan_builder), pose reimplement challenges (_challenge + _challenge_support), verify user implementations with tests and coverage (_verifier), track progress (_progress/_session), and offer local-LLM hints (_llm), driven by a Typer/Rich CLI (cli). - meta/pyproject.toml + .pre-commit-config.yaml + meta/requirements.txt: add anthropic/rich/typer deps, a cli.py per-file-ignore (Typer B008 etc), and code_tutor-specific ruff/pylint/mypy tuning. - _challenge.py and its tests are split across _challenge_support.py and test_challenge_part2/part3.py to stay under the 500-line file cap. Verification: pytest 631 passed at 100% line+branch coverage; every file <=500 lines; pre-commit (ruff ALL, mypy strict, pylint 10/10, bandit, evidence + multi-file contract) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
374 lines
11 KiB
Python
374 lines
11 KiB
Python
"""Tests for python_pkg.code_tutor._challenge -- part 3.
|
|
|
|
Covers the user-implementation runner and the two challenge flows plus the
|
|
public entry point: ``_run_user_impl``, ``_write_tests_first_flow``,
|
|
``_existing_tests_flow`` and ``run_coding_challenge``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from python_pkg.code_tutor._challenge import (
|
|
_existing_tests_flow,
|
|
_run_user_impl,
|
|
_write_tests_first_flow,
|
|
run_coding_challenge,
|
|
)
|
|
from python_pkg.code_tutor.tests.conftest import _item, _make_live_mock
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _run_user_impl
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_run_user_impl_skip(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
live_mock = _make_live_mock()
|
|
|
|
with (
|
|
patch("python_pkg.code_tutor._challenge_support.Live", return_value=live_mock),
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
):
|
|
result = _run_user_impl(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"def test_fn(): pass",
|
|
"# import",
|
|
mock_console,
|
|
lambda _: "skip",
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_run_user_impl_syntax_error(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
inputs = iter(["def (broken):", ""])
|
|
|
|
with patch("python_pkg.code_tutor._challenge.Syntax"):
|
|
result = _run_user_impl(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"def test_fn(): pass",
|
|
"# import",
|
|
mock_console,
|
|
lambda _: next(inputs, "END") if _ == "" else "END",
|
|
)
|
|
|
|
# Provide the END and the broken implementation
|
|
inputs2 = iter(["def (broken):", "END"])
|
|
|
|
with patch("python_pkg.code_tutor._challenge.Syntax"):
|
|
result = _run_user_impl(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"def test_fn(): pass",
|
|
"# import",
|
|
mock_console,
|
|
lambda _: next(inputs2),
|
|
)
|
|
assert result == "failed"
|
|
|
|
|
|
def test_run_user_impl_passed(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
inputs = iter(["def fn():", " return 42", "END"])
|
|
|
|
with (
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
patch("python_pkg.code_tutor._challenge._pytest_clean", return_value=True),
|
|
):
|
|
result = _run_user_impl(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"def test_fn(): pass",
|
|
"# import",
|
|
mock_console,
|
|
lambda _: next(inputs),
|
|
)
|
|
assert result == "passed"
|
|
# File restored
|
|
assert (tmp_path / "mod.py").read_text(encoding="utf-8") == source
|
|
|
|
|
|
def test_run_user_impl_failed(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
inputs = iter(["def fn():", " return 0", "END"])
|
|
|
|
with (
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
patch("python_pkg.code_tutor._challenge._pytest_clean", return_value=False),
|
|
):
|
|
result = _run_user_impl(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"def test_fn(): pass",
|
|
"# import",
|
|
mock_console,
|
|
lambda _: next(inputs),
|
|
)
|
|
assert result == "failed"
|
|
assert (tmp_path / "mod.py").read_text(encoding="utf-8") == source
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _write_tests_first_flow
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_write_tests_first_flow_user_declines(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
mock_backend = MagicMock()
|
|
mock_console = MagicMock()
|
|
|
|
result = _write_tests_first_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
lambda _: "n",
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_write_tests_first_flow_tests_none(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
mock_backend = MagicMock()
|
|
mock_console = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._collect_and_rate_tests",
|
|
return_value=None,
|
|
),
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
):
|
|
result = _write_tests_first_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
lambda _: "y",
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_write_tests_first_flow_tests_fail_real(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
mock_backend = MagicMock()
|
|
mock_console = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._collect_and_rate_tests",
|
|
return_value="test code",
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._validate_tests_against_real",
|
|
return_value=False,
|
|
),
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
):
|
|
result = _write_tests_first_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
lambda _: "y",
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_write_tests_first_flow_success(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
mock_backend = MagicMock()
|
|
mock_console = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._collect_and_rate_tests",
|
|
return_value="test code",
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._validate_tests_against_real",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge._run_user_impl",
|
|
return_value="passed",
|
|
),
|
|
patch("python_pkg.code_tutor._challenge.Syntax"),
|
|
):
|
|
result = _write_tests_first_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
lambda _: "y",
|
|
)
|
|
assert result == "passed"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _existing_tests_flow
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_existing_tests_flow_user_declines(tmp_path: Path) -> None:
|
|
test_file = tmp_path / "test_mod.py"
|
|
test_file.write_text("def test_fn(): fn()\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
result = _existing_tests_flow(
|
|
_item(),
|
|
str(tmp_path),
|
|
"explanation",
|
|
[(test_file, ["test_fn"])],
|
|
mock_console,
|
|
lambda _: "n",
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_existing_tests_flow_user_skips_impl(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
test_file = tmp_path / "test_mod.py"
|
|
test_file.write_text("def test_fn(): fn()\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
|
|
with patch("python_pkg.code_tutor._challenge_support.Syntax"):
|
|
result = _existing_tests_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
[(test_file, ["test_fn"])],
|
|
mock_console,
|
|
lambda _: (
|
|
"y"
|
|
if "challenge" in str(_) or _ == "Take the challenge? [y/N] "
|
|
else "skip"
|
|
),
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_existing_tests_flow_passes(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
test_file = tmp_path / "test_mod.py"
|
|
test_file.write_text("def test_fn(): fn()\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
|
|
inputs = iter(["y", "def fn():", " return 1", "END"])
|
|
|
|
with (
|
|
patch("python_pkg.code_tutor._challenge_support.Syntax"),
|
|
patch("python_pkg.code_tutor._challenge._patch_and_test", return_value=True),
|
|
):
|
|
result = _existing_tests_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
[(test_file, ["test_fn"])],
|
|
mock_console,
|
|
lambda _: next(inputs),
|
|
)
|
|
assert result == "passed"
|
|
|
|
|
|
def test_existing_tests_flow_fails(tmp_path: Path) -> None:
|
|
source = "def fn():\n return 1\n"
|
|
(tmp_path / "mod.py").write_text(source, encoding="utf-8")
|
|
test_file = tmp_path / "test_mod.py"
|
|
test_file.write_text("def test_fn(): fn()\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
|
|
inputs = iter(["y", "def fn():", " return 0", "END"])
|
|
|
|
with (
|
|
patch("python_pkg.code_tutor._challenge_support.Syntax"),
|
|
patch("python_pkg.code_tutor._challenge._patch_and_test", return_value=False),
|
|
):
|
|
result = _existing_tests_flow(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
[(test_file, ["test_fn"])],
|
|
mock_console,
|
|
lambda _: next(inputs),
|
|
)
|
|
assert result == "failed"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_coding_challenge
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_run_coding_challenge_non_python() -> None:
|
|
mock_console = MagicMock()
|
|
mock_backend = MagicMock()
|
|
result = run_coding_challenge(
|
|
_item(file="main.go"),
|
|
"/codebase",
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
)
|
|
assert result == "skipped"
|
|
|
|
|
|
def test_run_coding_challenge_with_existing_tests(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
test_file = tmp_path / "test_mod.py"
|
|
test_file.write_text("def test_fn(): fn()\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
mock_backend = MagicMock()
|
|
|
|
with patch(
|
|
"python_pkg.code_tutor._challenge._existing_tests_flow", return_value="passed"
|
|
):
|
|
result = run_coding_challenge(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
)
|
|
assert result == "passed"
|
|
|
|
|
|
def test_run_coding_challenge_no_tests(tmp_path: Path) -> None:
|
|
(tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
|
|
mock_console = MagicMock()
|
|
mock_backend = MagicMock()
|
|
|
|
with patch(
|
|
"python_pkg.code_tutor._challenge._write_tests_first_flow",
|
|
return_value="skipped",
|
|
):
|
|
result = run_coding_challenge(
|
|
_item(file="mod.py"),
|
|
str(tmp_path),
|
|
"explanation",
|
|
mock_backend,
|
|
mock_console,
|
|
)
|
|
assert result == "skipped"
|