multiproc.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
  2. # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
  3. """Monkey-patching to add multiprocessing support for coverage.py"""
  4. import multiprocessing
  5. import multiprocessing.process
  6. import os
  7. import sys
  8. from coverage.misc import contract
  9. # An attribute that will be set on the module to indicate that it has been
  10. # monkey-patched.
  11. PATCHED_MARKER = "_coverage$patched"
  12. # The environment variable that specifies the rcfile for subprocesses.
  13. COVERAGE_RCFILE_ENV = "_COVERAGE_RCFILE"
  14. if sys.version_info >= (3, 4):
  15. OriginalProcess = multiprocessing.process.BaseProcess
  16. else:
  17. OriginalProcess = multiprocessing.Process
  18. original_bootstrap = OriginalProcess._bootstrap
  19. class ProcessWithCoverage(OriginalProcess):
  20. """A replacement for multiprocess.Process that starts coverage."""
  21. def _bootstrap(self):
  22. """Wrapper around _bootstrap to start coverage."""
  23. from coverage import Coverage # avoid circular import
  24. rcfile = os.environ[COVERAGE_RCFILE_ENV]
  25. cov = Coverage(data_suffix=True, config_file=rcfile)
  26. cov.start()
  27. try:
  28. return original_bootstrap(self)
  29. finally:
  30. cov.stop()
  31. cov.save()
  32. class Stowaway(object):
  33. """An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
  34. def __init__(self, rcfile):
  35. self.rcfile = rcfile
  36. def __getstate__(self):
  37. return {'rcfile': self.rcfile}
  38. def __setstate__(self, state):
  39. patch_multiprocessing(state['rcfile'])
  40. @contract(rcfile=str)
  41. def patch_multiprocessing(rcfile):
  42. """Monkey-patch the multiprocessing module.
  43. This enables coverage measurement of processes started by multiprocessing.
  44. This involves aggressive monkey-patching.
  45. `rcfile` is the path to the rcfile being used.
  46. """
  47. if hasattr(multiprocessing, PATCHED_MARKER):
  48. return
  49. if sys.version_info >= (3, 4):
  50. OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap
  51. else:
  52. multiprocessing.Process = ProcessWithCoverage
  53. # Set the value in ProcessWithCoverage that will be pickled into the child
  54. # process.
  55. os.environ[COVERAGE_RCFILE_ENV] = rcfile
  56. # When spawning processes rather than forking them, we have no state in the
  57. # new process. We sneak in there with a Stowaway: we stuff one of our own
  58. # objects into the data that gets pickled and sent to the sub-process. When
  59. # the Stowaway is unpickled, it's __setstate__ method is called, which
  60. # re-applies the monkey-patch.
  61. # Windows only spawns, so this is needed to keep Windows working.
  62. try:
  63. from multiprocessing import spawn
  64. original_get_preparation_data = spawn.get_preparation_data
  65. except (ImportError, AttributeError):
  66. pass
  67. else:
  68. def get_preparation_data_with_stowaway(name):
  69. """Get the original preparation data, and also insert our stowaway."""
  70. d = original_get_preparation_data(name)
  71. d['stowaway'] = Stowaway(rcfile)
  72. return d
  73. spawn.get_preparation_data = get_preparation_data_with_stowaway
  74. setattr(multiprocessing, PATCHED_MARKER, True)