1#!/usr/bin/env python3.4 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""This module is where all the record definitions and record containers live. 17""" 18 19import json 20import logging 21import pprint 22 23from acts import signals 24from acts import utils 25 26 27class TestResultEnums(object): 28 """Enums used for TestResultRecord class. 29 30 Includes the tokens to mark test result with, and the string names for each 31 field in TestResultRecord. 32 """ 33 34 RECORD_NAME = "Test Name" 35 RECORD_CLASS = "Test Class" 36 RECORD_BEGIN_TIME = "Begin Time" 37 RECORD_END_TIME = "End Time" 38 RECORD_RESULT = "Result" 39 RECORD_UID = "UID" 40 RECORD_EXTRAS = "Extras" 41 RECORD_EXTRA_ERRORS = "Extra Errors" 42 RECORD_DETAILS = "Details" 43 TEST_RESULT_PASS = "PASS" 44 TEST_RESULT_FAIL = "FAIL" 45 TEST_RESULT_SKIP = "SKIP" 46 TEST_RESULT_BLOCKED = "BLOCKED" 47 TEST_RESULT_UNKNOWN = "UNKNOWN" 48 49 50class TestResultRecord(object): 51 """A record that holds the information of a test case execution. 52 53 Attributes: 54 test_name: A string representing the name of the test case. 55 begin_time: Epoch timestamp of when the test case started. 56 end_time: Epoch timestamp of when the test case ended. 57 self.uid: Unique identifier of a test case. 58 self.result: Test result, PASS/FAIL/SKIP. 59 self.extras: User defined extra information of the test result. 60 self.details: A string explaining the details of the test case. 61 """ 62 63 def __init__(self, t_name, t_class=None): 64 self.test_name = t_name 65 self.test_class = t_class 66 self.begin_time = None 67 self.end_time = None 68 self.uid = None 69 self.result = None 70 self.extras = None 71 self.details = None 72 self.extra_errors = {} 73 74 def test_begin(self): 75 """Call this when the test case it records begins execution. 76 77 Sets the begin_time of this record. 78 """ 79 self.begin_time = utils.get_current_epoch_time() 80 81 def _test_end(self, result, e): 82 """Class internal function to signal the end of a test case execution. 83 84 Args: 85 result: One of the TEST_RESULT enums in TestResultEnums. 86 e: A test termination signal (usually an exception object). It can 87 be any exception instance or of any subclass of 88 acts.signals.TestSignal. 89 """ 90 self.end_time = utils.get_current_epoch_time() 91 self.result = result 92 if self.extra_errors: 93 self.result = TestResultEnums.TEST_RESULT_UNKNOWN 94 if isinstance(e, signals.TestSignal): 95 self.details = e.details 96 self.extras = e.extras 97 elif e: 98 self.details = str(e) 99 100 def test_pass(self, e=None): 101 """To mark the test as passed in this record. 102 103 Args: 104 e: An instance of acts.signals.TestPass. 105 """ 106 self._test_end(TestResultEnums.TEST_RESULT_PASS, e) 107 108 def test_fail(self, e=None): 109 """To mark the test as failed in this record. 110 111 Only test_fail does instance check because we want "assert xxx" to also 112 fail the test same way assert_true does. 113 114 Args: 115 e: An exception object. It can be an instance of AssertionError or 116 acts.base_test.TestFailure. 117 """ 118 self._test_end(TestResultEnums.TEST_RESULT_FAIL, e) 119 120 def test_skip(self, e=None): 121 """To mark the test as skipped in this record. 122 123 Args: 124 e: An instance of acts.signals.TestSkip. 125 """ 126 self._test_end(TestResultEnums.TEST_RESULT_SKIP, e) 127 128 def test_blocked(self, e=None): 129 """To mark the test as blocked in this record. 130 131 Args: 132 e: An instance of acts.signals.Test 133 """ 134 self._test_end(TestResultEnums.TEST_RESULT_BLOCKED, e) 135 136 def test_unknown(self, e=None): 137 """To mark the test as unknown in this record. 138 139 Args: 140 e: An exception object. 141 """ 142 self._test_end(TestResultEnums.TEST_RESULT_UNKNOWN, e) 143 144 def add_error(self, tag, e): 145 """Add extra error happened during a test mark the test result as 146 UNKNOWN. 147 148 If an error is added the test record, the record's result is equivalent 149 to the case where an uncaught exception happened. 150 151 Args: 152 tag: A string describing where this error came from, e.g. 'on_pass'. 153 e: An exception object. 154 """ 155 self.result = TestResultEnums.TEST_RESULT_UNKNOWN 156 self.extra_errors[tag] = str(e) 157 158 def __str__(self): 159 d = self.to_dict() 160 l = ["%s = %s" % (k, v) for k, v in d.items()] 161 s = ', '.join(l) 162 return s 163 164 def __repr__(self): 165 """This returns a short string representation of the test record.""" 166 t = utils.epoch_to_human_time(self.begin_time) 167 return "%s %s %s" % (t, self.test_name, self.result) 168 169 def to_dict(self): 170 """Gets a dictionary representating the content of this class. 171 172 Returns: 173 A dictionary representating the content of this class. 174 """ 175 d = {} 176 d[TestResultEnums.RECORD_NAME] = self.test_name 177 d[TestResultEnums.RECORD_CLASS] = self.test_class 178 d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time 179 d[TestResultEnums.RECORD_END_TIME] = self.end_time 180 d[TestResultEnums.RECORD_RESULT] = self.result 181 d[TestResultEnums.RECORD_UID] = self.uid 182 d[TestResultEnums.RECORD_EXTRAS] = self.extras 183 d[TestResultEnums.RECORD_DETAILS] = self.details 184 d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors 185 return d 186 187 def json_str(self): 188 """Converts this test record to a string in json format. 189 190 Format of the json string is: 191 { 192 'Test Name': <test name>, 193 'Begin Time': <epoch timestamp>, 194 'Details': <details>, 195 ... 196 } 197 198 Returns: 199 A json-format string representing the test record. 200 """ 201 return json.dumps(self.to_dict()) 202 203 204class TestResult(object): 205 """A class that contains metrics of a test run. 206 207 This class is essentially a container of TestResultRecord objects. 208 209 Attributes: 210 self.requested: A list of strings, each is the name of a test requested 211 by user. 212 self.failed: A list of records for tests failed. 213 self.executed: A list of records for tests that were actually executed. 214 self.passed: A list of records for tests passed. 215 self.skipped: A list of records for tests skipped. 216 self.unknown: A list of records for tests with unknown result token. 217 """ 218 219 def __init__(self): 220 self.requested = [] 221 self.failed = [] 222 self.executed = [] 223 self.passed = [] 224 self.skipped = [] 225 self.blocked = [] 226 self.unknown = [] 227 self.controller_info = {} 228 229 def __add__(self, r): 230 """Overrides '+' operator for TestResult class. 231 232 The add operator merges two TestResult objects by concatenating all of 233 their lists together. 234 235 Args: 236 r: another instance of TestResult to be added 237 238 Returns: 239 A TestResult instance that's the sum of two TestResult instances. 240 """ 241 if not isinstance(r, TestResult): 242 raise TypeError("Operand %s of type %s is not a TestResult." % 243 (r, type(r))) 244 sum_result = TestResult() 245 for name in sum_result.__dict__: 246 r_value = getattr(r, name) 247 l_value = getattr(self, name) 248 if isinstance(r_value, list): 249 setattr(sum_result, name, l_value + r_value) 250 elif isinstance(r_value, dict): 251 # '+' operator for TestResult is only valid when multiple 252 # TestResult objs were created in the same test run, which means 253 # the controller info would be the same across all of them. 254 # TODO(angli): have a better way to validate this situation. 255 setattr(sum_result, name, l_value) 256 return sum_result 257 258 def add_controller_info(self, name, info): 259 try: 260 json.dumps(info) 261 except TypeError: 262 logging.warning(("Controller info for %s is not JSON serializable!" 263 " Coercing it to string.") % name) 264 self.controller_info[name] = str(info) 265 return 266 self.controller_info[name] = info 267 268 def add_record(self, record): 269 """Adds a test record to test result. 270 271 A record is considered executed once it's added to the test result. 272 273 Args: 274 record: A test record object to add. 275 """ 276 if record.result == TestResultEnums.TEST_RESULT_FAIL: 277 self.executed.append(record) 278 self.failed.append(record) 279 elif record.result == TestResultEnums.TEST_RESULT_SKIP: 280 self.skipped.append(record) 281 elif record.result == TestResultEnums.TEST_RESULT_PASS: 282 self.executed.append(record) 283 self.passed.append(record) 284 elif record.result == TestResultEnums.TEST_RESULT_BLOCKED: 285 self.blocked.append(record) 286 else: 287 self.executed.append(record) 288 self.unknown.append(record) 289 290 def add_controller_info(self, name, info): 291 try: 292 json.dumps(info) 293 except TypeError: 294 logging.warning(("Controller info for %s is not JSON serializable!" 295 " Coercing it to string.") % name) 296 self.controller_info[name] = str(info) 297 return 298 self.controller_info[name] = info 299 300 @property 301 def is_all_pass(self): 302 """True if no tests failed or threw errors, False otherwise.""" 303 num_of_failures = ( 304 len(self.failed) + len(self.unknown) + len(self.blocked)) 305 if num_of_failures == 0: 306 return True 307 return False 308 309 def json_str(self): 310 """Converts this test result to a string in json format. 311 312 Format of the json string is: 313 { 314 "Results": [ 315 {<executed test record 1>}, 316 {<executed test record 2>}, 317 ... 318 ], 319 "Summary": <summary dict> 320 } 321 322 Returns: 323 A json-format string representing the test results. 324 """ 325 d = {} 326 d["ControllerInfo"] = self.controller_info 327 d["Results"] = [record.to_dict() for record in self.executed] 328 d["Summary"] = self.summary_dict() 329 json_str = json.dumps(d, indent=4, sort_keys=True) 330 return json_str 331 332 def summary_str(self): 333 """Gets a string that summarizes the stats of this test result. 334 335 The summary rovides the counts of how many test cases fall into each 336 category, like "Passed", "Failed" etc. 337 338 Format of the string is: 339 Requested <int>, Executed <int>, ... 340 341 Returns: 342 A summary string of this test result. 343 """ 344 l = ["%s %s" % (k, v) for k, v in self.summary_dict().items()] 345 # Sort the list so the order is the same every time. 346 msg = ", ".join(sorted(l)) 347 return msg 348 349 def summary_dict(self): 350 """Gets a dictionary that summarizes the stats of this test result. 351 352 The summary rovides the counts of how many test cases fall into each 353 category, like "Passed", "Failed" etc. 354 355 Returns: 356 A dictionary with the stats of this test result. 357 """ 358 d = {} 359 d["ControllerInfo"] = self.controller_info 360 d["Requested"] = len(self.requested) 361 d["Executed"] = len(self.executed) 362 d["Passed"] = len(self.passed) 363 d["Failed"] = len(self.failed) 364 d["Skipped"] = len(self.skipped) 365 d["Blocked"] = len(self.blocked) 366 d["Unknown"] = len(self.unknown) 367 return d 368