1# 2# Copyright (C) 2016 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16"""This module is where all the record definitions and record containers live. 17""" 18 19import json 20import logging 21import pprint 22 23from vts.runners.host import signals 24from vts.runners.host import utils 25from vts.utils.python.common import list_utils 26 27 28class TestResultEnums(object): 29 """Enums used for TestResultRecord class. 30 31 Includes the tokens to mark test result with, and the string names for each 32 field in TestResultRecord. 33 """ 34 35 RECORD_NAME = "Test Name" 36 RECORD_CLASS = "Test Class" 37 RECORD_BEGIN_TIME = "Begin Time" 38 RECORD_END_TIME = "End Time" 39 RECORD_RESULT = "Result" 40 RECORD_UID = "UID" 41 RECORD_EXTRAS = "Extras" 42 RECORD_EXTRA_ERRORS = "Extra Errors" 43 RECORD_DETAILS = "Details" 44 RECORD_TABLES = "Tables" 45 TEST_RESULT_PASS = "PASS" 46 TEST_RESULT_FAIL = "FAIL" 47 TEST_RESULT_SKIP = "SKIP" 48 TEST_RESULT_ERROR = "ERROR" 49 50 51class TestResultRecord(object): 52 """A record that holds the information of a test case execution. 53 54 Attributes: 55 test_name: A string representing the name of the test case. 56 begin_time: Epoch timestamp of when the test case started. 57 end_time: Epoch timestamp of when the test case ended. 58 uid: Unique identifier of a test case. 59 result: Test result, PASS/FAIL/SKIP. 60 extras: User defined extra information of the test result. 61 details: A string explaining the details of the test case. 62 tables: A dict of 2-dimensional lists containing tabular results. 63 """ 64 65 def __init__(self, t_name, t_class=None): 66 self.test_name = t_name 67 self.test_class = t_class 68 self.begin_time = None 69 self.end_time = None 70 self.uid = None 71 self.result = None 72 self.extras = None 73 self.details = None 74 self.extra_errors = {} 75 self.tables = {} 76 77 def testBegin(self): 78 """Call this when the test case it records begins execution. 79 80 Sets the begin_time of this record. 81 """ 82 self.begin_time = utils.get_current_epoch_time() 83 84 def _testEnd(self, result, e): 85 """Class internal function to signal the end of a test case execution. 86 87 Args: 88 result: One of the TEST_RESULT enums in TestResultEnums. 89 e: A test termination signal (usually an exception object). It can 90 be any exception instance or of any subclass of 91 vts.runners.host.signals.TestSignal. 92 """ 93 self.end_time = utils.get_current_epoch_time() 94 self.result = result 95 if isinstance(e, signals.TestSignal): 96 self.details = e.details 97 self.extras = e.extras 98 elif e: 99 self.details = str(e) 100 101 def testPass(self, e=None): 102 """To mark the test as passed in this record. 103 104 Args: 105 e: An instance of vts.runners.host.signals.TestPass. 106 """ 107 self._testEnd(TestResultEnums.TEST_RESULT_PASS, e) 108 109 def testFail(self, e=None): 110 """To mark the test as failed in this record. 111 112 Only testFail does instance check because we want "assert xxx" to also 113 fail the test same way assert_true does. 114 115 Args: 116 e: An exception object. It can be an instance of AssertionError or 117 vts.runners.host.base_test.TestFailure. 118 """ 119 self._testEnd(TestResultEnums.TEST_RESULT_FAIL, e) 120 121 def testSkip(self, e=None): 122 """To mark the test as skipped in this record. 123 124 Args: 125 e: An instance of vts.runners.host.signals.TestSkip. 126 """ 127 self._testEnd(TestResultEnums.TEST_RESULT_SKIP, e) 128 129 def testError(self, e=None): 130 """To mark the test as error in this record. 131 132 Args: 133 e: An exception object. 134 """ 135 self._testEnd(TestResultEnums.TEST_RESULT_ERROR, e) 136 137 def addError(self, tag, e): 138 """Add extra error happened during a test mark the test result as 139 ERROR. 140 141 If an error is added the test record, the record's result is equivalent 142 to the case where an uncaught exception happened. 143 144 Args: 145 tag: A string describing where this error came from, e.g. 'on_pass'. 146 e: An exception object. 147 """ 148 self.result = TestResultEnums.TEST_RESULT_ERROR 149 self.extra_errors[tag] = str(e) 150 151 def addTable(self, name, rows): 152 """Add a table as part of the test result. 153 154 Args: 155 name: The table name. 156 rows: A 2-dimensional list which contains the data. 157 """ 158 if name in self.tables: 159 logging.warning("Overwrite table %s" % name) 160 self.tables[name] = rows 161 162 def __str__(self): 163 d = self.getDict() 164 l = ["%s = %s" % (k, v) for k, v in d.items()] 165 s = ', '.join(l) 166 return s 167 168 def __repr__(self): 169 """This returns a short string representation of the test record.""" 170 t = utils.epoch_to_human_time(self.begin_time) 171 return "%s %s %s" % (t, self.test_name, self.result) 172 173 def getDict(self): 174 """Gets a dictionary representating the content of this class. 175 176 Returns: 177 A dictionary representating the content of this class. 178 """ 179 d = {} 180 d[TestResultEnums.RECORD_NAME] = self.test_name 181 d[TestResultEnums.RECORD_CLASS] = self.test_class 182 d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time 183 d[TestResultEnums.RECORD_END_TIME] = self.end_time 184 d[TestResultEnums.RECORD_RESULT] = self.result 185 d[TestResultEnums.RECORD_UID] = self.uid 186 d[TestResultEnums.RECORD_EXTRAS] = self.extras 187 d[TestResultEnums.RECORD_DETAILS] = self.details 188 d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors 189 d[TestResultEnums.RECORD_TABLES] = self.tables 190 return d 191 192 def jsonString(self): 193 """Converts this test record to a string in json format. 194 195 Format of the json string is: 196 { 197 'Test Name': <test name>, 198 'Begin Time': <epoch timestamp>, 199 'Details': <details>, 200 ... 201 } 202 203 Returns: 204 A json-format string representing the test record. 205 """ 206 return json.dumps(self.getDict()) 207 208 209class TestResult(object): 210 """A class that contains metrics of a test run. 211 212 This class is essentially a container of TestResultRecord objects. 213 214 Attributes: 215 self.requested: A list of records for tests requested by user. 216 self.failed: A list of records for tests failed. 217 self.executed: A list of records for tests that were actually executed. 218 self.passed: A list of records for tests passed. 219 self.skipped: A list of records for tests skipped. 220 self.error: A list of records for tests with error result token. 221 self._test_module_name: A string, test module's name. 222 self._test_module_timestamp: An integer, test module's execution start 223 timestamp. 224 """ 225 226 def __init__(self): 227 self.requested = [] 228 self.failed = [] 229 self.executed = [] 230 self.passed = [] 231 self.skipped = [] 232 self.error = [] 233 self._test_module_name = None 234 self._test_module_timestamp = None 235 236 def __add__(self, r): 237 """Overrides '+' operator for TestResult class. 238 239 The add operator merges two TestResult objects by concatenating all of 240 their lists together. 241 242 Args: 243 r: another instance of TestResult to be added 244 245 Returns: 246 A TestResult instance that's the sum of two TestResult instances. 247 """ 248 if not isinstance(r, TestResult): 249 raise TypeError("Operand %s of type %s is not a TestResult." % 250 (r, type(r))) 251 r.reportNonExecutedRecord() 252 sum_result = TestResult() 253 for name in sum_result.__dict__: 254 if name.startswith("_test_module"): 255 l_value = getattr(self, name) 256 r_value = getattr(r, name) 257 if l_value is None and r_value is None: 258 continue 259 elif l_value is None and r_value is not None: 260 value = r_value 261 elif l_value is not None and r_value is None: 262 value = l_value 263 else: 264 if name == "_test_module_name": 265 if l_value != r_value: 266 raise TypeError("_test_module_name is different.") 267 value = l_value 268 elif name == "_test_module_timestamp": 269 if int(l_value) < int(r_value): 270 value = l_value 271 else: 272 value = r_value 273 else: 274 raise TypeError("unknown _test_module* attribute.") 275 setattr(sum_result, name, value) 276 else: 277 l_value = list(getattr(self, name)) 278 r_value = list(getattr(r, name)) 279 setattr(sum_result, name, l_value + r_value) 280 return sum_result 281 282 def reportNonExecutedRecord(self): 283 """Check and report any requested tests that did not finish. 284 285 Adds a test record to self.error list iff it is in requested list but not 286 self.executed result list. 287 """ 288 for requested in self.requested: 289 found = False 290 291 for executed in self.executed: 292 if (requested.test_name == executed.test_name and 293 requested.test_class == executed.test_class): 294 found = True 295 break 296 297 if not found: 298 requested.testBegin() 299 requested.testError() 300 self.error.append(requested) 301 302 def removeRecord(self, record): 303 """Remove a test record from test results. 304 305 Records will be ed using test_name and test_class attribute. 306 All entries that match the provided record in all result lists will 307 be removed after calling this method. 308 309 Args: 310 record: A test record object to add. 311 """ 312 lists = [ 313 self.requested, self.failed, self.executed, self.passed, 314 self.skipped, self.error 315 ] 316 317 for l in lists: 318 indexToRemove = [] 319 for idx in range(len(l)): 320 if (l[idx].test_name == record.test_name and 321 l[idx].test_class == record.test_class): 322 indexToRemove.append(idx) 323 324 for idx in reversed(indexToRemove): 325 del l[idx] 326 327 def addRecord(self, record): 328 """Adds a test record to test results. 329 330 A record is considered executed once it's added to the test result. 331 332 Args: 333 record: A test record object to add. 334 """ 335 self.executed.append(record) 336 if record.result == TestResultEnums.TEST_RESULT_FAIL: 337 self.failed.append(record) 338 elif record.result == TestResultEnums.TEST_RESULT_SKIP: 339 self.skipped.append(record) 340 elif record.result == TestResultEnums.TEST_RESULT_PASS: 341 self.passed.append(record) 342 else: 343 self.error.append(record) 344 345 def setTestModuleKeys(self, name, start_timestamp): 346 """Sets the test module's name and start_timestamp.""" 347 self._test_module_name = name 348 self._test_module_timestamp = start_timestamp 349 350 def failClass(self, class_name, e): 351 """Add a record to indicate a test class setup has failed and no test 352 in the class was executed. 353 354 Args: 355 class_name: A string that is the name of the failed test class. 356 e: An exception object. 357 """ 358 record = TestResultRecord("setup_class", class_name) 359 record.testBegin() 360 record.testFail(e) 361 self.executed.append(record) 362 self.failed.append(record) 363 364 def skipClass(self, class_name, reason): 365 """Add a record to indicate all test cases in the class are skipped. 366 367 Args: 368 class_name: A string that is the name of the skipped test class. 369 reason: A string that is the reason for skipping. 370 """ 371 record = TestResultRecord("unknown", class_name) 372 record.testBegin() 373 record.testSkip(signals.TestSkip(reason)) 374 self.executed.append(record) 375 self.skipped.append(record) 376 377 def jsonString(self): 378 """Converts this test result to a string in json format. 379 380 Format of the json string is: 381 { 382 "Results": [ 383 {<executed test record 1>}, 384 {<executed test record 2>}, 385 ... 386 ], 387 "Summary": <summary dict> 388 } 389 390 Returns: 391 A json-format string representing the test results. 392 """ 393 records = list_utils.MergeUniqueKeepOrder( 394 self.executed, self.failed, self.passed, self.skipped, self.error) 395 executed = [record.getDict() for record in records] 396 397 d = {} 398 d["Results"] = executed 399 d["Summary"] = self.summaryDict() 400 d["TestModule"] = self.testModuleDict() 401 jsonString = json.dumps(d, indent=4, sort_keys=True) 402 return jsonString 403 404 def summary(self): 405 """Gets a string that summarizes the stats of this test result. 406 407 The summary rovides the counts of how many test cases fall into each 408 category, like "Passed", "Failed" etc. 409 410 Format of the string is: 411 Requested <int>, Executed <int>, ... 412 413 Returns: 414 A summary string of this test result. 415 """ 416 l = ["%s %d" % (k, v) for k, v in self.summaryDict().items()] 417 # Sort the list so the order is the same every time. 418 msg = ", ".join(sorted(l)) 419 return msg 420 421 def summaryDict(self): 422 """Gets a dictionary that summarizes the stats of this test result. 423 424 The summary rovides the counts of how many test cases fall into each 425 category, like "Passed", "Failed" etc. 426 427 Returns: 428 A dictionary with the stats of this test result. 429 """ 430 d = {} 431 d["Requested"] = len(self.requested) 432 d["Executed"] = len(self.executed) 433 d["Passed"] = len(self.passed) 434 d["Failed"] = len(self.failed) 435 d["Skipped"] = len(self.skipped) 436 d["Error"] = len(self.error) 437 return d 438 439 def testModuleDict(self): 440 """Returns a dict that summarizes the test module DB indexing keys.""" 441 d = {} 442 d["Name"] = self._test_module_name 443 d["Timestamp"] = self._test_module_timestamp 444 return d 445