From 0d13f56d7c5fc26482bc2d95c128d312d06b55a7 Mon Sep 17 00:00:00 2001 From: Aadarsh Padiyath Date: Tue, 13 Oct 2026 12:58:23 -0400 Subject: [PATCH] Let CodeTailor accept passing solutions on python3 questions CodeTailor only cleaned a question's test code when its language was exactly "python", so "python3" questions kept their own unittest.main() call. That call exits before JOBE prints PASS, so every generated solution read as failing and help fell back to the example solution. Clean for any Python language, and strip unittest.main(...) as well as myTests().main(). Co-Authored-By: Claude Opus 5.5 --- bases/rsptx/book_server_api/routers/coach.py | 12 ++++- .../test_clean_python_testcase.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/bases/rsptx/book_server_api/test_clean_python_testcase.py diff --git a/bases/rsptx/book_server_api/routers/coach.py b/bases/rsptx/book_server_api/routers/coach.py index eb3343f2e..7200bce15 100644 --- a/bases/rsptx/book_server_api/routers/coach.py +++ b/bases/rsptx/book_server_api/routers/coach.py @@ -114,7 +114,14 @@ def clean_python_testcase(raw_test_code: str) -> str: result = result.replace( "class myTests(TestCaseGui):", "class myTests(unittest.TestCase):" ) - result = re.sub(r"^\s*myTests\(\)\.main\(\)\s*$", "", result, flags=re.MULTILINE) + # A test file's own runner call (myTests().main() or unittest.main(...)) + # exits before JOBE's PASS line prints, which reads as a failed test. + result = re.sub( + r"^\s*(?:myTests\(\)|unittest)\.main\(.*\)\s*$", + "", + result, + flags=re.MULTILINE, + ) return result @@ -718,7 +725,8 @@ async def parsons_scaffolding( status_code=status.HTTP_400_BAD_REQUEST, ) - if language and language.lower() == "python": + # Questions say "python3" as often as "python" + if language and language.lower().startswith("python"): internal_test_case = clean_python_testcase(internal_test_case) # Files the program opens (datafiles) must travel with each JOBE test run, diff --git a/test/bases/rsptx/book_server_api/test_clean_python_testcase.py b/test/bases/rsptx/book_server_api/test_clean_python_testcase.py new file mode 100644 index 000000000..103fc25e9 --- /dev/null +++ b/test/bases/rsptx/book_server_api/test_clean_python_testcase.py @@ -0,0 +1,46 @@ +""" +Tests for clean_python_testcase, which turns a question's test code into +something CodeTailor can run on JOBE. + +CodeTailor appends its own runner and reads a final PASS line. A runner call +left in the test code (unittest.main() exits the program) ran the tests first +and exited, so no PASS printed and every generated solution fell back to the +example solution. +""" + +import pytest + +from rsptx.book_server_api.routers.coach import clean_python_testcase + +_TESTS = """\ +import unittest + +class myTests(unittest.TestCase): + + def testOne(self): + self.assertEqual(find_lowest_close()[1], "1-Feb-89") +""" + + +@pytest.mark.parametrize( + "runner", + ["unittest.main()", "unittest.main(verbosity=2)", "myTests().main()"], +) +def test_runner_call_is_removed(runner): + cleaned = clean_python_testcase(_TESTS + "\n" + runner + "\n") + assert ".main(" not in cleaned + assert "def testOne(self):" in cleaned + + +def test_browser_style_tests_become_unittest(): + raw = ( + "from unittest.gui import TestCaseGui\n" + "class myTests(TestCaseGui):\n" + " def testOne(self):\n" + " self.assertTrue(True)\n" + "myTests().main()\n" + ) + cleaned = clean_python_testcase(raw) + assert "import unittest\n" in cleaned + assert "class myTests(unittest.TestCase):" in cleaned + assert ".main(" not in cleaned