Skip to content

Commit 682e940

Browse files
committed
Applied JOBE for submitting Javacode with unit tests and get results
1 parent 8bb67a0 commit 682e940

File tree

4 files changed

+158
-54
lines changed

4 files changed

+158
-54
lines changed

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: 152 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,87 @@ 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+
API_KEY = "67033pV7eUUvqo07OJDIV8UZ049aLEK1"
82+
RUN_PROXY = "/ns/rsproxy/jobeRun"
83+
PUSH_PROXY = "/ns/rsproxy/jobePushFile/"
84+
CHECK_PROXY = "/ns/rsproxy/jobeCheckFile/"
85+
86+
def inject_pass_fail_prints(test_code):
87+
"""
88+
Inserts System.out.println("PASS") before System.exit(0)
89+
and System.out.println("FAIL") + message before System.exit(1),
90+
inside the BackendTest main method.
91+
92+
Assumes test_code contains:
93+
public class BackendTest { public static void main(...) { ... } }
94+
"""
95+
96+
# Insert PASS before System.exit(0) if not already present
97+
if 'System.out.println("PASS")' not in test_code:
98+
test_code = re.sub(
99+
r"(TestHelper\.runAllTests\(\);\s*)(System\.exit\(0\);)",
100+
r'\1System.out.println("PASS");\n \2',
101+
test_code
102+
)
103+
104+
# Insert FAIL prints before System.exit(1) inside catch(Exception e)
105+
if 'System.out.println("FAIL")' not in test_code:
106+
test_code = re.sub(
107+
r"(catch\s*\(\s*Exception\s+e\s*\)\s*\{\s*)(System\.exit\(1\);)",
108+
r'\1System.out.println("FAIL");\n System.out.println(e.getMessage());\n \2',
109+
test_code
110+
)
111+
112+
return test_code
113+
114+
# modified from rsproxy.py and livecode.js logic
29115
def load_and_run_java_tests(java_code, test_code):
30116
"""
31117
Compile and run Java code with test cases.
@@ -42,50 +128,78 @@ def extract_class_name(code):
42128
return match.group(1)
43129
else:
44130
raise ValueError("Could not find a public class declaration.")
131+
132+
test_code = inject_pass_fail_prints(test_code)
133+
print("modified_test_code\n", test_code)
134+
student_class = extract_class_name(java_code)
135+
test_class = extract_class_name(test_code)
136+
137+
student_filename = f"{student_class}.java"
138+
test_filename = f"{test_class}.java"
139+
140+
# Runestone-style file ids: "runestone" + md5(filename + content)
141+
student_id = "runestone" + hashlib.md5((student_filename + java_code).encode("utf-8")).hexdigest()
142+
test_id = "runestone" + hashlib.md5((test_filename + test_code).encode("utf-8")).hexdigest()
143+
144+
runs_url = settings.jobe_server + "/jobe/index.php/restapi/runs/"
145+
student_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + student_id
146+
test_file_url = settings.jobe_server + "/jobe/index.php/restapi/files/" + test_id
147+
148+
sess = rq.Session()
149+
sess.headers["Content-type"] = "application/json; charset=utf-8"
150+
sess.headers["Accept"] = "application/json"
151+
if getattr(settings, "jobe_key", None):
152+
sess.headers["X-API-KEY"] = settings.jobe_key
153+
154+
# base64 encode content for JOBE file store ---
155+
student_b64 = base64.b64encode(java_code.encode("utf-8")).decode("ascii")
156+
test_b64 = base64.b64encode(test_code.encode("utf-8")).decode("ascii")
45157

46-
temp_dir = tempfile.mkdtemp()
47158
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
159+
r = sess.head(student_file_url, timeout=10)
160+
if r.status_code != 204:
161+
# if not found (typically 404), push it
162+
put = sess.put(student_file_url, json={"file_contents": student_b64}, timeout=10)
163+
if put.status_code != 204:
164+
return False, {"error": "Failed to push student file", "status": put.status_code, "body": put.text[:500]}
165+
166+
r = sess.head(test_file_url, timeout=10)
167+
if r.status_code != 204:
168+
put = sess.put(test_file_url, json={"file_contents": test_b64}, timeout=10)
169+
if put.status_code != 204:
170+
return False, {"error": "Failed to push test file", "status": put.status_code, "body": put.text[:500]}
171+
172+
# JOBE runs this, and it calls test class main()
173+
runner_code = f"""public class TestRunner {{
174+
public static void main(String[] args) {{
175+
{test_class}.main(args);
176+
}}
177+
}}"""
178+
179+
runspec = {
180+
"language_id": "java",
181+
"sourcecode": runner_code,
182+
"sourcefilename": "",
183+
"parameters": {},
184+
"file_list": [
185+
[student_id, student_filename],
186+
[test_id, test_filename],
187+
],
188+
}
189+
190+
resp = sess.post(runs_url, json={"run_spec": runspec}, timeout=10)
72191

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

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

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

90204
def load_and_run_tests(unittest_case, code_to_test, time_limit=6):
91205
"""

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,8 @@ export class ActiveCode extends RunestoneBase {
549549

550550
// This function is used to convert JUnit test code to a format suitable for backend processing.
551551
function junitToBackend(junitCode) {
552-
console.log("Original JUnit code:", junitCode);
553552
// Extract only the TestHelper class - match from the first line to the first empty line after it
554-
const helperMatch = junitCode.match(/class TestHelper[\s\S]*?\n\s*\n/);
553+
const helperMatch = junitCode.match(/class\s+TestHelper\s*\{[\s\S]*?\}\s*/);
555554
const helperCode = helperMatch ? helperMatch[0] : "";
556555

557556
// Add backend runner - it always calls TestHelper

projects/book_server/Dockerfile

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,6 @@ RUN rm /usr/src/app/$wheel
3838
FROM python:3.13-slim
3939
WORKDIR /usr/src/app
4040

41-
# Install Java JDK (so `javac` is available)
42-
RUN apt-get update && apt-get install -y --no-install-recommends \
43-
default-jdk ca-certificates \
44-
&& rm -rf /var/lib/apt/lists/*
45-
46-
# Set Java environment variables
47-
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
48-
ENV PATH="$JAVA_HOME/bin:$PATH"
49-
5041
COPY --from=builder /usr/local /usr/local
5142

5243
CMD ["uvicorn", "rsptx.book_server_api.main:app", "--host", "0.0.0.0", "--port", "8000"]

0 commit comments

Comments
 (0)