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

Issue:
Not working when using --last-failed
Using pytest-json-report 1.4.0 and pytest 6.2.5. There is a bug when using `pytest --last-failed --json-report`, and no all tests are executed. See the traceback:

INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/_pytest/main.py", line 269, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/_pytest/main.py", line 322, in _main
INTERNALERROR>     config.hook.pytest_collection(session=session)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/_pytest/main.py", line 333, in pytest_collection
INTERNALERROR>     session.perform_collect()
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/_pytest/main.py", line 637, in perform_collect
INTERNALERROR>     hook.pytest_collection_modifyitems(
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_callers.py", line 55, in _multicall
INTERNALERROR>     gen.send(outcome)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/_pytest/cacheprovider.py", line 350, in pytest_collection_modifyitems
INTERNALERROR>     config.hook.pytest_deselected(items=previously_passed)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pluggy/_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/opt/app-root/lib64/python3.8/site-packages/pytest_jsonreport/plugin.py", line 146, in pytest_deselected
INTERNALERROR>     item._json_collectitem['deselected'] = True
**INTERNALERROR> AttributeError: 'Function' object has no attribute '_json_collectitem'**

I fixed it for my use case by protecting the line `item._json_collectitem['deselected'] = True` with a `if hasattr(item, "_json_collectitem"):`, but I'm not sure if that's the most appropriate way to do it.


Discussion / hints:

Fix patch:
diff --git a/tox.ini b/tox.ini
index 8be34eb..ca635af 100644
--- a/tox.ini
+++ b/tox.ini
@@ -47,3 +47,4 @@ disable =
     no-self-use,
     too-many-instance-attributes,
     fixme,
+    consider-using-f-string,


Test patch:
diff --git a/pytest_jsonreport/plugin.py b/pytest_jsonreport/plugin.py
index 947eb66..fb1e0a2 100644
--- a/pytest_jsonreport/plugin.py
+++ b/pytest_jsonreport/plugin.py
@@ -143,7 +143,10 @@ def pytest_deselected(self, items):
         if self._must_omit('collectors'):
             return
         for item in items:
-            item._json_collectitem['deselected'] = True
+            try:
+                item._json_collectitem['deselected'] = True
+            except AttributeError:
+                continue
 
     @pytest.hookimpl(hookwrapper=True)
     def pytest_collection_modifyitems(self, items):
@@ -249,7 +252,7 @@ def save_report(self, path):
                 import errno  # pylint: disable=import-outside-toplevel
                 if e.errno != errno.EEXIST:
                     raise
-        with open(path, 'w') as f:
+        with open(path, 'w', encoding='utf-8') as f:
             json.dump(
                 self.report,
                 f,
diff --git a/tests/test_jsonreport.py b/tests/test_jsonreport.py
index 366b4cb..bcb8006 100644
--- a/tests/test_jsonreport.py
+++ b/tests/test_jsonreport.py
@@ -592,3 +592,18 @@ def test_bug_41(misc_testdir):
     """#41: Create report file path if it doesn't exist."""
     misc_testdir.runpytest('--json-report', '--json-report-file=x/report.json')
     assert (misc_testdir.tmpdir / 'x/report.json').exists()
+
+
+def test_bug_69(testdir):
+    """#69: Handle deselection of test items that have not been collected."""
+    fn = testdir.makepyfile('''
+        def test_pass():
+            assert True
+        def test_fail():
+            assert False
+    ''').strpath
+    assert testdir.runpytest('--json-report', fn).ret == 1
+    # In this second run, `--last-failed` causes `test_pass` to not be
+    # *collected* but still explicitly *deselected*, so we assert there is no
+    # internal error caused by trying to access the collector obj.
+    assert testdir.runpytest('--json-report', '--last-failed', fn).ret == 1