Repository: abalkin/pytest-leaks (language: python)

Issue:
pytest-xdist compatibility
Currently, pytest-leaks does not report leaks properly with pytest-xdist. There's maybe an issue in the result reporting, which perhaps should be done via proper pytest channels instead of `self.leaks`. Or possibly somewhere else, I didn't check precisely why it doesn't work.


Discussion / hints:

Fix patch:
diff --git a/tox.ini b/tox.ini
index 4d9e3fd..41dffb1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,9 @@ envlist = flake8,rstlint,python2-debug,python3-debug,python2.7-dbg,python3-dbg,p
 skip_missing_interpreters = true
 
 [testenv]
-deps = pytest
+deps =
+  pytest
+  pytest-xdist
 commands = pytest {posargs:tests}
 
 [testenv:flake8]


Test patch:
diff --git a/pytest_leaks/plugin.py b/pytest_leaks/plugin.py
index a9ca197..cc84af0 100644
--- a/pytest_leaks/plugin.py
+++ b/pytest_leaks/plugin.py
@@ -6,6 +6,7 @@
 
 import sys
 import re
+import json
 
 from collections import OrderedDict
 
@@ -106,7 +107,9 @@ def __init__(self, config):
 
         # Get access to the builtin "runner" plugin.
         self.runner = config.pluginmanager.get_plugin('runner')
-        self.leaks = {}  # nodeid -> leaks
+
+        # Temporary storage for leak data
+        self._leaks = {}  # item.nodeid -> result
 
     def hunt_leaks(self, func):
         return hunt_leaks(func, self.stab, self.run)
@@ -177,16 +180,40 @@ def run_test():
                                           location=item.location)
             return True  # skip pytest implementation
         else:
-            self.leaks[item.nodeid] = call.result
+            self._leaks[item.nodeid] = call.result
 
         return  # proceed to pytest implementation
 
+    @pytest.hookimpl(hookwrapper=True)
+    def pytest_runtest_makereport(self, item, call):
+        outcome = yield
+
+        # Append leak report in 'call' phase
+        if call.when != 'call':
+            return
+
+        report = outcome.get_result()
+        leaks = self._leaks.pop(item.nodeid, None)
+        if leaks:
+            report.sections.append(('pytest-leaks', json.dumps(leaks)))
+            outcome.force_result(report)
+
+    def _leaks_from_report(self, report):
+        if report.when != "call":
+            return None
+
+        leaks = [data for key, data in report.get_sections('pytest-leaks')
+                 if key == 'pytest-leaks']
+        if leaks:
+            return Leaks(json.loads(leaks[0]))
+
+        return None
+
     @pytest.hookimpl(hookwrapper=True, trylast=True)
     def pytest_report_teststatus(self, report):
         outcome = yield
         if report.when == 'call' and report.outcome == 'passed':
-            leaks = self.leaks.get(report.nodeid)
-            if leaks:
+            if self._leaks_from_report(report):
                 # cat, letter, word
                 outcome.force_result(('leaked', 'L', 'LEAKED'))
 
@@ -194,19 +221,19 @@ def pytest_report_teststatus(self, report):
     def pytest_terminal_summary(self, terminalreporter, exitstatus):
         tr = terminalreporter
 
-        leaked = list(tr.getreports('leaked'))
-
         if 'pytest_sugar' in type(tr).__module__:
             # pytest-sugar doesn't run pytest_report_teststatus: ensure
             # leak summary gets shown
-            for rep in tr.getreports('passed'):
-                if self.leaks.get(rep.nodeid) and rep.when == "call":
-                    leaked.append(rep)
+            leaked = list(tr.getreports('passed'))
+        else:
+            leaked = list(tr.getreports('leaked'))
 
         if leaked:
             tr.write_sep("=", 'leaks summary', cyan=True)
             for rep in leaked:
-                tr.line("%s: %s" % (rep.nodeid, Leaks(self.leaks[rep.nodeid])))
+                leaks = self._leaks_from_report(rep)
+                if leaks:
+                    tr.line("%s: %s" % (rep.nodeid, leaks))
 
 
 class Namespace(object):
diff --git a/tests/test_leaks.py b/tests/test_leaks.py
index e583b7f..13d35b2 100644
--- a/tests/test_leaks.py
+++ b/tests/test_leaks.py
@@ -101,6 +101,21 @@ def test_leaks_checker(testdir):
     assert result.ret == 0
 
 
+def test_xdist_compatibility(testdir, request):
+    if not request.config.pluginmanager.hasplugin('xdist'):
+        pytest.skip('test requires pytest-xdist')
+
+    testdir.makepyfile(test_leaks_code)
+    testdir.plugins = ['xdist', 'leaks']
+    result = testdir.runpytest_subprocess('-R', ':', '-v', '-n2')
+    result.stdout.fnmatch_lines([
+        '*LEAKED*::test_refleaks*',
+        '*leaks summary*',
+        '*:test_refleaks: leaked references*',
+    ])
+    assert result.ret == 0
+
+
 leaking = None
 test_leaks_code = """
 garbage = []