1import os 2from xml.sax.saxutils import escape 3from json import JSONEncoder 4 5# Test result codes. 6 7class ResultCode(object): 8 """Test result codes.""" 9 10 # We override __new__ and __getnewargs__ to ensure that pickling still 11 # provides unique ResultCode objects in any particular instance. 12 _instances = {} 13 def __new__(cls, name, isFailure): 14 res = cls._instances.get(name) 15 if res is None: 16 cls._instances[name] = res = super(ResultCode, cls).__new__(cls) 17 return res 18 def __getnewargs__(self): 19 return (self.name, self.isFailure) 20 21 def __init__(self, name, isFailure): 22 self.name = name 23 self.isFailure = isFailure 24 25 def __repr__(self): 26 return '%s%r' % (self.__class__.__name__, 27 (self.name, self.isFailure)) 28 29PASS = ResultCode('PASS', False) 30XFAIL = ResultCode('XFAIL', False) 31FAIL = ResultCode('FAIL', True) 32XPASS = ResultCode('XPASS', True) 33UNRESOLVED = ResultCode('UNRESOLVED', True) 34UNSUPPORTED = ResultCode('UNSUPPORTED', False) 35 36# Test metric values. 37 38class MetricValue(object): 39 def format(self): 40 """ 41 format() -> str 42 43 Convert this metric to a string suitable for displaying as part of the 44 console output. 45 """ 46 raise RuntimeError("abstract method") 47 48 def todata(self): 49 """ 50 todata() -> json-serializable data 51 52 Convert this metric to content suitable for serializing in the JSON test 53 output. 54 """ 55 raise RuntimeError("abstract method") 56 57class IntMetricValue(MetricValue): 58 def __init__(self, value): 59 self.value = value 60 61 def format(self): 62 return str(self.value) 63 64 def todata(self): 65 return self.value 66 67class RealMetricValue(MetricValue): 68 def __init__(self, value): 69 self.value = value 70 71 def format(self): 72 return '%.4f' % self.value 73 74 def todata(self): 75 return self.value 76 77class JSONMetricValue(MetricValue): 78 """ 79 JSONMetricValue is used for types that are representable in the output 80 but that are otherwise uninterpreted. 81 """ 82 def __init__(self, value): 83 # Ensure the value is a serializable by trying to encode it. 84 # WARNING: The value may change before it is encoded again, and may 85 # not be encodable after the change. 86 try: 87 e = JSONEncoder() 88 e.encode(value) 89 except TypeError: 90 raise 91 self.value = value 92 93 def format(self): 94 e = JSONEncoder(indent=2, sort_keys=True) 95 return e.encode(self.value) 96 97 def todata(self): 98 return self.value 99 100def toMetricValue(value): 101 if isinstance(value, MetricValue): 102 return value 103 elif isinstance(value, int) or isinstance(value, long): 104 return IntMetricValue(value) 105 elif isinstance(value, float): 106 return RealMetricValue(value) 107 else: 108 # Try to create a JSONMetricValue and let the constructor throw 109 # if value is not a valid type. 110 return JSONMetricValue(value) 111 112 113# Test results. 114 115class Result(object): 116 """Wrapper for the results of executing an individual test.""" 117 118 def __init__(self, code, output='', elapsed=None): 119 # The result code. 120 self.code = code 121 # The test output. 122 self.output = output 123 # The wall timing to execute the test, if timing. 124 self.elapsed = elapsed 125 # The metrics reported by this test. 126 self.metrics = {} 127 128 def addMetric(self, name, value): 129 """ 130 addMetric(name, value) 131 132 Attach a test metric to the test result, with the given name and list of 133 values. It is an error to attempt to attach the metrics with the same 134 name multiple times. 135 136 Each value must be an instance of a MetricValue subclass. 137 """ 138 if name in self.metrics: 139 raise ValueError("result already includes metrics for %r" % ( 140 name,)) 141 if not isinstance(value, MetricValue): 142 raise TypeError("unexpected metric value: %r" % (value,)) 143 self.metrics[name] = value 144 145# Test classes. 146 147class TestSuite: 148 """TestSuite - Information on a group of tests. 149 150 A test suite groups together a set of logically related tests. 151 """ 152 153 def __init__(self, name, source_root, exec_root, config): 154 self.name = name 155 self.source_root = source_root 156 self.exec_root = exec_root 157 # The test suite configuration. 158 self.config = config 159 160 def getSourcePath(self, components): 161 return os.path.join(self.source_root, *components) 162 163 def getExecPath(self, components): 164 return os.path.join(self.exec_root, *components) 165 166class Test: 167 """Test - Information on a single test instance.""" 168 169 def __init__(self, suite, path_in_suite, config, file_path = None): 170 self.suite = suite 171 self.path_in_suite = path_in_suite 172 self.config = config 173 self.file_path = file_path 174 # A list of conditions under which this test is expected to fail. These 175 # can optionally be provided by test format handlers, and will be 176 # honored when the test result is supplied. 177 self.xfails = [] 178 # The test result, once complete. 179 self.result = None 180 181 def setResult(self, result): 182 if self.result is not None: 183 raise ArgumentError("test result already set") 184 if not isinstance(result, Result): 185 raise ArgumentError("unexpected result type") 186 187 self.result = result 188 189 # Apply the XFAIL handling to resolve the result exit code. 190 if self.isExpectedToFail(): 191 if self.result.code == PASS: 192 self.result.code = XPASS 193 elif self.result.code == FAIL: 194 self.result.code = XFAIL 195 196 def getFullName(self): 197 return self.suite.config.name + ' :: ' + '/'.join(self.path_in_suite) 198 199 def getFilePath(self): 200 if self.file_path: 201 return self.file_path 202 return self.getSourcePath() 203 204 def getSourcePath(self): 205 return self.suite.getSourcePath(self.path_in_suite) 206 207 def getExecPath(self): 208 return self.suite.getExecPath(self.path_in_suite) 209 210 def isExpectedToFail(self): 211 """ 212 isExpectedToFail() -> bool 213 214 Check whether this test is expected to fail in the current 215 configuration. This check relies on the test xfails property which by 216 some test formats may not be computed until the test has first been 217 executed. 218 """ 219 220 # Check if any of the xfails match an available feature or the target. 221 for item in self.xfails: 222 # If this is the wildcard, it always fails. 223 if item == '*': 224 return True 225 226 # If this is an exact match for one of the features, it fails. 227 if item in self.config.available_features: 228 return True 229 230 # If this is a part of the target triple, it fails. 231 if item in self.suite.config.target_triple: 232 return True 233 234 return False 235 236 237 def getJUnitXML(self): 238 test_name = self.path_in_suite[-1] 239 test_path = self.path_in_suite[:-1] 240 safe_test_path = [x.replace(".","_") for x in test_path] 241 safe_name = self.suite.name.replace(".","-") 242 243 if safe_test_path: 244 class_name = safe_name + "." + "/".join(safe_test_path) 245 else: 246 class_name = safe_name + "." + safe_name 247 248 xml = "<testcase classname='" + class_name + "' name='" + \ 249 test_name + "'" 250 xml += " time='%.2f'" % (self.result.elapsed,) 251 if self.result.code.isFailure: 252 xml += ">\n\t<failure >\n" + escape(self.result.output) 253 xml += "\n\t</failure>\n</testcase>" 254 else: 255 xml += "/>" 256 return xml 257