Skip to content

Commit 20a1b66

Browse files
authored
Merge pull request #1141 from xinyinghou/codetailor-runestone-dashboard
Fix CodeTailor Java version: : use JOBE to compile Java code in book_server
2 parents 801a379 + bf3b955 commit 20a1b66

4 files changed

Lines changed: 161 additions & 47 deletions

File tree

bases/rsptx/book_server_api/routers/personalized_parsons/end_to_end.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,11 @@ def request_fixed_code_from_openai(
138138
old_fixed_code=old_fixed_code,
139139
)
140140
unittest_result, cleaned_fixed_code = unittest_evaluation(
141-
language,
142-
fixed_code,
143-
default_start_code,
144-
default_test_code,
145-
unittest_case=unittest_code,
141+
language,
142+
fixed_code,
143+
default_start_code,
144+
default_test_code,
145+
unittest_code,
146146
)
147147

148148
print("this-round-result:", unittest_result, cleaned_fixed_code)

bases/rsptx/book_server_api/routers/personalized_parsons/evaluate_fixed_code.py

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
import tempfile
99
import os
1010
import shutil
11+
from unittest import result
12+
import requests as rq
13+
import hashlib
14+
import base64
15+
import json
16+
from ..rsproxy import get_jobe_server, settings
1117

1218

1319
class NullOutput:
@@ -25,7 +31,85 @@ class TimeoutError(Exception):
2531
def handler(signum, frame):
2632
raise TimeoutError("Test execution exceeded time limit")
2733

34+
def _runestone_file_id(filename: str, content: str) -> str:
35+
# Exactly: "runestone" + MD5(fileName + fileContent)
36+
md5 = hashlib.md5((filename + content).encode("utf-8")).hexdigest()
37+
return "runestone" + md5
2838

