summaryrefslogtreecommitdiff
path: root/coverage
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 /coverage
parent0ee53f71c4e7145fca1b6d39c5fe60cb1eb3055b (diff)
downloadpython-coveragepy-git-e36b42e2db46e892d9347ba0408c99b187ba8cb8.tar.gz
fix: make data collection operations thread-safe
Diffstat (limited to 'coverage')
-rw-r--r--coverage/sqldata.py20
1 files changed, 20 insertions, 0 deletions
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.