mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:47:12 +01:00
_challenge.py 467 -> 236. The two flows and run_coding_challenge go to _challenge_flows.py; the three static prompt strings to _challenge_prompts.py, which covered the last eleven lines after the first cut landed at 261. The 13 patch strings in test_challenge_part3.py were repointed per test, by which function is under test - the _run_user_impl tests keep _challenge. Getting that wrong does not fail here, it HANGS: an unintercepted _collect_and_rate_tests ran the real interactive flow and spun the suite for two minutes. Runs are now under an explicit timeout, and the patch targets are asserted to resolve before pytest starts. Pass count is 216 before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
379 lines
12 KiB
Python
379 lines
12 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 _run_user_impl
|
|
from python_pkg.code_tutor._challenge_flows import (
|
|
_existing_tests_flow,
|
|
_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._verdict.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_flows._collect_and_rate_tests",
|
|
return_value=None,
|
|
),
|
|
patch("python_pkg.code_tutor._challenge_flows.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_flows._collect_and_rate_tests",
|
|
return_value="test code",
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge_flows._validate_tests_against_real",
|
|
return_value=False,
|
|
),
|
|
patch("python_pkg.code_tutor._challenge_flows.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_flows._collect_and_rate_tests",
|
|
return_value="test code",
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge_flows._validate_tests_against_real",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"python_pkg.code_tutor._challenge_flows._run_user_impl",
|
|
return_value="passed",
|
|
),
|
|
patch("python_pkg.code_tutor._challenge_flows.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_flows._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_flows._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_flows._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_flows._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"
|