#   -*- coding: utf-8 -*-
#
#   This file is part of PyBuilder
#
#   Copyright 2011-2016 PyBuilder Team
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.


# 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 add multiprocessing support for coverage.py"""

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$CoverageConfig"

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

original_get_preparation_data = None
try:
    from multiprocessing import spawn

    original_get_preparation_data = spawn.get_preparation_data
except (ImportError, AttributeError):
    pass

original_bootstrap = OriginalProcess._bootstrap


class ProcessWithCoverage(OriginalProcess):
    """A replacement for multiprocess.Process that starts coverage."""

    def _bootstrap(self):
        """Wrapper around _bootstrap to start coverage."""
        from coverage import Coverage
        coverage_config = getattr(multiprocessing, PATCHED_MARKER)
        coverage_config.parallel = True
        cov = Coverage()
        cov.config = coverage_config
        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 __init__(self, coverage_config):
        self.coverage_config = coverage_config

    def __getstate__(self):
        return {'coverage_config': self.coverage_config}

    def __setstate__(self, state):
        patch_multiprocessing(state['coverage_config'])


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

    This enables coverage measurement of processes started by multiprocessing.
    This involves aggressive monkey-patching.

    `coverage_config` is the config of the coverage that initiated patching

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

    if sys.version_info >= (3, 4):
        OriginalProcess._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.
    if original_get_preparation_data:
        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(coverage_config)
            return d

        spawn.get_preparation_data = get_preparation_data_with_stowaway

    setattr(multiprocessing, PATCHED_MARKER, coverage_config)


def reverse_patch_multiprocessing():
    if sys.version_info >= (3, 4):
        OriginalProcess._bootstrap = original_bootstrap
    else:
        multiprocessing.Process = OriginalProcess

    if original_get_preparation_data:
        spawn.get_preparation_data = original_get_preparation_data

    delattr(multiprocessing, PATCHED_MARKER)
