Commits

Anonymous committed b50e6a7

Moved the coverage controllers out of pytest-cov to new package.

Comments (0)

Files changed (6)

+The MIT License
+
+Copyright (c) 2010 Meme Dough
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+include README.txt
+include LICENSE.txt
+include setup.py
+include cov_core.py
+include cov_core_init.py
+cov-core
+========
+
+This is a lib package for use by pytest-cov and nose-cov.  Unless your
+developing a coverage plugin for a test framework then you probably
+want one of those.
+"""Coverage controllers for use by pytest-cov and nose-cov."""
+
+import coverage
+import socket
+import sys
+import os
+
+try:
+    import configparser
+except ImportError:
+    import ConfigParser as configparser
+
+from cov_core_init import UNIQUE_SEP
+
+class CovController(object):
+    """Base class for different plugin implementations."""
+
+    def __init__(self, cov_source, cov_report, cov_config, config=None, nodeid=None):
+        """Get some common config used by multiple derived classes."""
+
+        self.cov_source = cov_source
+        self.cov_report = cov_report
+        self.cov_config = cov_config
+        self.config = config
+        self.nodeid = nodeid
+
+        self.cov = None
+        self.node_descs = set()
+        self.failed_slaves = []
+
+        # For data file name consider coverage rc file, coverage env
+        # vars in priority order.
+        parser = configparser.RawConfigParser()
+        parser.read(self.cov_config)
+        for default, section, item, env_var, option in [
+            ('.coverage', 'run', 'data_file', 'COVERAGE_FILE', 'cov_data_file')]:
+
+            # Lowest priority is coverage hard coded default.
+            result = default
+
+            # Override with coverage rc file.
+            if parser.has_option(section, item):
+                result = parser.get(section, item)
+
+            # Override with coverage env var.
+            if env_var:
+                result = os.environ.get(env_var, result)
+
+            # Set config option on ourselves.
+            setattr(self, option, result)
+
+    def set_env(self):
+        """Put info about coverage into the env so that subprocesses can activate coverage."""
+
+        os.environ['COV_CORE_SOURCE'] = UNIQUE_SEP.join(self.cov_source)
+        os.environ['COV_CORE_DATA_FILE'] = self.cov_data_file
+        os.environ['COV_CORE_CONFIG'] = self.cov_config
+
+    @staticmethod
+    def unset_env():
+        """Remove coverage info from env."""
+
+        del os.environ['COV_CORE_SOURCE']
+        del os.environ['COV_CORE_DATA_FILE']
+        del os.environ['COV_CORE_CONFIG']
+
+    @staticmethod
+    def get_node_desc(platform, version_info):
+        """Return a description of this node."""
+
+        return 'platform %s, python %s' % (platform, '%s.%s.%s-%s-%s' % version_info[:5])
+
+    @staticmethod
+    def sep(stream, s, txt):
+        if hasattr(stream, 'sep'):
+            stream.sep(s, txt)
+        else:
+            sep_total = max((70 - 2 - len(txt)), 2)
+            sep_len = sep_total / 2
+            sep_extra = sep_total % 2
+            out = '%s %s %s\n' % (s * sep_len, txt, s * (sep_len + sep_extra))
+            stream.write(out)
+
+    def summary(self, stream):
+        """Produce coverage reports."""
+
+        # Produce terminal report if wanted.
+        if 'term' in self.cov_report or 'term-missing' in self.cov_report:
+            if len(self.node_descs) == 1:
+                self.sep(stream, '-', 'coverage: %s' % ''.join(self.node_descs))
+            else:
+                self.sep(stream, '-', 'coverage')
+                for node_desc in sorted(self.node_descs):
+                    self.sep(stream, ' ', '%s' % node_desc)
+            show_missing = 'term-missing' in self.cov_report
+            self.cov.report(show_missing=show_missing, ignore_errors=True, file=stream)
+
+        # Produce annotated source code report if wanted.
+        if 'annotate' in self.cov_report:
+            self.cov.annotate(ignore_errors=True)
+
+        # Produce html report if wanted.
+        if 'html' in self.cov_report:
+            self.cov.html_report(ignore_errors=True)
+
+        # Produce xml report if wanted.
+        if 'xml' in self.cov_report:
+            self.cov.xml_report(ignore_errors=True)
+
+        # Report on any failed slaves.
+        if self.failed_slaves:
+            self.sep(stream, '-', 'coverage: failed slaves')
+            stream.write('The following slaves failed to return coverage data, '
+                         'ensure that pytest-cov is installed on these slaves.\n')
+            for node in self.failed_slaves:
+                stream.write('%s\n' % node.gateway.id)
+
+
+class Central(CovController):
+    """Implementation for centralised operation."""
+
+    def start(self):
+        """Erase any previous coverage data and start coverage."""
+
+        self.cov = coverage.coverage(source=self.cov_source,
+                                     data_file=self.cov_data_file,
+                                     config_file=self.cov_config)
+        self.cov.erase()
+        self.cov.start()
+        self.set_env()
+
+    def finish(self):
+        """Stop coverage, save data to file and set the list of coverage objects to report on."""
+
+        self.unset_env()
+        self.cov.stop()
+        self.cov.combine()
+        self.cov.save()
+        node_desc = self.get_node_desc(sys.platform, sys.version_info)
+        self.node_descs.add(node_desc)
+
+    def summary(self, stream):
+        """Produce coverage reports."""
+
+        CovController.summary(self, stream)
+
+
+class DistMaster(CovController):
+    """Implementation for distributed master."""
+
+    def start(self):
+        """Ensure coverage rc file rsynced if appropriate."""
+
+        if self.cov_config and os.path.exists(self.cov_config):
+            self.config.option.rsyncdir.append(self.cov_config)
+
+    def configure_node(self, node):
+        """Slaves need to know if they are collocated and what files have moved."""
+
+        node.slaveinput['cov_master_host'] = socket.gethostname()
+        node.slaveinput['cov_master_topdir'] = self.config.topdir
+        node.slaveinput['cov_master_rsync_roots'] = node.nodemanager.roots
+
+    def testnodedown(self, node, error):
+        """Collect data file name from slave.  Also save data to file if slave not collocated."""
+
+        # If slave doesn't return any data then it is likely that this
+        # plugin didn't get activated on the slave side.
+        if not (hasattr(node, 'slaveoutput') and 'cov_slave_node_id' in node.slaveoutput):
+            self.failed_slaves.append(node)
+            return
+
+        # If slave is not collocated then we must save the data file
+        # that it returns to us.
+        if 'cov_slave_lines' in node.slaveoutput:
+            cov = coverage.coverage(source=self.cov_source,
+                                    data_file=self.cov_data_file,
+                                    data_suffix=node.slaveoutput['cov_slave_node_id'],
+                                    config_file=self.cov_config)
+            cov.start()
+            cov.data.lines = node.slaveoutput['cov_slave_lines']
+            cov.data.arcs = node.slaveoutput['cov_slave_arcs']
+            cov.stop()
+            cov.save()
+
+        # Record the slave types that contribute to the data file.
+        rinfo = node.gateway._rinfo()
+        node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info)
+        self.node_descs.add(node_desc)
+
+    def finish(self):
+        """Combines coverage data and sets the list of coverage objects to report on."""
+
+        # Combine all the suffix files into the data file.
+        self.cov = coverage.coverage(source=self.cov_source,
+                                     data_file=self.cov_data_file,
+                                     config_file=self.cov_config)
+        self.cov.erase()
+        self.cov.combine()
+        self.cov.save()
+
+    def summary(self, stream):
+        """Produce coverage reports."""
+
+        CovController.summary(self, stream)
+
+
+class DistSlave(CovController):
+    """Implementation for distributed slaves."""
+
+    def start(self):
+        """Determine what data file and suffix to contribute to and start coverage."""
+
+        # Determine whether we are collocated with master.
+        self.is_collocated = bool(socket.gethostname() == self.config.slaveinput['cov_master_host'] and
+                                  self.config.topdir == self.config.slaveinput['cov_master_topdir'])
+
+        # If we are not collocated then rewrite master paths to slave paths.
+        if not self.is_collocated:
+            master_topdir = str(self.config.slaveinput['cov_master_topdir'])
+            slave_topdir = str(self.config.topdir)
+            self.cov_source = [source.replace(master_topdir, slave_topdir) for source in self.cov_source]
+            self.cov_data_file = self.cov_data_file.replace(master_topdir, slave_topdir)
+            self.cov_config = self.cov_config.replace(master_topdir, slave_topdir)
+
+        # Our slave node id makes us unique from all other slaves so
+        # adjust the data file that we contribute to and the master
+        # will combine our data with other slaves later.
+        self.cov_data_file += '.%s' % self.nodeid
+
+        # Erase any previous data and start coverage.
+        self.cov = coverage.coverage(source=self.cov_source,
+                                     data_file=self.cov_data_file,
+                                     config_file=self.cov_config)
+        self.cov.erase()
+        self.cov.start()
+        self.set_env()
+
+    def finish(self):
+        """Stop coverage and send relevant info back to the master."""
+
+        self.unset_env()
+        self.cov.stop()
+        self.cov.combine()
+        self.cov.save()
+
+        if self.is_collocated:
+            # If we are collocated then just inform the master of our
+            # data file to indicate that we have finished.
+            self.config.slaveoutput['cov_slave_node_id'] = self.nodeid
+        else:
+            # If we are not collocated then rewrite the filenames from
+            # the slave location to the master location.
+            slave_topdir = self.config.topdir
+            path_rewrites = [(str(slave_topdir.join(rsync_root.basename)), str(rsync_root))
+                             for rsync_root in self.config.slaveinput['cov_master_rsync_roots']]
+            path_rewrites.append((str(self.config.topdir), str(self.config.slaveinput['cov_master_topdir'])))
+
+            def rewrite_path(filename):
+                for slave_path, master_path in path_rewrites:
+                    filename = filename.replace(slave_path, master_path)
+                return filename
+
+            lines = dict((rewrite_path(filename), data) for filename, data in self.cov.data.lines.items())
+            arcs = dict((rewrite_path(filename), data) for filename, data in self.cov.data.arcs.items())
+
+            # Send all the data to the master over the channel.
+            self.config.slaveoutput['cov_slave_node_id'] = self.nodeid
+            self.config.slaveoutput['cov_slave_lines'] = lines
+            self.config.slaveoutput['cov_slave_arcs'] = arcs
+
+    def summary(self, stream):
+        """Only the master reports so do nothing."""
+
+        pass
+"""Activate coverage at python startup if appropriate.
+
+The python site initialisation will ensure that anything we import
+will be removed and not visible at the end of python startup.  However
+we minimise all work by putting these init actions in this separate
+module and only importing what is needed when needed.
+
+For normal python startup when coverage should not be activated we
+only import os, look for one env var and get out.
+
+For python startup when an ancestor process has set the env indicating
+that code coverage is being collected we activate coverage based on
+info passed via env vars.
+"""
+
+UNIQUE_SEP = '084031f3d2994d40a88c8b699b69e148'
+
+def init():
+
+    # Any errors encountered should only prevent coverage from
+    # starting, it should not cause python to complain that importing
+    # of site failed.
+    try:
+
+        # Only continue if ancestor process has set env.
+        import os
+        if os.environ.get('COV_CORE_SOURCE'):
+
+            # Only continue if we have all needed info from env.
+            cov_source = os.environ.get('COV_CORE_SOURCE').split(UNIQUE_SEP)
+            cov_data_file = os.environ.get('COV_CORE_DATA_FILE')
+            cov_config = os.environ.get('COV_CORE_CONFIG')
+            if cov_source and cov_data_file and cov_config:
+
+                # Import what we need to activate coverage.
+                import socket
+                import random
+                import coverage
+
+                # Produce a unique suffix for this process in the same
+                # manner as coverage.
+                data_suffix = '%s.%s.%s' % (socket.gethostname(),
+                                            os.getpid(),
+                                            random.randint(0, 999999))
+
+                # Activate coverage for this process.
+                cov = coverage.coverage(source=cov_source,
+                                        data_file=cov_data_file,
+                                        data_suffix=data_suffix,
+                                        config_file=cov_config,
+                                        auto_data=True)
+                cov.erase()
+                cov.start()
+
+    except Exception:
+        pass
+import setuptools
+import sys
+import os
+
+# The name of the path file must appear after easy-install.pth so that
+# cov_core has been added to the sys.path and cov_core_init can be
+# imported.
+PTH_FILE_NAME = 'init_cov_core.pth'
+
+PTH_FILE = '''\
+import cov_core_init; cov_core_init.init()
+'''
+
+UNKNOWN_SITE_PACKAGES_DIR ='''\
+Failed to find site-packages or dist-packages dir to put pth file in.
+Sub processes will not have coverage collected.
+
+To measure sub processes put the following in a file called %s:
+%s
+''' % (PTH_FILE_NAME, PTH_FILE)
+
+setuptools.setup(name='cov-core',
+                 version='1.0a2',
+                 description='plugin core for use by pytest-cov and nose-cov',
+                 long_description=open('README.txt').read().strip(),
+                 author='Meme Dough',
+                 author_email='memedough@gmail.com',
+                 url='http://bitbucket.org/memedough/cov-core/overview',
+                 py_modules=['cov_core',
+                             'cov_core_init'],
+                 install_requires=['coverage>=3.4a1'],
+                 license='MIT License',
+                 zip_safe=False,
+                 keywords='cover coverage',
+                 classifiers=['Development Status :: 4 - Beta',
+                              'Intended Audience :: Developers',
+                              'License :: OSI Approved :: MIT License',
+                              'Operating System :: OS Independent',
+                              'Programming Language :: Python',
+                              'Programming Language :: Python :: 2.4',
+                              'Programming Language :: Python :: 2.5',
+                              'Programming Language :: Python :: 2.6',
+                              'Programming Language :: Python :: 2.7',
+                              'Programming Language :: Python :: 3.0',
+                              'Programming Language :: Python :: 3.1',
+                              'Topic :: Software Development :: Testing'])
+
+if sys.argv[1] in ('install', 'develop'):
+    for path in sys.path:
+        if 'site-packages' in path or 'dist-packages' in path:
+            path = os.path.dirname(path)
+            pth_file = open(os.path.join(path, PTH_FILE_NAME), 'w')
+            pth_file.write(PTH_FILE)
+            pth_file.close()
+            break
+    else:
+        sys.stdout.write(UNKNOWN_SITE_PACKAGES_DIR)
+        sys.stdout.write(PTH_FILE)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.