• 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"""This module is where all the record definitions and record containers live.
17"""
18
19import json
20import logging
21import pprint
22
23from acts import signals
24from acts import utils
25
26
27class TestResultEnums(object):
28    """Enums used for TestResultRecord class.
29
30    Includes the tokens to mark test result with, and the string names for each
31    field in TestResultRecord.
32    """
33
34    RECORD_NAME = "Test Name"
35    RECORD_CLASS = "Test Class"
36    RECORD_BEGIN_TIME = "Begin Time"
37    RECORD_END_TIME = "End Time"
38    RECORD_RESULT = "Result"
39    RECORD_UID = "UID"
40    RECORD_EXTRAS = "Extras"
41    RECORD_EXTRA_ERRORS = "Extra Errors"
42    RECORD_DETAILS = "Details"
43    TEST_RESULT_PASS = "PASS"
44    TEST_RESULT_FAIL = "FAIL"
45    TEST_RESULT_SKIP = "SKIP"
46    TEST_RESULT_BLOCKED = "BLOCKED"
47    TEST_RESULT_UNKNOWN = "UNKNOWN"
48
49
50class TestResultRecord(object):
51    """A record that holds the information of a test case execution.
52
53    Attributes:
54        test_name: A string representing the name of the test case.
55        begin_time: Epoch timestamp of when the test case started.
56        end_time: Epoch timestamp of when the test case ended.
57        self.uid: Unique identifier of a test case.
58        self.result: Test result, PASS/FAIL/SKIP.
59        self.extras: User defined extra information of the test result.
60        self.details: A string explaining the details of the test case.
61    """
62
63    def __init__(self, t_name, t_class=None):
64        self.test_name = t_name
65        self.test_class = t_class
66        self.begin_time = None
67        self.end_time = None
68        self.uid = None
69        self.result = None
70        self.extras = None
71        self.details = None
72        self.extra_errors = {}
73
74    def test_begin(self):
75        """Call this when the test case it records begins execution.
76
77        Sets the begin_time of this record.
78        """
79        self.begin_time = utils.get_current_epoch_time()
80
81    def _test_end(self, result, e):
82        """Class internal function to signal the end of a test case execution.
83
84        Args:
85            result: One of the TEST_RESULT enums in TestResultEnums.
86            e: A test termination signal (usually an exception object). It can
87                be any exception instance or of any subclass of
88                acts.signals.TestSignal.
89        """
90        self.end_time = utils.get_current_epoch_time()
91        self.result = result
92        if self.extra_errors:
93            self.result = TestResultEnums.TEST_RESULT_UNKNOWN
94        if isinstance(e, signals.TestSignal):
95            self.details = e.details
96            self.extras = e.extras
97        elif e:
98            self.details = str(e)
99
100    def test_pass(self, e=None):
101        """To mark the test as passed in this record.
102
103        Args:
104            e: An instance of acts.signals.TestPass.
105        """
106        self._test_end(TestResultEnums.TEST_RESULT_PASS, e)
107
108    def test_fail(self, e=None):
109        """To mark the test as failed in this record.
110
111        Only test_fail does instance check because we want "assert xxx" to also
112        fail the test same way assert_true does.
113
114        Args:
115            e: An exception object. It can be an instance of AssertionError or
116                acts.base_test.TestFailure.
117        """
118        self._test_end(TestResultEnums.TEST_RESULT_FAIL, e)
119
120    def test_skip(self, e=None):
121        """To mark the test as skipped in this record.
122
123        Args:
124            e: An instance of acts.signals.TestSkip.
125        """
126        self._test_end(TestResultEnums.TEST_RESULT_SKIP, e)
127
128    def test_blocked(self, e=None):
129        """To mark the test as blocked in this record.
130
131        Args:
132            e: An instance of acts.signals.Test
133        """
134        self._test_end(TestResultEnums.TEST_RESULT_BLOCKED, e)
135
136    def test_unknown(self, e=None):
137        """To mark the test as unknown in this record.
138
139        Args:
140            e: An exception object.
141        """
142        self._test_end(TestResultEnums.TEST_RESULT_UNKNOWN, e)
143
144    def add_error(self, tag, e):
145        """Add extra error happened during a test mark the test result as
146        UNKNOWN.
147
148        If an error is added the test record, the record's result is equivalent
149        to the case where an uncaught exception happened.
150
151        Args:
152            tag: A string describing where this error came from, e.g. 'on_pass'.
153            e: An exception object.
154        """
155        self.result = TestResultEnums.TEST_RESULT_UNKNOWN
156        self.extra_errors[tag] = str(e)
157
158    def __str__(self):
159        d = self.to_dict()
160        l = ["%s = %s" % (k, v) for k, v in d.items()]
161        s = ', '.join(l)
162        return s
163
164    def __repr__(self):
165        """This returns a short string representation of the test record."""
166        t = utils.epoch_to_human_time(self.begin_time)
167        return "%s %s %s" % (t, self.test_name, self.result)
168
169    def to_dict(self):
170        """Gets a dictionary representating the content of this class.
171
172        Returns:
173            A dictionary representating the content of this class.
174        """
175        d = {}
176        d[TestResultEnums.RECORD_NAME] = self.test_name
177        d[TestResultEnums.RECORD_CLASS] = self.test_class
178        d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
179        d[TestResultEnums.RECORD_END_TIME] = self.end_time
180        d[TestResultEnums.RECORD_RESULT] = self.result
181        d[TestResultEnums.RECORD_UID] = self.uid
182        d[TestResultEnums.RECORD_EXTRAS] = self.extras
183        d[TestResultEnums.RECORD_DETAILS] = self.details
184        d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
185        return d
186
187    def json_str(self):
188        """Converts this test record to a string in json format.
189
190        Format of the json string is:
191            {
192                'Test Name': <test name>,
193                'Begin Time': <epoch timestamp>,
194                'Details': <details>,
195                ...
196            }
197
198        Returns:
199            A json-format string representing the test record.
200        """
201        return json.dumps(self.to_dict())
202
203
204class TestResult(object):
205    """A class that contains metrics of a test run.
206
207    This class is essentially a container of TestResultRecord objects.
208
209    Attributes:
210        self.requested: A list of strings, each is the name of a test requested
211            by user.
212        self.failed: A list of records for tests failed.
213        self.executed: A list of records for tests that were actually executed.
214        self.passed: A list of records for tests passed.
215        self.skipped: A list of records for tests skipped.
216        self.unknown: A list of records for tests with unknown result token.
217    """
218
219    def __init__(self):
220        self.requested = []
221        self.failed = []
222        self.executed = []
223        self.passed = []
224        self.skipped = []
225        self.blocked = []
226        self.unknown = []
227        self.controller_info = {}
228
229    def __add__(self, r):
230        """Overrides '+' operator for TestResult class.
231
232        The add operator merges two TestResult objects by concatenating all of
233        their lists together.
234
235        Args:
236            r: another instance of TestResult to be added
237
238        Returns:
239            A TestResult instance that's the sum of two TestResult instances.
240        """
241        if not isinstance(r, TestResult):
242            raise TypeError("Operand %s of type %s is not a TestResult." %
243                            (r, type(r)))
244        sum_result = TestResult()
245        for name in sum_result.__dict__:
246            r_value = getattr(r, name)
247            l_value = getattr(self, name)
248            if isinstance(r_value, list):
249                setattr(sum_result, name, l_value + r_value)
250            elif isinstance(r_value, dict):
251                # '+' operator for TestResult is only valid when multiple
252                # TestResult objs were created in the same test run, which means
253                # the controller info would be the same across all of them.
254                # TODO(angli): have a better way to validate this situation.
255                setattr(sum_result, name, l_value)
256        return sum_result
257
258    def add_controller_info(self, name, info):
259        try:
260            json.dumps(info)
261        except TypeError:
262            logging.warning(("Controller info for %s is not JSON serializable!"
263                             " Coercing it to string.") % name)
264            self.controller_info[name] = str(info)
265            return
266        self.controller_info[name] = info
267
268    def add_record(self, record):
269        """Adds a test record to test result.
270
271        A record is considered executed once it's added to the test result.
272
273        Args:
274            record: A test record object to add.
275        """
276        if record.result == TestResultEnums.TEST_RESULT_FAIL:
277            self.executed.append(record)
278            self.failed.append(record)
279        elif record.result == TestResultEnums.TEST_RESULT_SKIP:
280            self.skipped.append(record)
281        elif record.result == TestResultEnums.TEST_RESULT_PASS:
282            self.executed.append(record)
283            self.passed.append(record)
284        elif record.result == TestResultEnums.TEST_RESULT_BLOCKED:
285            self.blocked.append(record)
286        else:
287            self.executed.append(record)
288            self.unknown.append(record)
289
290    def add_controller_info(self, name, info):
291        try:
292            json.dumps(info)
293        except TypeError:
294            logging.warning(("Controller info for %s is not JSON serializable!"
295                             " Coercing it to string.") % name)
296            self.controller_info[name] = str(info)
297            return
298        self.controller_info[name] = info
299
300    @property
301    def is_all_pass(self):
302        """True if no tests failed or threw errors, False otherwise."""
303        num_of_failures = (
304            len(self.failed) + len(self.unknown) + len(self.blocked))
305        if num_of_failures == 0:
306            return True
307        return False
308
309    def json_str(self):
310        """Converts this test result to a string in json format.
311
312        Format of the json string is:
313            {
314                "Results": [
315                    {<executed test record 1>},
316                    {<executed test record 2>},
317                    ...
318                ],
319                "Summary": <summary dict>
320            }
321
322        Returns:
323            A json-format string representing the test results.
324        """
325        d = {}
326        d["ControllerInfo"] = self.controller_info
327        d["Results"] = [record.to_dict() for record in self.executed]
328        d["Summary"] = self.summary_dict()
329        json_str = json.dumps(d, indent=4, sort_keys=True)
330        return json_str
331
332    def summary_str(self):
333        """Gets a string that summarizes the stats of this test result.
334
335        The summary rovides the counts of how many test cases fall into each
336        category, like "Passed", "Failed" etc.
337
338        Format of the string is:
339            Requested <int>, Executed <int>, ...
340
341        Returns:
342            A summary string of this test result.
343        """
344        l = ["%s %s" % (k, v) for k, v in self.summary_dict().items()]
345        # Sort the list so the order is the same every time.
346        msg = ", ".join(sorted(l))
347        return msg
348
349    def summary_dict(self):
350        """Gets a dictionary that summarizes the stats of this test result.
351
352        The summary rovides the counts of how many test cases fall into each
353        category, like "Passed", "Failed" etc.
354
355        Returns:
356            A dictionary with the stats of this test result.
357        """
358        d = {}
359        d["ControllerInfo"] = self.controller_info
360        d["Requested"] = len(self.requested)
361        d["Executed"] = len(self.executed)
362        d["Passed"] = len(self.passed)
363        d["Failed"] = len(self.failed)
364        d["Skipped"] = len(self.skipped)
365        d["Blocked"] = len(self.blocked)
366        d["Unknown"] = len(self.unknown)
367        return d
368