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