Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,12 +559,17 @@ def load(self) -> None:

def _init_for_start(self) -> None:
"""Initialization for start()"""
self.orig_dir = None
# Construct the collector.
concurrency: list[str] = self.config.concurrency or []
coverpath_file = os.path.join(os.getcwd(), ".coverpath")
if "multiprocessing" in concurrency:
if self.config.config_file is None:
raise ConfigError("multiprocessing requires a configuration file")
patch_multiprocessing(rcfile=self.config.config_file)
if os.path.exists(coverpath_file):
with open(coverpath_file, encoding="utf-8") as cf:
self.orig_dir = cf.read().strip()

dycon = self.config.dynamic_context
if not dycon or dycon == "none":
Expand Down Expand Up @@ -656,6 +661,8 @@ def _init_for_start(self) -> None:

def _init_data(self, suffix: str | bool | None) -> None:
"""Create a data file if we don't have one yet."""
if not hasattr(self, "orig_dir"):
self.orig_dir = None
if self._data is None:
# Create the data file. We do this at construction time so that the
# data file will be written into the directory where the process
Expand All @@ -667,6 +674,7 @@ def _init_data(self, suffix: str | bool | None) -> None:
warn=self._warn,
debug=self._debug,
no_disk=self._no_disk,
orig_dir=self.orig_dir,
)
self._data_to_close.append(self._data)

Expand Down
2 changes: 2 additions & 0 deletions coverage/multiproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ def get_preparation_data_with_stowaway(name: str) -> dict[str, Any]:
"""Get the original preparation data, and also insert our stowaway."""
d = original_get_preparation_data(name)
d["stowaway"] = Stowaway(rcfile)
with open(".coverpath", "w", encoding="utf-8") as cpath:
cpath.write(d["orig_dir"])
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels kind of bad... even more configuration/hacking via the multiprocessing monkeypatching, but it does seem to work as part of storing the original working directory for the originally described issue, so that .coverage. files may be copied back there.

return d

spawn.get_preparation_data = get_preparation_data_with_stowaway
Expand Down
9 changes: 9 additions & 0 deletions coverage/sqldata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import glob
import itertools
import os
import shutil
import random
import socket
import sqlite3
Expand Down Expand Up @@ -232,6 +233,8 @@ def __init__(
no_disk: bool = False,
warn: TWarnFn | None = None,
debug: TDebugCtl | None = None,
*,
orig_dir = None,
) -> None:
"""Create a :class:`CoverageData` object to hold coverage-measured data.

Expand Down Expand Up @@ -271,6 +274,7 @@ def __init__(
self._current_context: str | None = None
self._current_context_id: int | None = None
self._query_context_ids: list[int] | None = None
self.orig_dir = orig_dir

__repr__ = auto_repr

Expand Down Expand Up @@ -898,6 +902,11 @@ def read(self) -> None:
def write(self) -> None:
"""Ensure the data is written to the data file."""
self._debug_dataio("Writing (no-op) data file", self._filename)
if self.orig_dir is not None:
for fname in os.listdir():
if fname.startswith(".coverage."):
if not os.path.exists(os.path.join(self.orig_dir, fname)):
shutil.copy(fname, self.orig_dir)
Copy link
Author

@tylerjereddy tylerjereddy Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line here does the trick to restore coverage of the missing add() function in the example below the fold when I install this feature branch locally:

import os
import tempfile
from concurrent.futures import ProcessPoolExecutor

def add(a, b):
    return a + b

def probe_dispatcher():
    orig_dir = os.getcwd()
    with tempfile.TemporaryDirectory() as temp_dir:
        os.chdir(temp_dir)
        dispatcher()
    os.chdir(orig_dir)

def dispatcher():
    futures = []
    with ProcessPoolExecutor() as executor:
        futures.append(executor.submit(add, 2, 2))
    for future in futures:
        future.result()

if __name__ == "__main__":
    probe_dispatcher()
image

whereas suppressing that shutil.copy() line restores the original bug:

image

While I can reproduce this lack of coverage with the new regression test I added here, I can't make that test pass on this feature branch at the moment... am I missing something about the meta testing/coverage testing harness?


def _start_using(self) -> None:
"""Call this before using the database at all."""
Expand Down
41 changes: 41 additions & 0 deletions tests/test_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import re
import sys
import threading
import concurrent.futures
import time

from types import ModuleType
Expand Down Expand Up @@ -412,6 +413,32 @@ def work(x):
return sum_range((x+1)*100)
"""

MULTI_CODE_DIR_CHANGE = """
import os
import tempfile
from concurrent.futures import ProcessPoolExecutor

def add(a, b):
return a + b

def probe_dispatcher():
orig_dir = os.getcwd()
with tempfile.TemporaryDirectory() as temp_dir:
os.chdir(temp_dir)
dispatcher()
os.chdir(orig_dir)

def dispatcher():
futures = []
with ProcessPoolExecutor({NPROCS}) as executor:
futures.append(executor.submit(add, 2, 2))
for future in futures:
future.result()

if __name__ == "__main__":
probe_dispatcher()
"""

MULTI_CODE = """
# Above this will be a definition of work().
import multiprocessing
Expand Down Expand Up @@ -522,6 +549,20 @@ def test_multiprocessing_simple(self, start_method: str) -> None:
start_method=start_method,
)

def test_gh_2065(self, start_method: str) -> None:
nprocs = 1
upto = 30
code = (MULTI_CODE_DIR_CHANGE).format(NPROCS=nprocs, UPTO=upto)
total = 0
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total is unused now since I'm not really wanting to write output from my sample test case... but perhaps more importantly I can't make this new regression test pass at the moment, despite the installed version of this feature branch doing the right thing for the reproducing external example as noted at https://github.com/nedbat/coveragepy/pull/2069/files#r2446215263. Am I missing something about the meta-testing/test harness here?

expected_out = f""
self.try_multiprocessing_code(
code,
expected_out,
concurrent.futures,
nprocs,
start_method=start_method,
)

def test_multiprocessing_append(self, start_method: str) -> None:
nprocs = 3
upto = 30
Expand Down
Loading