Repository: numirias/pytest-json-report (language: python)

Issue:
Crashes when used with `--tb=native` option
Hello and thanks for this supper useful plugin!

The plugin crashes if used together with `--tb=native`

Essentially this is because pytest's `ReprEntryNative` and `ReprEntry` classes don't have the same interface.

Here's an sample traceback: 
```
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pytest_jsonreport/plugin.py", line 180, in pytest_runtest_logreport
INTERNALERROR>     self._config.hook.pytest_json_runtest_stage(report=report)
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/manager.py", line 337, in traced_hookexec
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/callers.py", line 52, in from_call
INTERNALERROR>     result = func()
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/manager.py", line 335, in <lambda>
INTERNALERROR>     outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs))
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/manager.py", line 87, in <lambda>
INTERNALERROR>     firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pytest_jsonreport/plugin.py", line 197, in pytest_json_runtest_stage
INTERNALERROR>     traceback,
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pytest_jsonreport/serialize.py", line 58, in make_teststage
INTERNALERROR>     stage['traceback'] = make_traceback(traceback)
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pytest_jsonreport/serialize.py", line 91, in make_traceback
INTERNALERROR>     } for entry in traceback.reprentries]
INTERNALERROR>   File "/home/rciorba/repos/pytest-json-report/.tox/py37/lib/python3.7/site-packages/pytest_jsonreport/serialize.py", line 91, in <listcomp>
INTERNALERROR>     } for entry in traceback.reprentries]
INTERNALERROR> AttributeError: 'ReprEntryNative' object has no attribute 'reprfileloc'
```



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index f1e8330..adecddb 100644
--- a/README.md
+++ b/README.md
@@ -350,11 +350,11 @@ A test stage item.
 | `duration` | Duration of the test stage in seconds. |
 | `outcome` | Outcome of the test stage. (can be different from the overall test outcome) |
 | `crash` | Crash entry. (absent if no error occurred) |
-| `traceback` | List of traceback entries. (absent if no error occurred) |
+| `traceback` | List of traceback entries. (absent if no error occurred; affected by `--tb` option) |
 | `stdout` | Standard output. (absent if none available) |
 | `stderr` | Standard error. (absent if none available) |
 | `log` | [Log](#log) entry. (absent if none available) |
-| `longrepr` | Representation of the error. (absent if no error occurred) |
+| `longrepr` | Representation of the error. (absent if no error occurred; format affected by `--tb` option) |
 
 #### Example
 


Test patch:
diff --git a/pytest_jsonreport/plugin.py b/pytest_jsonreport/plugin.py
index 8f661d7..957f5d6 100644
--- a/pytest_jsonreport/plugin.py
+++ b/pytest_jsonreport/plugin.py
@@ -187,20 +187,14 @@ def pytest_runtest_logreport(self, report):
 
     @pytest.hookimpl(trylast=True)
     def pytest_json_runtest_stage(self, report):
-        if self._must_omit('traceback'):
-            traceback = None
-        else:
-            try:
-                traceback = report.longrepr.reprtraceback
-            except AttributeError:
-                traceback = None
         stage_details = report._json_report_extra[report.when]
         return serialize.make_teststage(
             report,
+            # TODO Can we use pytest's BaseReport.capstdout/err/log here?
             stage_details.get('stdout'),
             stage_details.get('stderr'),
             stage_details.get('log'),
-            traceback,
+            self._must_omit('traceback'),
         )
 
     @pytest.hookimpl(tryfirst=True)
diff --git a/pytest_jsonreport/serialize.py b/pytest_jsonreport/serialize.py
index 687099d..97c4d9c 100644
--- a/pytest_jsonreport/serialize.py
+++ b/pytest_jsonreport/serialize.py
@@ -57,50 +57,50 @@ def make_testitem(nodeid, keywords, location):
     return item
 
 
-def make_teststage(report, stdout, stderr, log, traceback):
+def make_teststage(report, stdout, stderr, log, omit_traceback):
     """Return JSON-serializable test stage (setup/call/teardown)."""
     stage = {
         'duration': report.duration,
         'outcome': report.outcome,
     }
-    stage.update(make_crash(report))
-    if traceback:
-        stage['traceback'] = make_traceback(traceback)
+    crash = getattr(report.longrepr, 'reprcrash', None)
+    if crash is not None:
+        stage['crash'] = make_fileloc(crash)
+        if not omit_traceback:
+            try:
+                stage['traceback'] = [make_fileloc(x.reprfileloc) for x in
+                                      report.longrepr.reprtraceback.reprentries]
+            except AttributeError:
+                # Happens if no detailed tb entries are available (e.g. due to
+                # `--tb=native`, see `_pytest._code.code.ReprTracebackNative`).
+                # Then we can't provide any tb info beyond the raw error text
+                # in `longrepr`, so just pass quietly.
+                pass
     if stdout:
         stage['stdout'] = stdout
     if stderr:
         stage['stderr'] = stderr
     if log:
         stage['log'] = log
-    if report.longreprtext:
-        stage['longrepr'] = report.longreprtext
+    # Error representation string (attr is computed property, so get only once)
+    longrepr = report.longreprtext
+    if longrepr:
+        stage['longrepr'] = longrepr
     return stage
 
 
-def make_crash(report):
-    """Return JSON-serializable crash details."""
-    try:
-        crash = report.longrepr.reprcrash
-    except AttributeError:
-        return {}
+def make_fileloc(loc):
+    """Return JSON-serializable file location representation.
+
+    See `_pytest._code.code.ReprFileLocation`.
+    """
     return {
-        'crash': {
-            'path': crash.path,
-            'lineno': crash.lineno,
-            'message': crash.message,
-        },
+        'path': loc.path,
+        'lineno': loc.lineno,
+        'message': loc.message,
     }
 
 
-def make_traceback(traceback):
-    """Return JSON-serializable traceback details."""
-    return [{
-        'path': entry.reprfileloc.path,
-        'lineno': entry.reprfileloc.lineno,
-        'message': entry.reprfileloc.message,
-    } for entry in traceback.reprentries]
-
-
 def make_summary(tests, **kwargs):
     """Return JSON-serializable test result summary."""
     summary = Counter([t['outcome'] for t in tests.values()])
@@ -110,7 +110,7 @@ def make_summary(tests, **kwargs):
 
 
 def make_warning(warning_message, when):
-    # warning_message is a warnings.WarningMessage object
+    # `warning_message` is a stdlib warnings.WarningMessage object
     return {
         'message': str(warning_message.message),
         'category': warning_message.category.__name__,
diff --git a/tests/test_jsonreport.py b/tests/test_jsonreport.py
index 2e3ee8e..3a20361 100644
--- a/tests/test_jsonreport.py
+++ b/tests/test_jsonreport.py
@@ -187,6 +187,23 @@ def test_report_crash_and_traceback(tests):
     assert call['traceback'] == traceback
 
 
+def test_report_traceback_styles(make_json):
+    """Handle different traceback styles (`--tb=...`)."""
+    code = '''
+        def test_raise(): assert False
+        def test_raise_nested(): f = lambda: g; f()
+    '''
+    for style in ('long', 'short'):
+        data = make_json(code, ['--json-report', '--tb=%s' % style])
+        for i in (0, 1):
+            assert isinstance(data['tests'][i]['call']['traceback'], list)
+
+    for style in ('native', 'line', 'no'):
+        data = make_json(code, ['--json-report', '--tb=%s' % style])
+        for i in (0, 1):
+            assert 'traceback' not in data['tests'][i]['call']
+
+
 def test_report_item_deselected(make_json):
     data = make_json("""
         import pytest