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