39+
def _b64_text_utf8(s: str) -> str:
40+
return base64.b64encode(s.encode("utf-8")).decode("ascii")
41+
42+
def _jobe_session():
43+
s = rq.Session()
44+
s.headers["Content-type"] = "application/json; charset=utf-8"
45+
s.headers["Accept"] = "application/json"
46+
if getattr(settings, "jobe_key", None):
47+
s.headers["X-API-KEY"] = settings.jobe_key
48+
return s
49+
50+
51+
def _ensure_file_on_jobe(sess: rq.Session, base_host: str, file_id: str, content: str) -> None:
52+
"""
53+
Mirrors JS logic:
54+
- HEAD /jobeCheckFile/<id>
55+
* 204 => already present (no upload)
56+
* 404 or 208 => upload via PUT
57+
- PUT /jobePushFile/<id> with {"file_contents": base64(content)}
58+
* expects 204 on success
59+
"""
60+
check_url = base_host + CHECK_PROXY + file_id
61+
r = sess.head(check_url, timeout=10)
62+
63+
if r.status_code == 204:
64+
return # already there
65+
66+
if r.status_code not in (404, 208):
67+
raise RuntimeError(f"Unexpected HEAD status from JOBE checkFile: {r.status_code} {r.text[:300]}")
68+
69+
put_url = base_host + PUSH_PROXY + file_id
70+
payload = {"file_contents": _b64_text_utf8(content)}
71+
pr = sess.put(
72+
put_url,
73+
data=json.dumps(payload),
74+
headers={"Content-type": "application/json", "Accept": "text/plain"},
75+
timeout=10,
76+
)
77+
if pr.status_code != 204:
78+
raise RuntimeError(f"Failed to push file to JOBE: {pr.status_code} {pr.text[:300]}")
79+
80+
# Match what the JS client uses
81+
PUSH_PROXY = "/ns/rsproxy/jobePushFile/"
82+
CHECK_PROXY = "/ns/rsproxy/jobeCheckFile/"
83+
84+
def inject_pass_fail_prints(test_code):
85+
"""
86+
Inserts System.out.println("PASS") before System.exit(0)
87+
and System.out.println("FAIL") + message before System.exit(1),
88+
inside the BackendTest main method.
89+
90+
Assumes test_code contains:
91+
public class BackendTest { public static void main(...) { ... } }
92+
"""
93+
94+
# Insert PASS before System.exit(0) if not already present
95+
if 'System.out.println("PASS")' not in test_code:
96+
test_code = re.sub(
97+
r"(TestHelper\.runAllTests\(\);\s*)(System\.exit\(0\);)",
98+
r'\1System.out.println("PASS");\n \2',
99+
test_code
100+
)
101+
102+
# Insert FAIL prints before System.exit(1) inside catch(Exception e)
103+
if 'System.out.println("FAIL")' not in test_code:
104+
test_code = re.sub(
105+
r"(catch\s*\(\s*Exception\s+e\s*\)\s*\{\s*)(System\.exit\(1\);)",
106+
r'\1System.out.println("FAIL");\n System.out.println(e.getMessage());\n \2',
107+
test_code
108+
)
109+
110+
return test_code
111+
112+
# modified from rsproxy.py and livecode.js logic
29113
def load_and_run_java_tests(java_code, test_code):
30114
"""
31115
Compile and run Java code with test cases.
@@ -42,50 +126,78 @@ def extract_class_name(code):
42126
return match.group(1)
43127
else:
44128
raise ValueError("Could not find a public class declaration.")
129+
130+
test_code = inject_pass_fail_prints(test_code)
131+
print("modified_test_code\n", test_code)
132+
student_class = extract_class_name(java_code)
133+
test_class = extract_class_name(test_code)
134+
135+
student_filename = f"{student_class}.java"
136+
test_filename = f"{test_class}.java"
137+
138+
# Runestone-style file ids: "runestone" + md5(filename + content)
139+
student_id = "runestone" + hashlib.md5((student_filename + java_code).encode("utf-8")).hexdigest()
140+
test_id = "runestone" + hashlib.md5((test_filename + test_code).encode("utf-8")).hexdigest()
141+
142+
runs_url = settings.jobe_server + "/jobe/index.php/restapi/runs/"
143+
student_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + student_id
144+
test_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + test_id
145+
146+
sess = rq.Session()
147+
sess.headers["Content-type"] = "application/json; charset=utf-8"
148+
sess.headers["Accept"] = "application/json"
149+
if getattr(settings, "jobe_key", None):
150+
sess.headers["X-API-KEY"] = settings.jobe_key
151+
152+
# base64 encode content for JOBE file store ---
153+
student_b64 = base64.b64encode(java_code.encode("utf-8")).decode("ascii")
154+
test_b64 = base64.b64encode(test_code.encode("utf-8")).decode("ascii")
45155

46-
temp_dir = tempfile.mkdtemp()
47156
try:
48-
# Extract class names from the code
49-
class_name = extract_class_name(java_code)
50-
test_class_name = extract_class_name(test_code)
51-
52-
# Write main Java file
53-
code_path = os.path.join(temp_dir, f"{class_name}.java")
54-
with open(code_path, "w") as f:
55-
f.write(java_code)
56-
57-
# Write test Java file
58-
test_path = os.path.join(temp_dir, f"{test_class_name}.java")
59-
with open(test_path, "w") as f:
60-
f.write(test_code)
61-
62-
# Compile both
63-
compile_result = subprocess.run(
64-
["javac", f"{class_name}.java", f"{test_class_name}.java"],
65-
cwd=temp_dir,
66-
capture_output=True,
67-
text=True,
68-
)
69-
if compile_result.returncode != 0:
70-
print("Compilation error:\n", compile_result.stderr)
71-
return False
157+
r = sess.head(student_file_url, timeout=10)
158+
if r.status_code != 204:
159+
# if not found (typically 404), push it
160+
put = sess.put(student_file_url, json={"file_contents": student_b64}, timeout=10)
161+
if put.status_code != 204:
162+
return False, {"error": "Failed to push student file", "status": put.status_code, "body": put.text[:500]}
163+
164+
r = sess.head(test_file_url, timeout=10)
165+
if r.status_code != 204:
166+
put = sess.put(test_file_url, json={"file_contents": test_b64}, timeout=10)
167+
if put.status_code != 204:
168+
return False, {"error": "Failed to push test file", "status": put.status_code, "body": put.text[:500]}
169+
170+
# JOBE runs this, and it calls test class main()
171+
runner_code = f"""public class TestRunner {{
172+
public static void main(String[] args) {{
173+
{test_class}.main(args);
174+
}}
175+
}}"""
176+
177+
runspec = {
178+
"language_id": "java",
179+
"sourcecode": runner_code,
180+
"sourcefilename": "",
181+
"parameters": {},
182+
"file_list": [
183+
[student_id, student_filename],
184+
[test_id, test_filename],
185+
],
186+
}
187+
188+
resp = sess.post(runs_url, json={"run_spec": runspec}, timeout=10)
72189

73-
# Run the test class
74-
run_result = subprocess.run(
75-
["java", test_class_name], cwd=temp_dir, capture_output=True, text=True
76-
)
190+
try:
191+
result = resp.json()
192+
except Exception:
193+
return False, {"error": "Non-JSON JOBE response", "status": resp.status_code, "body": resp.text[:800]}
77194

78-
if run_result.returncode == 0:
79-
return True
80-
else:
81-
return False
195+
out = (result.get("stdout") or "").strip()
196+
passed = (result.get("outcome") == 15) and out.startswith("PASS")
197+
return passed
82198

83-
except Exception as e:
84-
print("Error while running Java tests:", str(e))
199+
except Exception:
85200
return False
86-
finally:
87-
shutil.rmtree(temp_dir)
88-
89201

90202
def load_and_run_tests(unittest_case, code_to_test, time_limit=6):
91203
"""

bases/rsptx/book_server_api/routers/personalized_parsons/generate_parsons_blocks.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,11 @@ def generate_partial_Parsons(
204204
blocks = fixed_lines + unchanged_lines + matched_fixed_lines
205205
for fixed_line_key in distractor_tuple_dict.keys():
206206
blocks = [
207-
(line[0], line[1], line[2].rstrip() + " #matched-fixed\n")
208-
if line[2].strip() == fixed_line_key[2].strip()
209-
else (line[0], line[1], line[2])
207+
(
208+
(line[0], line[1], line[2].rstrip() + " #matched-fixed\n")
209+
if line[2].strip() == fixed_line_key[2].strip()
210+
else (line[0], line[1], line[2])
211+
)
210212
for line in blocks
211213
]
212214
fixed_line_code = fixed_line_key[2]

bases/rsptx/interactives/runestone/activecode/js/activecode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export class ActiveCode extends RunestoneBase {
550550
// This function is used to convert JUnit test code to a format suitable for backend processing.
551551
function junitToBackend(junitCode) {
552552
// Extract only the TestHelper class - match from the first line to the first empty line after it
553-
const helperMatch = junitCode.match(/class TestHelper[\s\S]*?\n\s*\n/);
553+
const helperMatch = junitCode.match(/class\s+TestHelper\s*\{[\s\S]*?\}\s*/);
554554
const helperCode = helperMatch ? helperMatch[0] : "";
555555

556556
// Add backend runner - it always calls TestHelper

0 commit comments

Comments
 (0)