Skip to content

Commit e36b42e

Browse files
committed
fix: make data collection operations thread-safe
1 parent 0ee53f7 commit e36b42e

File tree

3 files changed

+29
-1
lines changed

3 files changed

+29
-1
lines changed

CHANGES.rst

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Unreleased
2626

2727
- Dropped support for Python 2.7, PyPy 2, and Python 3.5.
2828

29+
- Data collection is now thread-safe. There may have been rare instances of
30+
exceptions raised in multi-threaded programs.
31+
2932
- Plugins (like the `Django coverage plugin`_) were generating "Already
3033
imported a file that will be measured" warnings about Django itself. These
3134
have been fixed, closing `issue 1150`_.

coverage/sqldata.py

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import collections
1010
import datetime
11+
import functools
1112
import glob
1213
import itertools
1314
import os
@@ -179,6 +180,10 @@ class CoverageData(SimpleReprMixin):
179180
Data in a :class:`CoverageData` can be serialized and deserialized with
180181
:meth:`dumps` and :meth:`loads`.
181182
183+
The methods used during the coverage.py collection phase
184+
(:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and
185+
:meth:`add_file_tracers`) are thread-safe. Other methods may not be.
186+
182187
"""
183188

184189
def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None):
@@ -207,6 +212,8 @@ def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=N
207212
# Maps thread ids to SqliteDb objects.
208213
self._dbs = {}
209214
self._pid = os.getpid()
215+
# Synchronize the operations used during collection.
216+
self._lock = threading.Lock()
210217

211218
# Are we in sync with the data file?
212219
self._have_used = False
@@ -218,6 +225,15 @@ def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=N
218225
self._current_context_id = None
219226
self._query_context_ids = None
220227

228+
def _locked(method): # pylint: disable=no-self-argument
229+
"""A decorator for methods that should hold self._lock."""
230+
@functools.wraps(method)
231+
def _wrapped(self, *args, **kwargs):
232+
with self._lock:
233+
# pylint: disable=not-callable
234+
return method(self, *args, **kwargs)
235+
return _wrapped
236+
221237
def _choose_filename(self):
222238
"""Set self._filename based on inited attributes."""
223239
if self._no_disk:
@@ -388,6 +404,7 @@ def _context_id(self, context):
388404
else:
389405
return None
390406

407+
@_locked
391408
def set_context(self, context):
392409
"""Set the current context for future :meth:`add_lines` etc.
393410
@@ -429,6 +446,7 @@ def data_filename(self):
429446
"""
430447
return self._filename
431448

449+
@_locked
432450
def add_lines(self, line_data):
433451
"""Add measured line data.
434452
@@ -461,6 +479,7 @@ def add_lines(self, line_data):
461479
(file_id, self._current_context_id, linemap),
462480
)
463481

482+
@_locked
464483
def add_arcs(self, arc_data):
465484
"""Add measured arc data.
466485
@@ -505,6 +524,7 @@ def _choose_lines_or_arcs(self, lines=False, arcs=False):
505524
('has_arcs', str(int(arcs)))
506525
)
507526

527+
@_locked
508528
def add_file_tracers(self, file_tracers):
509529
"""Add per-file plugin information.
510530

tests/test_data.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -486,10 +486,14 @@ def test_read_and_write_are_opposites(self):
486486

487487
def test_thread_stress(self):
488488
covdata = CoverageData()
489+
exceptions = []
489490

490491
def thread_main():
491492
"""Every thread will try to add the same data."""
492-
covdata.add_lines(LINES_1)
493+
try:
494+
covdata.add_lines(LINES_1)
495+
except Exception as ex:
496+
exceptions.append(ex)
493497

494498
threads = [threading.Thread(target=thread_main) for _ in range(10)]
495499
for t in threads:
@@ -498,6 +502,7 @@ def thread_main():
498502
t.join()
499503

500504
self.assert_lines1_data(covdata)
505+
assert exceptions == []
501506

502507

503508
class CoverageDataInTempDirTest(DataTestHelpers, CoverageTest):

0 commit comments

Comments
 (0)