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