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