1# Copyright (c) 2017 Mark D. Hill and David A. Wood 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer; 8# redistributions in binary form must reproduce the above copyright 9# notice, this list of conditions and the following disclaimer in the 10# documentation and/or other materials provided with the distribution; 11# neither the name of the copyright holders nor the names of its 12# contributors may be used to endorse or promote products derived from 13# this software without specific prior written permission. 14# 15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26# 27# Authors: Sean Wilson 28 29import os 30import pickle 31import xml.sax.saxutils 32 33from config import config 34import helper 35import state 36import log 37 38def _create_uid_index(iterable): 39 index = {} 40 for item in iterable: 41 assert item.uid not in index 42 index[item.uid] = item 43 return index 44 45 46class _CommonMetadataMixin: 47 @property 48 def name(self): 49 return self._metadata.name 50 @property 51 def uid(self): 52 return self._metadata.uid 53 @property 54 def result(self): 55 return self._metadata.result 56 @result.setter 57 def result(self, result): 58 self._metadata.result = result 59 60 @property 61 def unsuccessful(self): 62 return self._metadata.result.value != state.Result.Passed 63 64 65class InternalTestResult(object, _CommonMetadataMixin): 66 def __init__(self, obj, suite, directory): 67 self._metadata = obj.metadata 68 self.suite = suite 69 70 self.stderr = os.path.join( 71 InternalSavedResults.output_path(self.uid, suite.uid), 72 'stderr' 73 ) 74 self.stdout = os.path.join( 75 InternalSavedResults.output_path(self.uid, suite.uid), 76 'stdout' 77 ) 78 79 80class InternalSuiteResult(object, _CommonMetadataMixin): 81 def __init__(self, obj, directory): 82 self._metadata = obj.metadata 83 self.directory = directory 84 self._wrap_tests(obj) 85 86 def _wrap_tests(self, obj): 87 self._tests = [InternalTestResult(test, self, self.directory) 88 for test in obj] 89 self._tests_index = _create_uid_index(self._tests) 90 91 def get_test(self, uid): 92 return self._tests_index[uid] 93 94 def __iter__(self): 95 return iter(self._tests) 96 97 def get_test_result(self, uid): 98 return self.get_test(uid) 99 100 def aggregate_test_results(self): 101 results = {} 102 for test in self: 103 helper.append_dictlist(results, test.result.value, test) 104 return results 105 106 107class InternalLibraryResults(object, _CommonMetadataMixin): 108 def __init__(self, obj, directory): 109 self.directory = directory 110 self._metadata = obj.metadata 111 self._wrap_suites(obj) 112 113 def __iter__(self): 114 return iter(self._suites) 115 116 def _wrap_suites(self, obj): 117 self._suites = [InternalSuiteResult(suite, self.directory) 118 for suite in obj] 119 self._suites_index = _create_uid_index(self._suites) 120 121 def add_suite(self, suite): 122 if suite.uid in self._suites: 123 raise ValueError('Cannot have duplicate suite UIDs.') 124 self._suites[suite.uid] = suite 125 126 def get_suite_result(self, suite_uid): 127 return self._suites_index[suite_uid] 128 129 def get_test_result(self, test_uid, suite_uid): 130 return self.get_suite_result(suite_uid).get_test_result(test_uid) 131 132 def aggregate_test_results(self): 133 results = {} 134 for suite in self._suites: 135 for test in suite: 136 helper.append_dictlist(results, test.result.value, test) 137 return results 138 139class InternalSavedResults: 140 @staticmethod 141 def output_path(test_uid, suite_uid, base=None): 142 ''' 143 Return the path which results for a specific test case should be 144 stored. 145 ''' 146 if base is None: 147 base = config.result_path 148 return os.path.join( 149 base, 150 str(suite_uid).replace(os.path.sep, '-'), 151 str(test_uid).replace(os.path.sep, '-')) 152 153 @staticmethod 154 def save(results, path, protocol=pickle.HIGHEST_PROTOCOL): 155 if not os.path.exists(os.path.dirname(path)): 156 try: 157 os.makedirs(os.path.dirname(path)) 158 except OSError as exc: # Guard against race condition 159 if exc.errno != errno.EEXIST: 160 raise 161 162 with open(path, 'w') as f: 163 pickle.dump(results, f, protocol) 164 165 @staticmethod 166 def load(path): 167 with open(path, 'r') as f: 168 return pickle.load(f) 169 170 171class XMLElement(object): 172 def write(self, file_): 173 self.begin(file_) 174 self.end(file_) 175 176 def begin(self, file_): 177 file_.write('<') 178 file_.write(self.name) 179 for attr in self.attributes: 180 file_.write(' ') 181 attr.write(file_) 182 file_.write('>') 183 184 self.body(file_) 185 186 def body(self, file_): 187 for elem in self.elements: 188 file_.write('\n') 189 elem.write(file_) 190 file_.write('\n') 191 192 def end(self, file_): 193 file_.write('</%s>' % self.name) 194 195class XMLAttribute(object): 196 def __init__(self, name, value): 197 self.name = name 198 self.value = value 199 200 def write(self, file_): 201 file_.write('%s=%s' % (self.name, 202 xml.sax.saxutils.quoteattr(self.value))) 203 204 205class JUnitTestSuites(XMLElement): 206 name = 'testsuites' 207 result_map = { 208 state.Result.Errored: 'errors', 209 state.Result.Failed: 'failures', 210 state.Result.Passed: 'tests' 211 } 212 213 def __init__(self, internal_results): 214 results = internal_results.aggregate_test_results() 215 216 self.attributes = [] 217 for result, tests in results.items(): 218 self.attributes.append(self.result_attribute(result, 219 str(len(tests)))) 220 221 self.elements = [] 222 for suite in internal_results: 223 self.elements.append(JUnitTestSuite(suite)) 224 225 def result_attribute(self, result, count): 226 return XMLAttribute(self.result_map[result], count) 227 228class JUnitTestSuite(JUnitTestSuites): 229 name = 'testsuite' 230 result_map = { 231 state.Result.Errored: 'errors', 232 state.Result.Failed: 'failures', 233 state.Result.Passed: 'tests', 234 state.Result.Skipped: 'skipped' 235 } 236 237 def __init__(self, suite_result): 238 results = suite_result.aggregate_test_results() 239 240 self.attributes = [ 241 XMLAttribute('name', suite_result.name) 242 ] 243 for result, tests in results.items(): 244 self.attributes.append(self.result_attribute(result, 245 str(len(tests)))) 246 247 self.elements = [] 248 for test in suite_result: 249 self.elements.append(JUnitTestCase(test)) 250 251 def result_attribute(self, result, count): 252 return XMLAttribute(self.result_map[result], count) 253 254class JUnitTestCase(XMLElement): 255 name = 'testcase' 256 def __init__(self, test_result): 257 self.attributes = [ 258 XMLAttribute('name', test_result.name), 259 # TODO JUnit expects class of test.. add as test metadata. 260 XMLAttribute('classname', str(test_result.uid)), 261 XMLAttribute('status', str(test_result.result)), 262 ] 263 264 # TODO JUnit expects a message for the reason a test was 265 # skipped or errored, save this with the test metadata. 266 # http://llg.cubic.org/docs/junit/ 267 self.elements = [ 268 LargeFileElement('system-err', test_result.stderr), 269 LargeFileElement('system-out', test_result.stdout), 270 ] 271 272class LargeFileElement(XMLElement): 273 def __init__(self, name, filename): 274 self.name = name 275 self.filename = filename 276 self.attributes = [] 277 278 def body(self, file_): 279 try: 280 with open(self.filename, 'r') as f: 281 for line in f: 282 file_.write(xml.sax.saxutils.escape(line)) 283 except IOError: 284 # TODO Better error logic, this is sometimes O.K. 285 # if there was no stdout/stderr captured for the test 286 # 287 # TODO If that was the case, the file should still be made and it 288 # should just be empty instead of not existing. 289 pass 290 291 292 293class JUnitSavedResults: 294 @staticmethod 295 def save(results, path): 296 ''' 297 Compile the internal results into JUnit format writting it to the 298 given file. 299 ''' 300 results = JUnitTestSuites(results) 301 with open(path, 'w') as f: 302 results.write(f) 303 304