Repository: google/mobly

Issue:
Test result entry should include stacktrace
Test result entry in test summary should include the stacktrace if an exception happened during test.
Right now we only see the message of the exception, not the whole stacktrace.


Discussion:

Resolving patch:
diff --git a/mobly/records.py b/mobly/records.py
--- a/mobly/records.py
+++ b/mobly/records.py
@@ -13,13 +13,14 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 """This module is where all the record definitions and record containers live.
 """
 
 import json
 import logging
 import pprint
+import sys
+import traceback
 
 from mobly import signals
 from mobly import utils
@@ -41,6 +42,7 @@ class TestResultEnums(object):
     RECORD_EXTRAS = 'Extras'
     RECORD_EXTRA_ERRORS = 'Extra Errors'
     RECORD_DETAILS = 'Details'
+    RECORD_STACKTRACE = 'Stacktrace'
     TEST_RESULT_PASS = 'PASS'
     TEST_RESULT_FAIL = 'FAIL'
     TEST_RESULT_SKIP = 'SKIP'
@@ -69,6 +71,7 @@ def __init__(self, t_name, t_class=None):
         self.result = None
         self.extras = None
         self.details = None
+        self.stacktrace = None
         self.extra_errors = {}
 
     def test_begin(self):
@@ -93,9 +96,15 @@ def _test_end(self, result, e):
             self.result = TestResultEnums.TEST_RESULT_ERROR
         if isinstance(e, signals.TestSignal):
             self.details = e.details
+            _, _, exc_traceback = sys.exc_info()
+            if exc_traceback:
+                self.stacktrace = ''.join(traceback.format_tb(exc_traceback))
             self.extras = e.extras
-        elif e:
+        elif isinstance(e, Exception):
             self.details = str(e)
+            _, _, exc_traceback = sys.exc_info()
+            if exc_traceback:
+                self.stacktrace = ''.join(traceback.format_tb(exc_traceback))
 
     def test_pass(self, e=None):
         """To mark the test as passed in this record.
@@ -174,6 +183,7 @@ def to_dict(self):
         d[TestResultEnums.RECORD_EXTRAS] = self.extras
         d[TestResultEnums.RECORD_DETAILS] = self.details
         d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
+        d[TestResultEnums.RECORD_STACKTRACE] = self.stacktrace
         return d
 
     def json_str(self):


Test patch:
diff --git a/tests/mobly/records_test.py b/tests/mobly/records_test.py
--- a/tests/mobly/records_test.py
+++ b/tests/mobly/records_test.py
@@ -19,6 +19,7 @@
 from mobly import records
 from mobly import signals
 
+
 class RecordsTest(unittest.TestCase):
     """This test class tests the implementation of classes in mobly.records.
     """
@@ -50,6 +51,7 @@ def verify_record(self, record, result, details, extras):
         d[records.TestResultEnums.RECORD_UID] = None
         d[records.TestResultEnums.RECORD_CLASS] = None
         d[records.TestResultEnums.RECORD_EXTRA_ERRORS] = {}
+        d[records.TestResultEnums.RECORD_STACKTRACE] = None
         actual_d = record.to_dict()
         self.assertDictEqual(actual_d, d)
         # Verify that these code paths do not cause crashes and yield non-empty
@@ -57,95 +59,126 @@ def verify_record(self, record, result, details, extras):
         self.assertTrue(str(record), "str of the record should not be empty.")
         self.assertTrue(repr(record), "the record's repr shouldn't be empty.")
         self.assertTrue(record.json_str(), ("json str of the record should "
-                         "not be empty."))
+                                            "not be empty."))
 
     """ Begin of Tests """
+
     def test_result_record_pass_none(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         record.test_pass()
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_PASS,
-                           details=None,
-                           extras=None)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_PASS,
+            details=None,
+            extras=None)
 
     def test_result_record_pass_with_float_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestPass(self.details, self.float_extra)
         record.test_pass(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_PASS,
-                           details=self.details,
-                           extras=self.float_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_PASS,
+            details=self.details,
+            extras=self.float_extra)
 
     def test_result_record_pass_with_json_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestPass(self.details, self.json_extra)
         record.test_pass(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_PASS,
-                           details=self.details,
-                           extras=self.json_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_PASS,
+            details=self.details,
+            extras=self.json_extra)
 
     def test_result_record_fail_none(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         record.test_fail()
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_FAIL,
-                           details=None,
-                           extras=None)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_FAIL,
+            details=None,
+            extras=None)
+
+    def test_result_record_fail_stacktrace(self):
+        record = records.TestResultRecord(self.tn)
+        record.test_begin()
+        try:
+            raise Exception('Something failed.')
+        except Exception as e:
+            record.test_fail(e)
+        # Verify stacktrace separately if we expect a non-None value.
+        # Because stacktrace includes file names and line numbers, we can't do
+        # a simple equality check.
+        self.assertTrue('Something failed.' in record.stacktrace)
+        self.assertTrue(
+            'in test_result_record_fail_stacktrace\n    raise Exception' in
+            record.stacktrace)
+        record.stacktrace = None
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_FAIL,
+            details='Something failed.',
+            extras=None)
 
     def test_result_record_fail_with_float_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestFailure(self.details, self.float_extra)
         record.test_fail(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_FAIL,
-                           details=self.details,
-                           extras=self.float_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_FAIL,
+            details=self.details,
+            extras=self.float_extra)
 
     def test_result_record_fail_with_json_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestFailure(self.details, self.json_extra)
         record.test_fail(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_FAIL,
-                           details=self.details,
-                           extras=self.json_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_FAIL,
+            details=self.details,
+            extras=self.json_extra)
 
     def test_result_record_skip_none(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         record.test_skip()
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_SKIP,
-                           details=None,
-                           extras=None)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_SKIP,
+            details=None,
+            extras=None)
 
     def test_result_record_skip_with_float_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestSkip(self.details, self.float_extra)
         record.test_skip(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_SKIP,
-                           details=self.details,
-                           extras=self.float_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_SKIP,
+            details=self.details,
+            extras=self.float_extra)
 
     def test_result_record_skip_with_json_extra(self):
         record = records.TestResultRecord(self.tn)
         record.test_begin()
         s = signals.TestSkip(self.details, self.json_extra)
         record.test_skip(s)
-        self.verify_record(record=record,
-                           result=records.TestResultEnums.TEST_RESULT_SKIP,
-                           details=self.details,
-                           extras=self.json_extra)
+        self.verify_record(
+            record=record,
+            result=records.TestResultEnums.TEST_RESULT_SKIP,
+            details=self.details,
+            extras=self.json_extra)
 
     def test_result_add_operator_success(self):
         record1 = records.TestResultRecord(self.tn)
@@ -201,9 +234,11 @@ def test_result_fail_class_with_special_error(self):
         record1.test_pass(s)
         tr = records.TestResult()
         tr.add_record(record1)
+
         class SpecialError(Exception):
             def __init__(self, arg1, arg2):
                 self.msg = "%s %s" % (arg1, arg2)
+
         se = SpecialError("haha", 42)
         record2 = records.TestResultRecord("SomeTest", se)
         tr.fail_class(record2)
@@ -251,5 +286,6 @@ def test_is_all_pass_with_fail_class(self):
         tr.fail_class(record1)
         self.assertFalse(tr.is_all_pass)
 
+
 if __name__ == "__main__":
-   unittest.main()
\ No newline at end of file
+    unittest.main()