From 8bb67a004a1bcc35631786c8b5ac5d98e3737276 Mon Sep 17 00:00:00 2001 From: xinyinghou Date: Fri, 6 Feb 2026 20:45:43 +0000 Subject: [PATCH 1/3] updated dockerfile to include javac for book_server_api java code testing --- .../personalized_parsons/generate_parsons_blocks.py | 8 +++++--- .../interactives/runestone/activecode/js/activecode.js | 1 + projects/book_server/Dockerfile | 9 +++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/bases/rsptx/book_server_api/routers/personalized_parsons/generate_parsons_blocks.py b/bases/rsptx/book_server_api/routers/personalized_parsons/generate_parsons_blocks.py index 550fc1142..527ab4c3a 100644 --- a/bases/rsptx/book_server_api/routers/personalized_parsons/generate_parsons_blocks.py +++ b/bases/rsptx/book_server_api/routers/personalized_parsons/generate_parsons_blocks.py @@ -204,9 +204,11 @@ def generate_partial_Parsons( blocks = fixed_lines + unchanged_lines + matched_fixed_lines for fixed_line_key in distractor_tuple_dict.keys(): blocks = [ - (line[0], line[1], line[2].rstrip() + " #matched-fixed\n") - if line[2].strip() == fixed_line_key[2].strip() - else (line[0], line[1], line[2]) + ( + (line[0], line[1], line[2].rstrip() + " #matched-fixed\n") + if line[2].strip() == fixed_line_key[2].strip() + else (line[0], line[1], line[2]) + ) for line in blocks ] fixed_line_code = fixed_line_key[2] diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode.js b/bases/rsptx/interactives/runestone/activecode/js/activecode.js index ff86e69a1..6720c9500 100755 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode.js @@ -549,6 +549,7 @@ export class ActiveCode extends RunestoneBase { // This function is used to convert JUnit test code to a format suitable for backend processing. function junitToBackend(junitCode) { + console.log("Original JUnit code:", junitCode); // Extract only the TestHelper class - match from the first line to the first empty line after it const helperMatch = junitCode.match(/class TestHelper[\s\S]*?\n\s*\n/); const helperCode = helperMatch ? helperMatch[0] : ""; diff --git a/projects/book_server/Dockerfile b/projects/book_server/Dockerfile index 84e23180a..710931537 100644 --- a/projects/book_server/Dockerfile +++ b/projects/book_server/Dockerfile @@ -38,6 +38,15 @@ RUN rm /usr/src/app/$wheel FROM python:3.13-slim WORKDIR /usr/src/app +# Install Java JDK (so `javac` is available) +RUN apt-get update && apt-get install -y --no-install-recommends \ + default-jdk ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Set Java environment variables +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +ENV PATH="$JAVA_HOME/bin:$PATH" + COPY --from=builder /usr/local /usr/local CMD ["uvicorn", "rsptx.book_server_api.main:app", "--host", "0.0.0.0", "--port", "8000"] From 682e940d804dda8a93309b1f2f331c12ac2aaa41 Mon Sep 17 00:00:00 2001 From: xinyinghou Date: Sat, 7 Feb 2026 11:31:49 +0000 Subject: [PATCH 2/3] Applied JOBE for submitting Javacode with unit tests and get results --- .../personalized_parsons/end_to_end.py | 10 +- .../evaluate_fixed_code.py | 190 ++++++++++++++---- .../runestone/activecode/js/activecode.js | 3 +- projects/book_server/Dockerfile | 9 - 4 files changed, 158 insertions(+), 54 deletions(-) diff --git a/bases/rsptx/book_server_api/routers/personalized_parsons/end_to_end.py b/bases/rsptx/book_server_api/routers/personalized_parsons/end_to_end.py index d47127ef1..6ad418c98 100644 --- a/bases/rsptx/book_server_api/routers/personalized_parsons/end_to_end.py +++ b/bases/rsptx/book_server_api/routers/personalized_parsons/end_to_end.py @@ -138,11 +138,11 @@ def request_fixed_code_from_openai( old_fixed_code=old_fixed_code, ) unittest_result, cleaned_fixed_code = unittest_evaluation( - language, - fixed_code, - default_start_code, - default_test_code, - unittest_case=unittest_code, + language, + fixed_code, + default_start_code, + default_test_code, + unittest_code, ) print("this-round-result:", unittest_result, cleaned_fixed_code) diff --git a/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py b/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py index e3ec3dec2..d68e1e697 100644 --- a/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py +++ b/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py @@ -8,6 +8,12 @@ import tempfile import os import shutil +from unittest import result +import requests as rq +import hashlib +import base64 +import json +from ..rsproxy import get_jobe_server, settings class NullOutput: @@ -25,7 +31,87 @@ class TimeoutError(Exception): def handler(signum, frame): raise TimeoutError("Test execution exceeded time limit") +def _runestone_file_id(filename: str, content: str) -> str: + # Exactly: "runestone" + MD5(fileName + fileContent) + md5 = hashlib.md5((filename + content).encode("utf-8")).hexdigest() + return "runestone" + md5 +def _b64_text_utf8(s: str) -> str: + return base64.b64encode(s.encode("utf-8")).decode("ascii") + +def _jobe_session(): + s = rq.Session() + s.headers["Content-type"] = "application/json; charset=utf-8" + s.headers["Accept"] = "application/json" + if getattr(settings, "jobe_key", None): + s.headers["X-API-KEY"] = settings.jobe_key + return s + + +def _ensure_file_on_jobe(sess: rq.Session, base_host: str, file_id: str, content: str) -> None: + """ + Mirrors JS logic: + - HEAD /jobeCheckFile/ + * 204 => already present (no upload) + * 404 or 208 => upload via PUT + - PUT /jobePushFile/ with {"file_contents": base64(content)} + * expects 204 on success + """ + check_url = base_host + CHECK_PROXY + file_id + r = sess.head(check_url, timeout=10) + + if r.status_code == 204: + return # already there + + if r.status_code not in (404, 208): + raise RuntimeError(f"Unexpected HEAD status from JOBE checkFile: {r.status_code} {r.text[:300]}") + + put_url = base_host + PUSH_PROXY + file_id + payload = {"file_contents": _b64_text_utf8(content)} + pr = sess.put( + put_url, + data=json.dumps(payload), + headers={"Content-type": "application/json", "Accept": "text/plain"}, + timeout=10, + ) + if pr.status_code != 204: + raise RuntimeError(f"Failed to push file to JOBE: {pr.status_code} {pr.text[:300]}") + +# Match what the JS client uses +API_KEY = "67033pV7eUUvqo07OJDIV8UZ049aLEK1" +RUN_PROXY = "/ns/rsproxy/jobeRun" +PUSH_PROXY = "/ns/rsproxy/jobePushFile/" +CHECK_PROXY = "/ns/rsproxy/jobeCheckFile/" + +def inject_pass_fail_prints(test_code): + """ + Inserts System.out.println("PASS") before System.exit(0) + and System.out.println("FAIL") + message before System.exit(1), + inside the BackendTest main method. + + Assumes test_code contains: + public class BackendTest { public static void main(...) { ... } } + """ + + # Insert PASS before System.exit(0) if not already present + if 'System.out.println("PASS")' not in test_code: + test_code = re.sub( + r"(TestHelper\.runAllTests\(\);\s*)(System\.exit\(0\);)", + r'\1System.out.println("PASS");\n \2', + test_code + ) + + # Insert FAIL prints before System.exit(1) inside catch(Exception e) + if 'System.out.println("FAIL")' not in test_code: + test_code = re.sub( + r"(catch\s*\(\s*Exception\s+e\s*\)\s*\{\s*)(System\.exit\(1\);)", + r'\1System.out.println("FAIL");\n System.out.println(e.getMessage());\n \2', + test_code + ) + + return test_code + +# modified from rsproxy.py and livecode.js logic def load_and_run_java_tests(java_code, test_code): """ Compile and run Java code with test cases. @@ -42,50 +128,78 @@ def extract_class_name(code): return match.group(1) else: raise ValueError("Could not find a public class declaration.") + + test_code = inject_pass_fail_prints(test_code) + print("modified_test_code\n", test_code) + student_class = extract_class_name(java_code) + test_class = extract_class_name(test_code) + + student_filename = f"{student_class}.java" + test_filename = f"{test_class}.java" + + # Runestone-style file ids: "runestone" + md5(filename + content) + student_id = "runestone" + hashlib.md5((student_filename + java_code).encode("utf-8")).hexdigest() + test_id = "runestone" + hashlib.md5((test_filename + test_code).encode("utf-8")).hexdigest() + + runs_url = settings.jobe_server + "/jobe/index.php/restapi/runs/" + student_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + student_id + test_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + test_id + + sess = rq.Session() + sess.headers["Content-type"] = "application/json; charset=utf-8" + sess.headers["Accept"] = "application/json" + if getattr(settings, "jobe_key", None): + sess.headers["X-API-KEY"] = settings.jobe_key + + # base64 encode content for JOBE file store --- + student_b64 = base64.b64encode(java_code.encode("utf-8")).decode("ascii") + test_b64 = base64.b64encode(test_code.encode("utf-8")).decode("ascii") - temp_dir = tempfile.mkdtemp() try: - # Extract class names from the code - class_name = extract_class_name(java_code) - test_class_name = extract_class_name(test_code) - - # Write main Java file - code_path = os.path.join(temp_dir, f"{class_name}.java") - with open(code_path, "w") as f: - f.write(java_code) - - # Write test Java file - test_path = os.path.join(temp_dir, f"{test_class_name}.java") - with open(test_path, "w") as f: - f.write(test_code) - - # Compile both - compile_result = subprocess.run( - ["javac", f"{class_name}.java", f"{test_class_name}.java"], - cwd=temp_dir, - capture_output=True, - text=True, - ) - if compile_result.returncode != 0: - print("Compilation error:\n", compile_result.stderr) - return False + r = sess.head(student_file_url, timeout=10) + if r.status_code != 204: + # if not found (typically 404), push it + put = sess.put(student_file_url, json={"file_contents": student_b64}, timeout=10) + if put.status_code != 204: + return False, {"error": "Failed to push student file", "status": put.status_code, "body": put.text[:500]} + + r = sess.head(test_file_url, timeout=10) + if r.status_code != 204: + put = sess.put(test_file_url, json={"file_contents": test_b64}, timeout=10) + if put.status_code != 204: + return False, {"error": "Failed to push test file", "status": put.status_code, "body": put.text[:500]} + + # JOBE runs this, and it calls test class main() + runner_code = f"""public class TestRunner {{ + public static void main(String[] args) {{ + {test_class}.main(args); + }} + }}""" + + runspec = { + "language_id": "java", + "sourcecode": runner_code, + "sourcefilename": "", + "parameters": {}, + "file_list": [ + [student_id, student_filename], + [test_id, test_filename], + ], + } + + resp = sess.post(runs_url, json={"run_spec": runspec}, timeout=10) - # Run the test class - run_result = subprocess.run( - ["java", test_class_name], cwd=temp_dir, capture_output=True, text=True - ) + try: + result = resp.json() + except Exception: + return False, {"error": "Non-JSON JOBE response", "status": resp.status_code, "body": resp.text[:800]} - if run_result.returncode == 0: - return True - else: - return False + out = (result.get("stdout") or "").strip() + passed = (result.get("outcome") == 15) and out.startswith("PASS") + return passed - except Exception as e: - print("Error while running Java tests:", str(e)) + except Exception: return False - finally: - shutil.rmtree(temp_dir) - def load_and_run_tests(unittest_case, code_to_test, time_limit=6): """ diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode.js b/bases/rsptx/interactives/runestone/activecode/js/activecode.js index 6720c9500..3138cb9c3 100755 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode.js @@ -549,9 +549,8 @@ export class ActiveCode extends RunestoneBase { // This function is used to convert JUnit test code to a format suitable for backend processing. function junitToBackend(junitCode) { - console.log("Original JUnit code:", junitCode); // Extract only the TestHelper class - match from the first line to the first empty line after it - const helperMatch = junitCode.match(/class TestHelper[\s\S]*?\n\s*\n/); + const helperMatch = junitCode.match(/class\s+TestHelper\s*\{[\s\S]*?\}\s*/); const helperCode = helperMatch ? helperMatch[0] : ""; // Add backend runner - it always calls TestHelper diff --git a/projects/book_server/Dockerfile b/projects/book_server/Dockerfile index 710931537..84e23180a 100644 --- a/projects/book_server/Dockerfile +++ b/projects/book_server/Dockerfile @@ -38,15 +38,6 @@ RUN rm /usr/src/app/$wheel FROM python:3.13-slim WORKDIR /usr/src/app -# Install Java JDK (so `javac` is available) -RUN apt-get update && apt-get install -y --no-install-recommends \ - default-jdk ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Set Java environment variables -ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 -ENV PATH="$JAVA_HOME/bin:$PATH" - COPY --from=builder /usr/local /usr/local CMD ["uvicorn", "rsptx.book_server_api.main:app", "--host", "0.0.0.0", "--port", "8000"] From bf3b95536d9ea01e80fc779dac259bf595c2d9d3 Mon Sep 17 00:00:00 2001 From: xinyinghou Date: Sat, 7 Feb 2026 11:34:21 +0000 Subject: [PATCH 3/3] removed unused variables --- .../routers/personalized_parsons/evaluate_fixed_code.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py b/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py index d68e1e697..749436502 100644 --- a/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py +++ b/bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py @@ -78,8 +78,6 @@ def _ensure_file_on_jobe(sess: rq.Session, base_host: str, file_id: str, content raise RuntimeError(f"Failed to push file to JOBE: {pr.status_code} {pr.text[:300]}") # Match what the JS client uses -API_KEY = "67033pV7eUUvqo07OJDIV8UZ049aLEK1" -RUN_PROXY = "/ns/rsproxy/jobeRun" PUSH_PROXY = "/ns/rsproxy/jobePushFile/" CHECK_PROXY = "/ns/rsproxy/jobeCheckFile/"