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