• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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