88import tempfile
99import os
1010import 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
1319class NullOutput :
@@ -25,7 +31,87 @@ class TimeoutError(Exception):
2531def 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
29115def 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
90204def load_and_run_tests (unittest_case , code_to_test , time_limit = 6 ):
91205 """
0 commit comments