Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -25,7 +31,85 @@ 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/<id>
* 204 => already present (no upload)
* 404 or 208 => upload via PUT
- PUT /jobePushFile/<id> 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
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.
Expand All @@ -42,50 +126,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):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,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) {
// 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
Expand Down
Loading