@@ -219,6 +219,244 @@ def test_xdist_four_workers(tmp_path):
219219 assert [] == file_cov ['missing_lines' ]
220220
221221
222+ # ---------------------------------------------------------------------------
223+ # Regression tests for modules imported by a conftest.py before pytest_configure()
224+ # runs in an xdist worker (issue #84's "pre-imported modules" report). Adapted
225+ # from PR #85 (https://github.com/plasma-umass/slipcover/pull/85, author @nurikk),
226+ # who first diagnosed this gap and prototyped a fix by retroactively instrumenting
227+ # already-imported objects after the fact. Unfortunately, that approach has some
228+ # significant issues: on Python <3.12 the rewritten bytecode wasn't reinstalled,
229+ # and on 3.12+ branch coverage came back as a false 100% (no AST-level branch
230+ # preinstrumentation for modules that were already compiled). The fix here takes
231+ # a different angle: activate ImportManager earlier, before conftest.py is ever
232+ # read, so these modules go through the normal instrumentation path from the
233+ # start and need no after-the-fact repair.
234+ # ---------------------------------------------------------------------------
235+
236+
237+ _CONFTEST_PREIMPORT = """\
238+ import sys
239+ sys.path.insert(0, str(__import__('pathlib').Path(__file__).resolve().parent))
240+ import target # noqa: F401 -- imported before pytest_configure() runs in the worker
241+ """
242+
243+
244+ def test_xdist_preimported_module_covered (tmp_path , monkeypatch ):
245+ """A module imported by conftest.py before collection should still be covered."""
246+ monkeypatch .chdir (tmp_path )
247+
248+ (tmp_path / "target.py" ).write_text ("""\
249+ def greet(name):
250+ return f"hello {name}"
251+ """ )
252+ (tmp_path / "conftest.py" ).write_text (_CONFTEST_PREIMPORT )
253+ (tmp_path / "test_it.py" ).write_text ("""\
254+ from target import greet
255+
256+ def test_greet():
257+ assert greet("world") == "hello world"
258+ """ )
259+
260+ out = tmp_path / "out.json"
261+ result = subprocess .run (
262+ [sys .executable , '-m' , 'slipcover' , '--source' , str (tmp_path ),
263+ '--json' , '--out' , str (out ),
264+ '-m' , 'pytest' , '-n' , '2' , '-q' , 'test_it.py' ],
265+ cwd = str (tmp_path ), capture_output = True , text = True
266+ )
267+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
268+
269+ with out .open () as f :
270+ cov = json .load (f )
271+ check_summaries (cov )
272+
273+ keys = [k for k in cov ['files' ] if 'target.py' in k ]
274+ assert keys , f"target.py not in coverage: { list (cov ['files' ].keys ())} "
275+ assert cov ['files' ][keys [0 ]]['missing_lines' ] == []
276+
277+
278+ def test_xdist_preimported_property_covered (tmp_path , monkeypatch ):
279+ """Property getter bodies in a pre-imported module should be covered."""
280+ monkeypatch .chdir (tmp_path )
281+
282+ (tmp_path / "target.py" ).write_text ("""\
283+ class Config:
284+ @property
285+ def name(self):
286+ return "test"
287+
288+ @property
289+ def value(self):
290+ return 42
291+ """ )
292+ (tmp_path / "conftest.py" ).write_text (_CONFTEST_PREIMPORT )
293+ (tmp_path / "test_it.py" ).write_text ("""\
294+ from target import Config
295+
296+ def test_name():
297+ assert Config().name == "test"
298+
299+ def test_value():
300+ assert Config().value == 42
301+ """ )
302+
303+ out = tmp_path / "out.json"
304+ result = subprocess .run (
305+ [sys .executable , '-m' , 'slipcover' , '--source' , str (tmp_path ),
306+ '--json' , '--out' , str (out ),
307+ '-m' , 'pytest' , '-n' , '2' , '-q' , 'test_it.py' ],
308+ cwd = str (tmp_path ), capture_output = True , text = True
309+ )
310+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
311+
312+ with out .open () as f :
313+ cov = json .load (f )
314+ check_summaries (cov )
315+
316+ keys = [k for k in cov ['files' ] if 'target.py' in k ]
317+ assert keys , f"target.py not in coverage: { list (cov ['files' ].keys ())} "
318+ assert cov ['files' ][keys [0 ]]['missing_lines' ] == []
319+
320+
321+ def test_xdist_preimported_wrapped_function_covered (tmp_path , monkeypatch ):
322+ """functools.wraps-decorated function bodies in a pre-imported module should be covered."""
323+ monkeypatch .chdir (tmp_path )
324+
325+ (tmp_path / "target.py" ).write_text ("""\
326+ import functools
327+
328+ def decorator(fn):
329+ @functools.wraps(fn)
330+ def wrapper(*args, **kwargs):
331+ return fn(*args, **kwargs)
332+ return wrapper
333+
334+ @decorator
335+ def compute(x):
336+ return x * 2
337+ """ )
338+ (tmp_path / "conftest.py" ).write_text (_CONFTEST_PREIMPORT )
339+ (tmp_path / "test_it.py" ).write_text ("""\
340+ from target import compute
341+
342+ def test_compute():
343+ assert compute(21) == 42
344+ """ )
345+
346+ out = tmp_path / "out.json"
347+ result = subprocess .run (
348+ [sys .executable , '-m' , 'slipcover' , '--source' , str (tmp_path ),
349+ '--json' , '--out' , str (out ),
350+ '-m' , 'pytest' , '-n' , '2' , '-q' , 'test_it.py' ],
351+ cwd = str (tmp_path ), capture_output = True , text = True
352+ )
353+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
354+
355+ with out .open () as f :
356+ cov = json .load (f )
357+ check_summaries (cov )
358+
359+ keys = [k for k in cov ['files' ] if 'target.py' in k ]
360+ assert keys , f"target.py not in coverage: { list (cov ['files' ].keys ())} "
361+ # line 6: "return fn(*args, **kwargs)" inside the wrapped function body
362+ assert 6 not in cov ['files' ][keys [0 ]]['missing_lines' ]
363+
364+
365+ def test_xdist_preimported_nested_attr_covered (tmp_path , monkeypatch ):
366+ """A function stashed on a nested object attribute in a pre-imported module
367+ should be covered -- demonstrating early activation needs no bespoke
368+ object-graph walking to handle shapes like this."""
369+ monkeypatch .chdir (tmp_path )
370+
371+ (tmp_path / "target.py" ).write_text ("""\
372+ class Task:
373+ def __init__(self, fn):
374+ self.fn = fn
375+
376+ class Workflow:
377+ def __init__(self, task):
378+ self._task = task
379+
380+ def _impl(x):
381+ return x + 1
382+
383+ workflow = Workflow(Task(_impl))
384+ """ )
385+ (tmp_path / "conftest.py" ).write_text (_CONFTEST_PREIMPORT )
386+ (tmp_path / "test_it.py" ).write_text ("""\
387+ from target import workflow
388+
389+ def test_workflow():
390+ assert workflow._task.fn(41) == 42
391+ """ )
392+
393+ out = tmp_path / "out.json"
394+ result = subprocess .run (
395+ [sys .executable , '-m' , 'slipcover' , '--source' , str (tmp_path ),
396+ '--json' , '--out' , str (out ),
397+ '-m' , 'pytest' , '-n' , '2' , '-q' , 'test_it.py' ],
398+ cwd = str (tmp_path ), capture_output = True , text = True
399+ )
400+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
401+
402+ with out .open () as f :
403+ cov = json .load (f )
404+ check_summaries (cov )
405+
406+ keys = [k for k in cov ['files' ] if 'target.py' in k ]
407+ assert keys , f"target.py not in coverage: { list (cov ['files' ].keys ())} "
408+ # line 10: "return x + 1" inside _impl
409+ assert 10 not in cov ['files' ][keys [0 ]]['missing_lines' ]
410+
411+
412+ def test_xdist_preimported_module_branch_coverage (tmp_path , monkeypatch ):
413+ """Branch coverage for a pre-imported module must reflect real, not vacuous,
414+ coverage: only the branch actually exercised should be covered, and the
415+ untaken branch must show up in missing_branches rather than a false 100%."""
416+ monkeypatch .chdir (tmp_path )
417+
418+ (tmp_path / "target.py" ).write_text ("""\
419+ def check(x):
420+ if x > 0:
421+ return "positive"
422+ else:
423+ return "non-positive"
424+ """ )
425+ (tmp_path / "conftest.py" ).write_text (_CONFTEST_PREIMPORT )
426+ (tmp_path / "test_it.py" ).write_text ("""\
427+ from target import check
428+
429+ def test_positive():
430+ assert check(1) == "positive"
431+ """ )
432+
433+ out = tmp_path / "out.json"
434+ result = subprocess .run (
435+ [sys .executable , '-m' , 'slipcover' , '--branch' , '--source' , str (tmp_path ),
436+ '--json' , '--out' , str (out ),
437+ '-m' , 'pytest' , '-n' , '2' , '-q' , 'test_it.py' ],
438+ cwd = str (tmp_path ), capture_output = True , text = True
439+ )
440+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
441+
442+ with out .open () as f :
443+ cov = json .load (f )
444+ check_summaries (cov )
445+
446+ keys = [k for k in cov ['files' ] if 'target.py' in k ]
447+ assert keys , f"target.py not in coverage: { list (cov ['files' ].keys ())} "
448+ module_cov = cov ['files' ][keys [0 ]]
449+
450+ executed_branches = [tuple (b ) for b in module_cov .get ('executed_branches' , [])]
451+ missing_branches = [tuple (b ) for b in module_cov .get ('missing_branches' , [])]
452+
453+ # Only the true branch (line 2 -> line 3) was exercised; the else branch
454+ # (line 2 -> line 5) was never taken and must show up as missing -- a vacuous
455+ # "0 missing branches" here is exactly the false-100% bug being guarded against.
456+ assert (2 , 3 ) in executed_branches , f"true branch not covered: { executed_branches } "
457+ assert (2 , 5 ) in missing_branches , f"else branch should be missing: { missing_branches } "
458+
459+
222460def test_xdist_fail_under_uses_merged_coverage (tmp_path ):
223461 """--fail-under must be checked against the merged (all-workers) coverage,
224462 not just the coordinator process' own view. Under xdist, actual test code
0 commit comments