Skip to content

Commit 780c0e0

Browse files
committed
Add fix for sloppy tests that mess with CWD and break coverage's internals. Fixes nedbat/coveragepy#881 and #306 others.
1 parent fba1c45 commit 780c0e0

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

src/pytest_cov/engine.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Coverage controllers for use by pytest-cov and nose-cov."""
22
import contextlib
33
import copy
4+
import functools
45
import os
56
import random
67
import socket
@@ -31,6 +32,25 @@ def _backup(obj, attr):
3132
setattr(obj, attr, backup)
3233

3334

35+
def _ensure_topdir(meth):
36+
@functools.wraps(meth)
37+
def ensure_topdir_wrapper(self, *args, **kwargs):
38+
try:
39+
original_cwd = os.getcwd()
40+
except OSError:
41+
# Looks like it's gone, this is non-ideal because a side-effect will
42+
# be introduced in the tests here but we can't do anything about it.
43+
original_cwd = None
44+
os.chdir(self.topdir)
45+
try:
46+
return meth(self, *args, **kwargs)
47+
finally:
48+
if original_cwd is not None:
49+
os.chdir(original_cwd)
50+
51+
return ensure_topdir_wrapper
52+
53+
3454
class CovController(object):
3555
"""Base class for different plugin implementations."""
3656

@@ -50,15 +70,26 @@ def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, c
5070
self.node_descs = set()
5171
self.failed_workers = []
5272
self.topdir = os.getcwd()
73+
self.is_collocated = None
5374

75+
@contextlib.contextmanager
76+
def ensure_topdir(self):
77+
original_cwd = os.getcwd()
78+
os.chdir(self.topdir)
79+
yield
80+
os.chdir(original_cwd)
81+
82+
@_ensure_topdir
5483
def pause(self):
5584
self.cov.stop()
5685
self.unset_env()
5786

87+
@_ensure_topdir
5888
def resume(self):
5989
self.cov.start()
6090
self.set_env()
6191

92+
@_ensure_topdir
6293
def set_env(self):
6394
"""Put info about coverage into the env so that subprocesses can activate coverage."""
6495
if self.cov_source is None:
@@ -99,6 +130,7 @@ def sep(stream, s, txt):
99130
out = '%s %s %s\n' % (s * sep_len, txt, s * (sep_len + sep_extra))
100131
stream.write(out)
101132

133+
@_ensure_topdir
102134
def summary(self, stream):
103135
"""Produce coverage reports."""
104136
total = None
@@ -171,6 +203,7 @@ def summary(self, stream):
171203
class Central(CovController):
172204
"""Implementation for centralised operation."""
173205

206+
@_ensure_topdir
174207
def start(self):
175208
cleanup()
176209

@@ -190,6 +223,7 @@ def start(self):
190223
self.cov.start()
191224
self.set_env()
192225

226+
@_ensure_topdir
193227
def finish(self):
194228
"""Stop coverage, save data to file and set the list of coverage objects to report on."""
195229

@@ -209,6 +243,7 @@ def finish(self):
209243
class DistMaster(CovController):
210244
"""Implementation for distributed master."""
211245

246+
@_ensure_topdir
212247
def start(self):
213248
cleanup()
214249

@@ -259,7 +294,7 @@ def testnodedown(self, node, error):
259294
socket.gethostname(), os.getpid(),
260295
random.randint(0, 999999),
261296
output['cov_worker_node_id']
262-
)
297+
)
263298

264299
cov = coverage.Coverage(source=self.cov_source,
265300
branch=self.cov_branch,
@@ -284,6 +319,7 @@ def testnodedown(self, node, error):
284319
node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info)
285320
self.node_descs.add(node_desc)
286321

322+
@_ensure_topdir
287323
def finish(self):
288324
"""Combines coverage data and sets the list of coverage objects to report on."""
289325

@@ -299,7 +335,9 @@ def finish(self):
299335
class DistWorker(CovController):
300336
"""Implementation for distributed workers."""
301337

338+
@_ensure_topdir
302339
def start(self):
340+
303341
cleanup()
304342

305343
# Determine whether we are collocated with master.
@@ -323,6 +361,7 @@ def start(self):
323361
self.cov.start()
324362
self.set_env()
325363

364+
@_ensure_topdir
326365
def finish(self):
327366
"""Stop coverage and send relevant info back to the master."""
328367
self.unset_env()

src/pytest_cov/plugin.py

+2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def __init__(self, options, pluginmanager, start=True):
142142
self.cov_total = None
143143
self.failed = False
144144
self._started = False
145+
self._start_path = None
145146
self._disabled = False
146147
self.options = options
147148

@@ -189,6 +190,7 @@ class Config(object):
189190
)
190191
self.cov_controller.start()
191192
self._started = True
193+
self._start_path = os.getcwd()
192194
cov_config = self.cov_controller.cov.config
193195
if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'):
194196
self.options.cov_fail_under = cov_config.fail_under

tests/test_pytest_cov.py

+37
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,43 @@ def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop):
544544
assert result.ret == 0
545545

546546

547+
@xdist_params
548+
def test_borken_cwd(testdir, monkeypatch, opts):
549+
mod = testdir.makepyfile(mod='''
550+
def foobar(a, b):
551+
return a + b
552+
''')
553+
554+
script = testdir.makepyfile('''
555+
import os
556+
import pytest
557+
import mod
558+
559+
@pytest.fixture
560+
def bad():
561+
if not os.path.exists('/tmp/crappo'):
562+
os.mkdir('/tmp/crappo')
563+
os.chdir('/tmp/crappo')
564+
yield
565+
os.rmdir('/tmp/crappo')
566+
567+
def test_foobar(bad):
568+
assert mod.foobar(1, 2) == 3
569+
''')
570+
result = testdir.runpytest('-v', '-s',
571+
'--cov=mod',
572+
'--cov-branch',
573+
script, *opts.split())
574+
575+
result.stdout.fnmatch_lines([
576+
'*- coverage: platform *, python * -*',
577+
'*mod* 100%',
578+
'*1 passed*',
579+
])
580+
581+
assert result.ret == 0
582+
583+
547584
def test_subprocess_with_path_aliasing(testdir, monkeypatch):
548585
src = testdir.mkdir('src')
549586
src.join('parent_script.py').write(SCRIPT_PARENT)

0 commit comments

Comments
 (0)