summaryrefslogtreecommitdiff
path: root/coverage/monkey.py
blob: d0bdf009fb5fc2fb555dd193eb735f63c91bfb66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt

"""Monkey-patching to make coverage.py work right in some cases."""

import multiprocessing
import multiprocessing.process
import sys

# An attribute that will be set on modules to indicate that they have been
# monkey-patched.
PATCHED_MARKER = "_coverage$patched"

if sys.version_info >= (3, 4):
    klass = multiprocessing.process.BaseProcess
else:
    klass = multiprocessing.Process

original_bootstrap = klass._bootstrap


class ProcessWithCoverage(klass):
    """A replacement for multiprocess.Process that starts coverage."""
    def _bootstrap(self):
        """Wrapper around _bootstrap to start coverage."""
        from coverage import Coverage
        cov = Coverage(data_suffix=True)
        cov.start()
        try:
            return original_bootstrap(self)
        finally:
            cov.stop()
            cov.save()


class Stowaway(object):
    """An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
    def __getstate__(self):
        return {}

    def __setstate__(self, state_unused):
        patch_multiprocessing()


def patch_multiprocessing():
    """Monkey-patch the multiprocessing module.

    This enables coverage measurement of processes started by multiprocessing.
    This is wildly experimental!

    """
    if hasattr(multiprocessing, PATCHED_MARKER):
        return

    if sys.version_info >= (3, 4):
        klass._bootstrap = ProcessWithCoverage._bootstrap
    else:
        multiprocessing.Process = ProcessWithCoverage

    # When spawning processes rather than forking them, we have no state in the
    # new process.  We sneak in there with a Stowaway: we stuff one of our own
    # objects into the data that gets pickled and sent to the sub-process. When
    # the Stowaway is unpickled, it's __setstate__ method is called, which
    # re-applies the monkey-patch.
    # Windows only spawns, so this is needed to keep Windows working.
    try:
        from multiprocessing import spawn
        original_get_preparation_data = spawn.get_preparation_data
    except (ImportError, AttributeError):
        pass
    else:
        def get_preparation_data_with_stowaway(name):
            """Get the original preparation data, and also insert our stowaway."""
            d = original_get_preparation_data(name)
            d['stowaway'] = Stowaway()
            return d

        spawn.get_preparation_data = get_preparation_data_with_stowaway

    setattr(multiprocessing, PATCHED_MARKER, True)