1- import enum
2- import json
1+ import os
32import subprocess
4- from typing import Dict , Optional , IO , Type
5-
3+ import tempfile
4+ import xml .etree .ElementTree as eTree
5+ from glob import glob
6+ from typing import Type , List , Set
67from testers .specs import TestSpecs
78from testers .tester import Tester , Test , TestError
89
910
1011class JavaTest (Test ):
11- class JUnitStatus (enum .Enum ):
12- SUCCESSFUL = 1
13- ABORTED = 2
14- FAILED = 3
15-
16- ERRORS = {
17- "bad_javac" : 'Java compilation error: "{}"' ,
18- "bad_java" : 'Java runtime error: "{}"' ,
19- }
20-
21- def __init__ (self , tester : "JavaTester" , result : Dict , feedback_open : Optional [IO ] = None ,) -> None :
22- """
23- Initialize a Java test created by tester.
24-
25- The result was created after running some junit tests.
26- Test feedback will be written to feedback_open.
27- """
28- self .class_name , _sep , self .method_name = result ["name" ].partition ("." )
29- self .description = result .get ("description" )
30- self .status = JavaTest .JUnitStatus [result ["status" ]]
31- self .message = result .get ("message" )
12+ def __init__ (self , tester , result , feedback_open = None ):
13+ self ._test_name = result ['name' ]
14+ self .status = result ['status' ]
15+ self .message = result ['message' ]
3216 super ().__init__ (tester , feedback_open )
3317
3418 @property
35- def test_name (self ) -> str :
36- """ The name of this test """
37- name = f"{ self .class_name } .{ self .method_name } "
38- if self .description :
39- name += f" ({ self .description } )"
40- return name
19+ def test_name (self ):
20+ return self ._test_name
4121
4222 @Test .run_decorator
43- def run (self ) -> str :
44- """
45- Return a json string containing all test result information.
46- """
47- if self .status == JavaTest .JUnitStatus .SUCCESSFUL :
48- return self .passed ()
49- elif self .status == JavaTest .JUnitStatus .FAILED :
23+ def run (self ):
24+ if self .status == "success" :
25+ return self .passed (message = self .message )
26+ elif self .status == "failure" :
5027 return self .failed (message = self .message )
5128 else :
5229 return self .error (message = self .message )
5330
5431
5532class JavaTester (Tester ):
5633
57- JAVA_TESTER_CLASS = "edu.toronto.cs.teach.JavaTester"
34+ JUNIT_TESTER_JAR = os .path .join (os .path .dirname (__file__ ), "lib" , "junit-platform-console-standalone.jar" )
35+ JUNIT_JUPITER_RESULT = "TEST-junit-jupiter.xml"
36+ JUNIT_VINTAGE_RESULT = "TEST-junit-vintage.xml"
5837
5938 def __init__ (self , specs : TestSpecs , test_class : Type [JavaTest ] = JavaTest ) -> None :
6039 """
@@ -63,17 +42,62 @@ def __init__(self, specs: TestSpecs, test_class: Type[JavaTest] = JavaTest) -> N
6342 This tester will create tests of type test_class.
6443 """
6544 super ().__init__ (specs , test_class )
66- self .java_classpath = f'.:{ self .specs ["install_data" , "path_to_tester_jars" ]} /*'
45+ classpath = self .specs .get ("test_data" , "classpath" , default = '.' ) or '.'
46+ self .java_classpath = ":" .join (self ._parse_file_paths (classpath ))
47+ self .out_dir = tempfile .TemporaryDirectory (dir = os .getcwd ())
48+ self .reports_dir = tempfile .TemporaryDirectory (dir = os .getcwd ())
6749
68- def compile (self ) -> None :
50+ @staticmethod
51+ def _parse_file_paths (glob_string : str ) -> List [str ]:
52+ """
53+ Return the real (absolute) paths of all files described by the glob <glob_string>.
54+ Only files that exist in the current directory (or its subdirectories) are returned.
55+ """
56+ curr_path = os .path .realpath ('.' )
57+ return [x for p in glob_string .split (':' ) for x in glob (os .path .realpath (p )) if curr_path in x ]
58+
59+ def _get_sources (self ) -> Set :
60+ """
61+ Return all java source files for this test.
62+ """
63+ sources = self .specs .get ("test_data" , "sources_path" , default = '' )
64+ scripts = ':' .join (self .specs ["test_data" , "script_files" ] + [sources ])
65+ return {path for path in self ._parse_file_paths (scripts ) if os .path .splitext (path )[1 ] == '.java' }
66+
67+ def _parse_junitxml (self ):
68+ """
69+ Parse junit results and yield a hash containing result data for each testcase.
70+ """
71+ for xml_filename in [self .JUNIT_JUPITER_RESULT , self .JUNIT_VINTAGE_RESULT ]:
72+ tree = eTree .parse (os .path .join (self .reports_dir .name , xml_filename ))
73+ root = tree .getroot ()
74+ for testcase in root .iterfind ('testcase' ):
75+ result = {}
76+ classname = testcase .attrib ['classname' ]
77+ testname = testcase .attrib ['name' ]
78+ result ['name' ] = '{}.{}' .format (classname , testname )
79+ result ['time' ] = float (testcase .attrib .get ('time' , 0 ))
80+ failure = testcase .find ('failure' )
81+ if failure is not None :
82+ result ['status' ] = 'failure'
83+ failure_type = failure .attrib .get ('type' , '' )
84+ failure_message = failure .attrib .get ('message' , '' )
85+ result ['message' ] = f'{ failure_type } : { failure_message } '
86+ else :
87+ result ['status' ] = 'success'
88+ result ['message' ] = ''
89+ yield result
90+
91+ def compile (self ) -> subprocess .CompletedProcess :
6992 """
7093 Compile the junit tests specified in the self.specs specifications.
7194 """
72- javac_command = ["javac" , "-cp" , self .java_classpath ]
73- javac_command .extend (self .specs ["test_data" , "script_files" ])
95+ classpath = f"{ self .java_classpath } :{ self .JUNIT_TESTER_JAR } "
96+ javac_command = ["javac" , "-cp" , classpath , "-d" , self .out_dir .name ]
97+ javac_command .extend (self ._get_sources ())
7498 # student files imported by tests will be compiled on cascade
75- subprocess .run (
76- javac_command , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , universal_newlines = True , check = True ,
99+ return subprocess .run (
100+ javac_command , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , universal_newlines = True , check = False ,
77101 )
78102
79103 def run_junit (self ) -> subprocess .CompletedProcess :
@@ -82,13 +106,19 @@ def run_junit(self) -> subprocess.CompletedProcess:
82106 """
83107 java_command = [
84108 "java" ,
85- "-cp" ,
86- self .java_classpath ,
87- JavaTester .JAVA_TESTER_CLASS ,
109+ "-jar" ,
110+ self .JUNIT_TESTER_JAR ,
111+ f"-cp={ self .java_classpath } :{ self .out_dir .name } " ,
112+ f"--reports-dir={ self .reports_dir .name } "
88113 ]
89- java_command .extend (self .specs ["test_data" , "script_files" ])
114+ classes = [f"-c={ os .path .splitext (os .path .basename (f ))[0 ]} " for f in self .specs ["test_data" , "script_files" ]]
115+ java_command .extend (classes )
90116 java = subprocess .run (
91- java_command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , universal_newlines = True , check = True ,
117+ java_command ,
118+ stdout = subprocess .PIPE ,
119+ stderr = subprocess .PIPE ,
120+ universal_newlines = True ,
121+ check = False
92122 )
93123 return java
94124
@@ -99,20 +129,20 @@ def run(self) -> None:
99129 """
100130 # check that the submission compiles against the tests
101131 try :
102- self .compile ()
132+ compile_result = self .compile ()
133+ if compile_result .stderr :
134+ raise TestError (compile_result .stderr )
103135 except subprocess .CalledProcessError as e :
104- msg = JavaTest .ERRORS ["bad_javac" ].format (e .stdout )
105- raise TestError (msg ) from e
136+ raise TestError (e )
106137 # run the tests with junit
107138 try :
108139 results = self .run_junit ()
109140 if results .stderr :
110141 raise TestError (results .stderr )
111142 except subprocess .CalledProcessError as e :
112- msg = JavaTest .ERRORS ["bad_java" ].format (e .stdout + e .stderr )
113- raise TestError (msg ) from e
143+ raise TestError (e )
114144 with self .open_feedback () as feedback_open :
115- for result in json . loads ( results . stdout ):
145+ for result in self . _parse_junitxml ( ):
116146 test = self .test_class (self , result , feedback_open )
117147 result_json = test .run ()
118148 print (result_json , flush = True )
0 commit comments