• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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#
16"""This module is where all the record definitions and record containers live.
17"""
18
19import json
20import logging
21import pprint
22
23from vts.runners.host import signals
24from vts.runners.host import utils
25from vts.utils.python.common import list_utils
26
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    RECORD_TABLES = "Tables"
45    TEST_RESULT_PASS = "PASS"
46    TEST_RESULT_FAIL = "FAIL"
47    TEST_RESULT_SKIP = "SKIP"
48    TEST_RESULT_ERROR = "ERROR"
49
50
51class TestResultRecord(object):
52    """A record that holds the information of a test case execution.
53
54    Attributes:
55        test_name: A string representing the name of the test case.
56        begin_time: Epoch timestamp of when the test case started.
57        end_time: Epoch timestamp of when the test case ended.
58        uid: Unique identifier of a test case.
59        result: Test result, PASS/FAIL/SKIP.
60        extras: User defined extra information of the test result.
61        details: A string explaining the details of the test case.
62        tables: A dict of 2-dimensional lists containing tabular results.
63    """
64
65    def __init__(self, t_name, t_class=None):
66        self.test_name = t_name
67        self.test_class = t_class
68        self.begin_time = None
69        self.end_time = None
70        self.uid = None
71        self.result = None
72        self.extras = None
73        self.details = None
74        self.extra_errors = {}
75        self.tables = {}
76
77    def testBegin(self):
78        """Call this when the test case it records begins execution.
79
80        Sets the begin_time of this record.
81        """
82        self.begin_time = utils.get_current_epoch_time()
83
84    def _testEnd(self, result, e):
85        """Class internal function to signal the end of a test case execution.
86
87        Args:
88            result: One of the TEST_RESULT enums in TestResultEnums.
89            e: A test termination signal (usually an exception object). It can
90                be any exception instance or of any subclass of
91                vts.runners.host.signals.TestSignal.
92        """
93        self.end_time = utils.get_current_epoch_time()
94        self.result = result
95        if isinstance(e, signals.TestSignal):
96            self.details = e.details
97            self.extras = e.extras
98        elif e:
99            self.details = str(e)
100
101    def testPass(self, e=None):
102        """To mark the test as passed in this record.
103
104        Args:
105            e: An instance of vts.runners.host.signals.TestPass.
106        """
107        self._testEnd(TestResultEnums.TEST_RESULT_PASS, e)
108
109    def testFail(self, e=None):
110        """To mark the test as failed in this record.
111
112        Only testFail does instance check because we want "assert xxx" to also
113        fail the test same way assert_true does.
114
115        Args:
116            e: An exception object. It can be an instance of AssertionError or
117                vts.runners.host.base_test.TestFailure.
118        """
119        self._testEnd(TestResultEnums.TEST_RESULT_FAIL, e)
120
121    def testSkip(self, e=None):
122        """To mark the test as skipped in this record.
123
124        Args:
125            e: An instance of vts.runners.host.signals.TestSkip.
126        """
127        self._testEnd(TestResultEnums.TEST_RESULT_SKIP, e)
128
129    def testError(self, e=None):
130        """To mark the test as error in this record.
131
132        Args:
133            e: An exception object.
134        """
135        self._testEnd(TestResultEnums.TEST_RESULT_ERROR, e)
136
137    def addError(self, tag, e):
138        """Add extra error happened during a test mark the test result as
139        ERROR.
140
141        If an error is added the test record, the record's result is equivalent
142        to the case where an uncaught exception happened.
143
144        Args:
145            tag: A string describing where this error came from, e.g. 'on_pass'.
146            e: An exception object.
147        """
148        self.result = TestResultEnums.TEST_RESULT_ERROR
149        self.extra_errors[tag] = str(e)
150
151    def addTable(self, name, rows):
152        """Add a table as part of the test result.
153
154        Args:
155            name: The table name.
156            rows: A 2-dimensional list which contains the data.
157        """
158        if name in self.tables:
159            logging.warning("Overwrite table %s" % name)
160        self.tables[name] = rows
161
162    def __str__(self):
163        d = self.getDict()
164        l = ["%s = %s" % (k, v) for k, v in d.items()]
165        s = ', '.join(l)
166        return s
167
168    def __repr__(self):
169        """This returns a short string representation of the test record."""
170        t = utils.epoch_to_human_time(self.begin_time)
171        return "%s %s %s" % (t, self.test_name, self.result)
172
173    def getDict(self):
174        """Gets a dictionary representating the content of this class.
175
176        Returns:
177            A dictionary representating the content of this class.
178        """
179        d = {}
180        d[TestResultEnums.RECORD_NAME] = self.test_name
181        d[TestResultEnums.RECORD_CLASS] = self.test_class
182        d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
183        d[TestResultEnums.RECORD_END_TIME] = self.end_time
184        d[TestResultEnums.RECORD_RESULT] = self.result
185        d[TestResultEnums.RECORD_UID] = self.uid
186        d[TestResultEnums.RECORD_EXTRAS] = self.extras
187        d[TestResultEnums.RECORD_DETAILS] = self.details
188        d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
189        d[TestResultEnums.RECORD_TABLES] = self.tables
190        return d
191
192    def jsonString(self):
193        """Converts this test record to a string in json format.
194
195        Format of the json string is:
196            {
197                'Test Name': <test name>,
198                'Begin Time': <epoch timestamp>,
199                'Details': <details>,
200                ...
201            }
202
203        Returns:
204            A json-format string representing the test record.
205        """
206        return json.dumps(self.getDict())
207
208
209class TestResult(object):
210    """A class that contains metrics of a test run.
211
212    This class is essentially a container of TestResultRecord objects.
213
214    Attributes:
215        self.requested: A list of records for tests requested by user.
216        self.failed: A list of records for tests failed.
217        self.executed: A list of records for tests that were actually executed.
218        self.passed: A list of records for tests passed.
219        self.skipped: A list of records for tests skipped.
220        self.error: A list of records for tests with error result token.
221        self._test_module_name: A string, test module's name.
222        self._test_module_timestamp: An integer, test module's execution start
223                                     timestamp.
224    """
225
226    def __init__(self):
227        self.requested = []
228        self.failed = []
229        self.executed = []
230        self.passed = []
231        self.skipped = []
232        self.error = []
233        self._test_module_name = None
234        self._test_module_timestamp = None
235
236    def __add__(self, r):
237        """Overrides '+' operator for TestResult class.
238
239        The add operator merges two TestResult objects by concatenating all of
240        their lists together.
241
242        Args:
243            r: another instance of TestResult to be added
244
245        Returns:
246            A TestResult instance that's the sum of two TestResult instances.
247        """
248        if not isinstance(r, TestResult):
249            raise TypeError("Operand %s of type %s is not a TestResult." %
250                            (r, type(r)))
251        r.reportNonExecutedRecord()
252        sum_result = TestResult()
253        for name in sum_result.__dict__:
254            if name.startswith("_test_module"):
255                l_value = getattr(self, name)
256                r_value = getattr(r, name)
257                if l_value is None and r_value is None:
258                    continue
259                elif l_value is None and r_value is not None:
260                    value = r_value
261                elif l_value is not None and r_value is None:
262                    value = l_value
263                else:
264                    if name == "_test_module_name":
265                        if l_value != r_value:
266                            raise TypeError("_test_module_name is different.")
267                        value = l_value
268                    elif name == "_test_module_timestamp":
269                        if int(l_value) < int(r_value):
270                            value = l_value
271                        else:
272                            value = r_value
273                    else:
274                        raise TypeError("unknown _test_module* attribute.")
275                setattr(sum_result, name, value)
276            else:
277                l_value = list(getattr(self, name))
278                r_value = list(getattr(r, name))
279                setattr(sum_result, name, l_value + r_value)
280        return sum_result
281
282    def reportNonExecutedRecord(self):
283        """Check and report any requested tests that did not finish.
284
285        Adds a test record to self.error list iff it is in requested list but not
286        self.executed result list.
287        """
288        for requested in self.requested:
289            found = False
290
291            for executed in self.executed:
292                if (requested.test_name == executed.test_name and
293                        requested.test_class == executed.test_class):
294                    found = True
295                    break
296
297            if not found:
298                requested.testBegin()
299                requested.testError()
300                self.error.append(requested)
301
302    def removeRecord(self, record):
303        """Remove a test record from test results.
304
305        Records will be ed using test_name and test_class attribute.
306        All entries that match the provided record in all result lists will
307        be removed after calling this method.
308
309        Args:
310            record: A test record object to add.
311        """
312        lists = [
313            self.requested, self.failed, self.executed, self.passed,
314            self.skipped, self.error
315        ]
316
317        for l in lists:
318            indexToRemove = []
319            for idx in range(len(l)):
320                if (l[idx].test_name == record.test_name and
321                        l[idx].test_class == record.test_class):
322                    indexToRemove.append(idx)
323
324            for idx in reversed(indexToRemove):
325                del l[idx]
326
327    def addRecord(self, record):
328        """Adds a test record to test results.
329
330        A record is considered executed once it's added to the test result.
331
332        Args:
333            record: A test record object to add.
334        """
335        self.executed.append(record)
336        if record.result == TestResultEnums.TEST_RESULT_FAIL:
337            self.failed.append(record)
338        elif record.result == TestResultEnums.TEST_RESULT_SKIP:
339            self.skipped.append(record)
340        elif record.result == TestResultEnums.TEST_RESULT_PASS:
341            self.passed.append(record)
342        else:
343            self.error.append(record)
344
345    def setTestModuleKeys(self, name, start_timestamp):
346        """Sets the test module's name and start_timestamp."""
347        self._test_module_name = name
348        self._test_module_timestamp = start_timestamp
349
350    def failClass(self, class_name, e):
351        """Add a record to indicate a test class setup has failed and no test
352        in the class was executed.
353
354        Args:
355            class_name: A string that is the name of the failed test class.
356            e: An exception object.
357        """
358        record = TestResultRecord("setup_class", class_name)
359        record.testBegin()
360        record.testFail(e)
361        self.executed.append(record)
362        self.failed.append(record)
363
364    def skipClass(self, class_name, reason):
365        """Add a record to indicate all test cases in the class are skipped.
366
367        Args:
368            class_name: A string that is the name of the skipped test class.
369            reason: A string that is the reason for skipping.
370        """
371        record = TestResultRecord("unknown", class_name)
372        record.testBegin()
373        record.testSkip(signals.TestSkip(reason))
374        self.executed.append(record)
375        self.skipped.append(record)
376
377    def jsonString(self):
378        """Converts this test result to a string in json format.
379
380        Format of the json string is:
381            {
382                "Results": [
383                    {<executed test record 1>},
384                    {<executed test record 2>},
385                    ...
386                ],
387                "Summary": <summary dict>
388            }
389
390        Returns:
391            A json-format string representing the test results.
392        """
393        records = list_utils.MergeUniqueKeepOrder(
394            self.executed, self.failed, self.passed, self.skipped, self.error)
395        executed = [record.getDict() for record in records]
396
397        d = {}
398        d["Results"] = executed
399        d["Summary"] = self.summaryDict()
400        d["TestModule"] = self.testModuleDict()
401        jsonString = json.dumps(d, indent=4, sort_keys=True)
402        return jsonString
403
404    def summary(self):
405        """Gets a string that summarizes the stats of this test result.
406
407        The summary rovides the counts of how many test cases fall into each
408        category, like "Passed", "Failed" etc.
409
410        Format of the string is:
411            Requested <int>, Executed <int>, ...
412
413        Returns:
414            A summary string of this test result.
415        """
416        l = ["%s %d" % (k, v) for k, v in self.summaryDict().items()]
417        # Sort the list so the order is the same every time.
418        msg = ", ".join(sorted(l))
419        return msg
420
421    def summaryDict(self):
422        """Gets a dictionary that summarizes the stats of this test result.
423
424        The summary rovides the counts of how many test cases fall into each
425        category, like "Passed", "Failed" etc.
426
427        Returns:
428            A dictionary with the stats of this test result.
429        """
430        d = {}
431        d["Requested"] = len(self.requested)
432        d["Executed"] = len(self.executed)
433        d["Passed"] = len(self.passed)
434        d["Failed"] = len(self.failed)
435        d["Skipped"] = len(self.skipped)
436        d["Error"] = len(self.error)
437        return d
438
439    def testModuleDict(self):
440        """Returns a dict that summarizes the test module DB indexing keys."""
441        d = {}
442        d["Name"] = self._test_module_name
443        d["Timestamp"] = self._test_module_timestamp
444        return d
445