1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16from __builtin__ import property
17"""This module is where all the record definitions and record containers live.
18"""
19
20import json
21import logging
22import pprint
23
24from vts.runners.host import signals
25from vts.runners.host import utils
26from vts.utils.python.common import list_utils
27
28
29class TestResultEnums(object):
30    """Enums used for TestResultRecord class.
31
32    Includes the tokens to mark test result with, and the string names for each
33    field in TestResultRecord.
34    """
35
36    RECORD_NAME = "Test Name"
37    RECORD_CLASS = "Test Class"
38    RECORD_BEGIN_TIME = "Begin Time"
39    RECORD_END_TIME = "End Time"
40    RECORD_RESULT = "Result"
41    RECORD_UID = "UID"
42    RECORD_EXTRAS = "Extras"
43    RECORD_EXTRA_ERRORS = "Extra Errors"
44    RECORD_DETAILS = "Details"
45    RECORD_TABLES = "Tables"
46    TEST_RESULT_PASS = "PASS"
47    TEST_RESULT_FAIL = "FAIL"
48    TEST_RESULT_SKIP = "SKIP"
49    TEST_RESULT_ERROR = "ERROR"
50
51
52class TestResultRecord(object):
53    """A record that holds the information of a test case execution.
54
55    Attributes:
56        test_name: A string representing the name of the test case.
57        begin_time: Epoch timestamp of when the test case started.
58        end_time: Epoch timestamp of when the test case ended.
59        uid: Unique identifier of a test case.
60        result: Test result, PASS/FAIL/SKIP.
61        extras: User defined extra information of the test result.
62        details: A string explaining the details of the test case.
63        tables: A dict of 2-dimensional lists containing tabular results.
64    """
65
66    def __init__(self, t_name, t_class=None):
67        self.test_name = t_name
68        self.test_class = t_class
69        self.begin_time = None
70        self.end_time = None
71        self.uid = None
72        self.result = None
73        self.extras = None
74        self.details = None
75        self.extra_errors = {}
76        self.tables = {}
77
78    @property
79    def fullname(self):
80        return '%s.%s' % (self.test_class, self.test_name)
81
82    def isSameTestCase(self, record):
83        return self.fullname == record.fullname
84
85    def testBegin(self):
86        """Call this when the test case it records begins execution.
87
88        Sets the begin_time of this record.
89        """
90        self.begin_time = utils.get_current_epoch_time()
91
92    def _testEnd(self, result, e):
93        """Class internal function to signal the end of a test case execution.
94
95        Args:
96            result: One of the TEST_RESULT enums in TestResultEnums.
97            e: A test termination signal (usually an exception object). It can
98                be any exception instance or of any subclass of
99                vts.runners.host.signals.TestSignal.
100        """
101        self.end_time = utils.get_current_epoch_time()
102        self.result = result
103        if isinstance(e, signals.TestSignal):
104            self.details = e.details
105            self.extras = e.extras
106        elif e:
107            self.details = str(e)
108
109    def testPass(self, e=None):
110        """To mark the test as passed in this record.
111
112        Args:
113            e: An instance of vts.runners.host.signals.TestPass.
114        """
115        self._testEnd(TestResultEnums.TEST_RESULT_PASS, e)
116
117    def testFail(self, e=None):
118        """To mark the test as failed in this record.
119
120        Only testFail does instance check because we want "assert xxx" to also
121        fail the test same way assert_true does.
122
123        Args:
124            e: An exception object. It can be an instance of AssertionError or
125                vts.runners.host.base_test.TestFailure.
126        """
127        self._testEnd(TestResultEnums.TEST_RESULT_FAIL, e)
128
129    def testSkip(self, e=None):
130        """To mark the test as skipped in this record.
131
132        Args:
133            e: An instance of vts.runners.host.signals.TestSkip.
134        """
135        self._testEnd(TestResultEnums.TEST_RESULT_SKIP, e)
136
137    def testError(self, e=None):
138        """To mark the test as error in this record.
139
140        Args:
141            e: An exception object.
142        """
143        self._testEnd(TestResultEnums.TEST_RESULT_ERROR, e)
144
145    def addError(self, tag, e):
146        """Add extra error happened during a test mark the test result as
147        ERROR.
148
149        If an error is added the test record, the record's result is equivalent
150        to the case where an uncaught exception happened.
151
152        Args:
153            tag: A string describing where this error came from, e.g. 'on_pass'.
154            e: An exception object.
155        """
156        self.result = TestResultEnums.TEST_RESULT_ERROR
157        self.extra_errors[tag] = str(e)
158
159    def addTable(self, name, rows):
160        """Add a table as part of the test result.
161
162        Args:
163            name: The table name.
164            rows: A 2-dimensional list which contains the data.
165        """
166        if name in self.tables:
167            logging.warning("Overwrite table %s" % name)
168        self.tables[name] = rows
169
170    def __str__(self):
171        d = self.getDict()
172        l = ["%s = %s" % (k, v) for k, v in d.items()]
173        s = ', '.join(l)
174        return s
175
176    def __repr__(self):
177        """This returns a short string representation of the test record."""
178        t = utils.epoch_to_human_time(self.begin_time)
179        return "%s %s %s" % (t, self.test_name, self.result)
180
181    def getDict(self):
182        """Gets a dictionary representating the content of this class.
183
184        Returns:
185            A dictionary representating the content of this class.
186        """
187        d = {}
188        d[TestResultEnums.RECORD_NAME] = self.test_name
189        d[TestResultEnums.RECORD_CLASS] = self.test_class
190        d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
191        d[TestResultEnums.RECORD_END_TIME] = self.end_time
192        d[TestResultEnums.RECORD_RESULT] = self.result
193        d[TestResultEnums.RECORD_UID] = self.uid
194        d[TestResultEnums.RECORD_EXTRAS] = self.extras
195        d[TestResultEnums.RECORD_DETAILS] = self.details
196        d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
197        d[TestResultEnums.RECORD_TABLES] = self.tables
198        return d
199
200    def jsonString(self):
201        """Converts this test record to a string in json format.
202
203        Format of the json string is:
204            {
205                'Test Name': <test name>,
206                'Begin Time': <epoch timestamp>,
207                'Details': <details>,
208                ...
209            }
210
211        Returns:
212            A json-format string representing the test record.
213        """
214        return json.dumps(self.getDict())
215
216
217class TestResult(object):
218    """A class that contains metrics of a test run.
219
220    This class is essentially a container of TestResultRecord objects.
221
222    Attributes:
223        self.requested: A list of records for tests requested by user.
224        self.failed: A list of records for tests failed.
225        self.executed: A list of records for tests that were actually executed.
226        self.passed: A list of records for tests passed.
227        self.skipped: A list of records for tests skipped.
228        self.error: A list of records for tests with error result token.
229        self.class_errors: A list of strings, the errors that occurred during
230                            class setup.
231        self._test_module_name: A string, test module's name.
232        self._test_module_timestamp: An integer, test module's execution start
233                                     timestamp.
234    """
235
236    def __init__(self):
237        self.requested = []
238        self.failed = []
239        self.executed = []
240        self.passed = []
241        self.skipped = []
242        self.error = []
243        self._test_module_name = None
244        self._test_module_timestamp = None
245        self.class_errors = []
246
247    def __add__(self, r):
248        """Overrides '+' operator for TestResult class.
249
250        The add operator merges two TestResult objects by concatenating all of
251        their lists together.
252
253        Args:
254            r: another instance of TestResult to be added
255
256        Returns:
257            A TestResult instance that's the sum of two TestResult instances.
258        """
259        if not isinstance(r, TestResult):
260            raise TypeError("Operand %s of type %s is not a TestResult." %
261                            (r, type(r)))
262        r.reportNonExecutedRecord()
263        sum_result = TestResult()
264        for name in sum_result.__dict__:
265            if name.startswith("_test_module"):
266                l_value = getattr(self, name)
267                r_value = getattr(r, name)
268                if l_value is None and r_value is None:
269                    continue
270                elif l_value is None and r_value is not None:
271                    value = r_value
272                elif l_value is not None and r_value is None:
273                    value = l_value
274                else:
275                    if name == "_test_module_name":
276                        if l_value != r_value:
277                            raise TypeError("_test_module_name is different.")
278                        value = l_value
279                    elif name == "_test_module_timestamp":
280                        if int(l_value) < int(r_value):
281                            value = l_value
282                        else:
283                            value = r_value
284                    else:
285                        raise TypeError("unknown _test_module* attribute.")
286                setattr(sum_result, name, value)
287            else:
288                l_value = list(getattr(self, name))
289                r_value = list(getattr(r, name))
290                setattr(sum_result, name, l_value + r_value)
291        return sum_result
292
293    def getNonPassingRecords(self, non_executed=True, failed=True, skipped=False, error=True):
294        """Returns a list of non-passing test records.
295
296        Returns:
297            a list of TestResultRecord, records that do not have passing result.
298            non_executed: bool, whether to include non-executed results
299            failed: bool, whether to include failed results
300            skipped: bool, whether to include skipped results
301            error: bool, whether to include error results
302        """
303        return ((self.getNonExecutedRecords() if non_executed else [])
304            + (self.failed if failed else [])
305            + (self.skipped if skipped else [])
306            + (self.error if error else []))
307
308    def getNonExecutedRecords(self):
309        """Returns a list of records that were requested but not executed."""
310        res = []
311
312        for requested in self.requested:
313            found = False
314
315            for executed in self.executed:
316                if requested.isSameTestCase(executed):
317                    found = True
318                    break
319
320            if not found:
321                res.append(requested)
322
323        return res
324
325    def reportNonExecutedRecord(self):
326        """Check and report any requested tests that did not finish.
327
328        Adds a test record to self.error list iff it is in requested list but not
329        self.executed result list.
330        """
331        for requested in self.getNonExecutedRecords():
332            requested.testBegin()
333            requested.testError(
334                "Unknown error: test case requested but not executed.")
335            self.error.append(requested)
336
337    def removeRecord(self, record, remove_requested=True):
338        """Remove a test record from test results.
339
340        Records will be ed using test_name and test_class attribute.
341        All entries that match the provided record in all result lists will
342        be removed after calling this method.
343
344        Args:
345            record: A test record object to add.
346            remove_requested: bool, whether to remove the test case from requested
347                              list as well.
348        """
349        lists = [
350            self.requested, self.failed, self.executed, self.passed,
351            self.skipped, self.error
352        ]
353
354        for l in lists:
355            indexToRemove = []
356            for idx in range(len(l)):
357                if l[idx].isSameTestCase(record):
358                    indexToRemove.append(idx)
359
360            for idx in reversed(indexToRemove):
361                del l[idx]
362
363    def addRecord(self, record):
364        """Adds a test record to test results.
365
366        A record is considered executed once it's added to the test result.
367
368        Args:
369            record: A test record object to add.
370        """
371        self.removeRecord(record, remove_requested=False)
372
373        self.executed.append(record)
374        if record.result == TestResultEnums.TEST_RESULT_FAIL:
375            self.failed.append(record)
376        elif record.result == TestResultEnums.TEST_RESULT_SKIP:
377            self.skipped.append(record)
378        elif record.result == TestResultEnums.TEST_RESULT_PASS:
379            self.passed.append(record)
380        else:
381            self.error.append(record)
382
383    def setTestModuleKeys(self, name, start_timestamp):
384        """Sets the test module's name and start_timestamp."""
385        self._test_module_name = name
386        self._test_module_timestamp = start_timestamp
387
388    def failClass(self, class_name, e):
389        """Add a record to indicate a test class setup has failed and no test
390        in the class was executed.
391
392        Args:
393            class_name: A string that is the name of the failed test class.
394            e: An exception object.
395        """
396        self.class_errors.append("%s: %s" % (class_name, e))
397
398    def passClass(self, class_name, e=None):
399        """Add a record to indicate a test class setup has passed and no test
400        in the class was executed.
401
402        Args:
403            class_name: A string that is the name of the failed test class.
404            e: An exception object.
405        """
406        record = TestResultRecord("setup_class", class_name)
407        record.testBegin()
408        record.testPass(e)
409        self.executed.append(record)
410        self.passed.append(record)
411
412    def skipClass(self, class_name, reason):
413        """Add a record to indicate all test cases in the class are skipped.
414
415        Args:
416            class_name: A string that is the name of the skipped test class.
417            reason: A string that is the reason for skipping.
418        """
419        record = TestResultRecord("skip_class", class_name)
420        record.testBegin()
421        record.testSkip(signals.TestSkip(reason))
422        self.executed.append(record)
423        self.skipped.append(record)
424
425    def jsonString(self):
426        """Converts this test result to a string in json format.
427
428        Format of the json string is:
429            {
430                "Results": [
431                    {<executed test record 1>},
432                    {<executed test record 2>},
433                    ...
434                ],
435                "Summary": <summary dict>
436            }
437
438        Returns:
439            A json-format string representing the test results.
440        """
441        records = list_utils.MergeUniqueKeepOrder(
442            self.executed, self.failed, self.passed, self.skipped, self.error)
443        executed = [record.getDict() for record in records]
444
445        d = {}
446        d["Results"] = executed
447        d["Summary"] = self.summaryDict()
448        d["TestModule"] = self.testModuleDict()
449        d["Class Errors"] = ("\n".join(self.class_errors)
450                             if self.class_errors else None)
451        jsonString = json.dumps(d, indent=4, sort_keys=True)
452        return jsonString
453
454    def summary(self):
455        """Gets a string that summarizes the stats of this test result.
456
457        The summary rovides the counts of how many test cases fall into each
458        category, like "Passed", "Failed" etc.
459
460        Format of the string is:
461            Requested <int>, Executed <int>, ...
462
463        Returns:
464            A summary string of this test result.
465        """
466        l = ["%s %d" % (k, v) for k, v in self.summaryDict().items()]
467        # Sort the list so the order is the same every time.
468        msg = ", ".join(sorted(l))
469        return msg
470
471    @property
472    def progressStr(self):
473        """Gets a string that shows test progress.
474
475        Format of the string is:
476          x/n, where x is number of executed + skipped + 1,
477          and n is number of requested tests.
478        """
479        return '%s/%s' % (len(self.executed) + len(self.skipped) + 1,
480                          len(self.requested))
481
482    def summaryDict(self):
483        """Gets a dictionary that summarizes the stats of this test result.
484
485        The summary rovides the counts of how many test cases fall into each
486        category, like "Passed", "Failed" etc.
487
488        Returns:
489            A dictionary with the stats of this test result.
490        """
491        d = {}
492        d["Requested"] = len(self.requested)
493        d["Executed"] = len(self.executed)
494        d["Passed"] = len(self.passed)
495        d["Failed"] = len(self.failed)
496        d["Skipped"] = len(self.skipped)
497        d["Error"] = len(self.error)
498        return d
499
500    def testModuleDict(self):
501        """Returns a dict that summarizes the test module DB indexing keys."""
502        d = {}
503        d["Name"] = self._test_module_name
504        d["Timestamp"] = self._test_module_timestamp
505        return d
506