1#!/usr/bin/env python3 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 collections 20import copy 21import io 22import json 23import logging 24 25from acts import logger 26from acts.libs import yaml_writer 27 28from mobly.records import ExceptionRecord 29from mobly.records import OUTPUT_FILE_SUMMARY 30from mobly.records import TestResultEnums as MoblyTestResultEnums 31from mobly.records import TestResultRecord as MoblyTestResultRecord 32from mobly.records import TestResult as MoblyTestResult 33from mobly.records import TestSummaryEntryType 34from mobly.records import TestSummaryWriter as MoblyTestSummaryWriter 35 36 37class TestSummaryWriter(MoblyTestSummaryWriter): 38 """Writes test results to a summary file in real time. Inherits from Mobly's 39 TestSummaryWriter. 40 """ 41 42 def dump(self, content, entry_type): 43 """Update Mobly's implementation of dump to work on OrderedDict. 44 45 See MoblyTestSummaryWriter.dump for documentation. 46 """ 47 new_content = collections.OrderedDict(copy.deepcopy(content)) 48 new_content['Type'] = entry_type.value 49 new_content.move_to_end('Type', last=False) 50 # Both user code and Mobly code can trigger this dump, hence the lock. 51 with self._lock: 52 # For Python3, setting the encoding on yaml.safe_dump does not work 53 # because Python3 file descriptors set an encoding by default, which 54 # PyYAML uses instead of the encoding on yaml.safe_dump. So, the 55 # encoding has to be set on the open call instead. 56 with io.open(self._path, 'a', encoding='utf-8') as f: 57 # Use safe_dump here to avoid language-specific tags in final 58 # output. 59 yaml_writer.safe_dump(new_content, f) 60 61 62class TestResultEnums(MoblyTestResultEnums): 63 """Enums used for TestResultRecord class. Inherits from Mobly's 64 TestResultEnums. 65 66 Includes the tokens to mark test result with, and the string names for each 67 field in TestResultRecord. 68 """ 69 70 RECORD_LOG_BEGIN_TIME = "Log Begin Time" 71 RECORD_LOG_END_TIME = "Log End Time" 72 73 74class TestResultRecord(MoblyTestResultRecord): 75 """A record that holds the information of a test case execution. This class 76 inherits from Mobly's TestResultRecord class. 77 78 Attributes: 79 test_name: A string representing the name of the test case. 80 begin_time: Epoch timestamp of when the test case started. 81 end_time: Epoch timestamp of when the test case ended. 82 self.uid: Unique identifier of a test case. 83 self.result: Test result, PASS/FAIL/SKIP. 84 self.extras: User defined extra information of the test result. 85 self.details: A string explaining the details of the test case. 86 """ 87 88 def __init__(self, t_name, t_class=None): 89 super().__init__(t_name, t_class) 90 self.log_begin_time = None 91 self.log_end_time = None 92 93 def test_begin(self): 94 """Call this when the test case it records begins execution. 95 96 Sets the begin_time of this record. 97 """ 98 super().test_begin() 99 self.log_begin_time = logger.epoch_to_log_line_timestamp( 100 self.begin_time) 101 102 def _test_end(self, result, e): 103 """Class internal function to signal the end of a test case execution. 104 105 Args: 106 result: One of the TEST_RESULT enums in TestResultEnums. 107 e: A test termination signal (usually an exception object). It can 108 be any exception instance or of any subclass of 109 acts.signals.TestSignal. 110 """ 111 super()._test_end(result, e) 112 if self.end_time: 113 self.log_end_time = logger.epoch_to_log_line_timestamp( 114 self.end_time) 115 116 def to_dict(self): 117 """Gets a dictionary representing the content of this class. 118 119 Returns: 120 A dictionary representing the content of this class. 121 """ 122 d = collections.OrderedDict() 123 d[TestResultEnums.RECORD_NAME] = self.test_name 124 d[TestResultEnums.RECORD_CLASS] = self.test_class 125 d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time 126 d[TestResultEnums.RECORD_END_TIME] = self.end_time 127 d[TestResultEnums.RECORD_LOG_BEGIN_TIME] = self.log_begin_time 128 d[TestResultEnums.RECORD_LOG_END_TIME] = self.log_end_time 129 d[TestResultEnums.RECORD_RESULT] = self.result 130 d[TestResultEnums.RECORD_UID] = self.uid 131 d[TestResultEnums.RECORD_EXTRAS] = self.extras 132 d[TestResultEnums.RECORD_DETAILS] = self.details 133 d[TestResultEnums.RECORD_EXTRA_ERRORS] = { 134 key: value.to_dict() 135 for (key, value) in self.extra_errors.items() 136 } 137 d[TestResultEnums.RECORD_STACKTRACE] = self.stacktrace 138 return d 139 140 def json_str(self): 141 """Converts this test record to a string in json format. 142 143 Format of the json string is: 144 { 145 'Test Name': <test name>, 146 'Begin Time': <epoch timestamp>, 147 'Details': <details>, 148 ... 149 } 150 151 Returns: 152 A json-format string representing the test record. 153 """ 154 return json.dumps(self.to_dict()) 155 156 157class TestResult(MoblyTestResult): 158 """A class that contains metrics of a test run. This class inherits from 159 Mobly's TestResult class. 160 161 This class is essentially a container of TestResultRecord objects. 162 163 Attributes: 164 self.requested: A list of strings, each is the name of a test requested 165 by user. 166 self.failed: A list of records for tests failed. 167 self.executed: A list of records for tests that were actually executed. 168 self.passed: A list of records for tests passed. 169 self.skipped: A list of records for tests skipped. 170 """ 171 172 def __init__(self): 173 super().__init__() 174 self.controller_info = {} 175 self.extras = {} 176 177 def __add__(self, r): 178 """Overrides '+' operator for TestResult class. 179 180 The add operator merges two TestResult objects by concatenating all of 181 their lists together. 182 183 Args: 184 r: another instance of TestResult to be added 185 186 Returns: 187 A TestResult instance that's the sum of two TestResult instances. 188 """ 189 if not isinstance(r, TestResult): 190 raise TypeError("Operand %s of type %s is not a TestResult." % 191 (r, type(r))) 192 sum_result = TestResult() 193 for name in sum_result.__dict__: 194 r_value = getattr(r, name) 195 l_value = getattr(self, name) 196 if isinstance(r_value, list): 197 setattr(sum_result, name, l_value + r_value) 198 elif isinstance(r_value, dict): 199 sum_dict = copy.deepcopy(l_value) 200 sum_dict.update(copy.deepcopy(r_value)) 201 setattr(sum_result, name, sum_dict) 202 return sum_result 203 204 def add_controller_info(self, name, info): 205 try: 206 json.dumps(info) 207 except TypeError: 208 logging.warning(("Controller info for %s is not JSON serializable!" 209 " Coercing it to string.") % name) 210 self.controller_info[name] = str(info) 211 return 212 self.controller_info[name] = info 213 214 def set_extra_data(self, name, info): 215 try: 216 json.dumps(info) 217 except TypeError: 218 logging.warning("Controller info for %s is not JSON serializable! " 219 "Coercing it to string." % name) 220 info = str(info) 221 self.extras[name] = info 222 223 def json_str(self): 224 """Converts this test result to a string in json format. 225 226 Format of the json string is: 227 { 228 "Results": [ 229 {<executed test record 1>}, 230 {<executed test record 2>}, 231 ... 232 ], 233 "Summary": <summary dict> 234 } 235 236 Returns: 237 A json-format string representing the test results. 238 """ 239 d = collections.OrderedDict() 240 d["ControllerInfo"] = self.controller_info 241 d["Results"] = [record.to_dict() for record in self.executed] 242 d["Summary"] = self.summary_dict() 243 d["Extras"] = self.extras 244 d["Error"] = self.errors_list() 245 json_str = json.dumps(d, indent=4) 246 return json_str 247 248 def summary_str(self): 249 """Gets a string that summarizes the stats of this test result. 250 251 The summary provides the counts of how many test cases fall into each 252 category, like "Passed", "Failed" etc. 253 254 Format of the string is: 255 Requested <int>, Executed <int>, ... 256 257 Returns: 258 A summary string of this test result. 259 """ 260 l = ["%s %s" % (k, v) for k, v in self.summary_dict().items()] 261 msg = ", ".join(l) 262 return msg 263 264 def errors_list(self): 265 l = list() 266 for record in self.error: 267 if isinstance(record, TestResultRecord): 268 keys = [TestResultEnums.RECORD_NAME, 269 TestResultEnums.RECORD_DETAILS, 270 TestResultEnums.RECORD_EXTRA_ERRORS] 271 elif isinstance(record, ExceptionRecord): 272 keys = [TestResultEnums.RECORD_DETAILS, 273 TestResultEnums.RECORD_POSITION] 274 else: 275 return [] 276 l.append({k: record.to_dict()[k] for k in keys}) 277 return l 278