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