summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2021-05-03 07:56:05 -0400
committerNed Batchelder <ned@nedbatchelder.com>2021-05-03 08:17:39 -0400
commite36b42e2db46e892d9347ba0408c99b187ba8cb8 (patch)
tree58fb67d980bfc760f584f211e3af0c58d61d7dbf
parent0ee53f71c4e7145fca1b6d39c5fe60cb1eb3055b (diff)
downloadpython-coveragepy-git-e36b42e2db46e892d9347ba0408c99b187ba8cb8.tar.gz
fix: make data collection operations thread-safe
-rw-r--r--CHANGES.rst3
-rw-r--r--coverage/sqldata.py20
-rw-r--r--tests/test_data.py7
3 files changed, 29 insertions, 1 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 29af7340..3c65e5d8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -26,6 +26,9 @@ Unreleased
- Dropped support for Python 2.7, PyPy 2, and Python 3.5.
+- Data collection is now thread-safe. There may have been rare instances of
+ exceptions raised in multi-threaded programs.
+
- Plugins (like the `Django coverage plugin`_) were generating "Already
imported a file that will be measured" warnings about Django itself. These
have been fixed, closing `issue 1150`_.
diff --git a/coverage/sqldata.py b/coverage/sqldata.py
index 14279518..0b606d03 100644
--- a/coverage/sqldata.py
+++ b/coverage/sqldata.py
@@ -8,6 +8,7 @@
import collections
import datetime
+import functools
import glob
import itertools
import os
@@ -179,6 +180,10 @@ class CoverageData(SimpleReprMixin):
Data in a :class:`CoverageData` can be serialized and deserialized with
:meth:`dumps` and :meth:`loads`.
+ The methods used during the coverage.py collection phase
+ (:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and
+ :meth:`add_file_tracers`) are thread-safe. Other methods may not be.
+
"""
def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None):
@@ -207,6 +212,8 @@ class CoverageData(SimpleReprMixin):
# Maps thread ids to SqliteDb objects.
self._dbs = {}
self._pid = os.getpid()
+ # Synchronize the operations used during collection.
+ self._lock = threading.Lock()
# Are we in sync with the data file?
self._have_used = False
@@ -218,6 +225,15 @@ class CoverageData(SimpleReprMixin):
self._current_context_id = None
self._query_context_ids = None
+ def _locked(method): # pylint: disable=no-self-argument
+ """A decorator for methods that should hold self._lock."""
+ @functools.wraps(method)
+ def _wrapped(self, *args, **kwargs):
+ with self._lock:
+ # pylint: disable=not-callable
+ return method(self, *args, **kwargs)
+ return _wrapped
+
def _choose_filename(self):
"""Set self._filename based on inited attributes."""
if self._no_disk:
@@ -388,6 +404,7 @@ class CoverageData(SimpleReprMixin):
else:
return None
+ @_locked
def set_context(self, context):
"""Set the current context for future :meth:`add_lines` etc.
@@ -429,6 +446,7 @@ class CoverageData(SimpleReprMixin):
"""
return self._filename
+ @_locked
def add_lines(self, line_data):
"""Add measured line data.
@@ -461,6 +479,7 @@ class CoverageData(SimpleReprMixin):
(file_id, self._current_context_id, linemap),
)
+ @_locked
def add_arcs(self, arc_data):
"""Add measured arc data.
@@ -505,6 +524,7 @@ class CoverageData(SimpleReprMixin):
('has_arcs', str(int(arcs)))
)
+ @_locked
def add_file_tracers(self, file_tracers):
"""Add per-file plugin information.
diff --git a/tests/test_data.py b/tests/test_data.py
index 4b385b7f..be978e5e 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -486,10 +486,14 @@ class CoverageDataTest(DataTestHelpers, CoverageTest):
def test_thread_stress(self):
covdata = CoverageData()
+ exceptions = []
def thread_main():
"""Every thread will try to add the same data."""
- covdata.add_lines(LINES_1)
+ try:
+ covdata.add_lines(LINES_1)
+ except Exception as ex:
+ exceptions.append(ex)
threads = [threading.Thread(target=thread_main) for _ in range(10)]
for t in threads:
@@ -498,6 +502,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest):
t.join()
self.assert_lines1_data(covdata)
+ assert exceptions == []
class CoverageDataInTempDirTest(DataTestHelpers, CoverageTest):