Anonymous avatar Anonymous committed d064e21

Added support to collect coverage of sub processes.

Comments (0)

Files changed (4)

 include LICENSE.txt
 include setup.py
 include pytest_cov.py
+include pytest_cov_init.py
 include test_pytest_cov.py
 except ImportError:
     import ConfigParser as configparser
 
+from pytest_cov_init import UNIQUE_SEP
+
+
 def pytest_addoption(parser):
     """Add options to control coverage."""
 
     def __init__(self, config):
         """Creates a coverage pytest plugin.
 
-        We read the rc file that coverage uses so that everything in
-        the rc file will be honoured.  Specifically we tell coverage
-        through it's API the data file name, html dir name and xml
-        file name.  So we need to know what these are in the rc file
-        or env vars.
-
-        Doing this ensures that users can rely on the coverage rc file
-        and env vars to work the same under this plugin as they do
-        under coverage itself.
+        We read the rc file that coverage uses to get the data file
+        name.  This is needed since we give coverage through it's API
+        the data file name.
         """
 
         # Our implementation is unknown at this time.
         self.cov_controller = None
 
-        # For data file, html dir and xml file consider coverage rc
-        # file, coverage env vars and our own options in priority
-        # order.
+        # For data file name consider coverage rc file, coverage env
+        # vars in priority order.
         parser = configparser.RawConfigParser()
         parser.read(config.getvalue('cov_config'))
         for default, section, item, env_var, option in [
         self.config = config
         self.cov_source = self.config.getvalue('cov_source')
         self.cov_data_file = self.config.getvalue('cov_data_file')
-        self.cov_config = os.path.realpath(self.config.getvalue('cov_config'))
+        self.cov_config = self.config.getvalue('cov_config')
+
+    def set_env(self):
+        """Put info about coverage into the env so that subprocesses can activate coverage."""
+
+        os.environ['PYTEST_COV_SOURCE'] = UNIQUE_SEP.join(self.cov_source)
+        os.environ['PYTEST_COV_DATA_FILE'] = self.cov_data_file
+        os.environ['PYTEST_COV_CONFIG'] = self.cov_config
+
+    @staticmethod
+    def unset_env():
+        """Remove coverage info from env."""
+
+        del os.environ['PYTEST_COV_SOURCE']
+        del os.environ['PYTEST_COV_DATA_FILE']
+        del os.environ['PYTEST_COV_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])
 
     def terminal_summary(self, terminalreporter):
         """Produce coverage reports."""
 
         # Produce annotated source code report if wanted.
         if 'annotate' in cov_report:
-            cov.annotate(ignore_errors=True)
+            self.cov.annotate(ignore_errors=True)
 
         # Produce html report if wanted.
         if 'html' in cov_report:
-            cov.html_report(ignore_errors=True)
+            self.cov.html_report(ignore_errors=True)
 
         # Produce xml report if wanted.
         if 'xml' in cov_report:
-            cov.xml_report(ignore_errors=True)
+            self.cov.xml_report(ignore_errors=True)
 
         # Report on any failed slaves.
         if self.failed_slaves:
                                      config_file=self.cov_config)
         self.cov.erase()
         self.cov.start()
+        self.set_env()
 
     def sessionfinish(self, session, exitstatus):
         """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 = get_node_desc(sys.platform, sys.version_info)
+        node_desc = self.get_node_desc(sys.platform, sys.version_info)
         self.node_descs.add(node_desc)
 
     def terminal_summary(self, terminalreporter):
 
         # 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_data_suffix' in node.slaveoutput):
+        if not (hasattr(node, 'slaveoutput') and 'cov_slave_node_id' in node.slaveoutput):
             self.failed_slaves.append(node)
             return
 
         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_data_suffix'],
+                                    data_suffix=node.slaveoutput['cov_slave_node_id'],
                                     config_file=self.cov_config)
             cov.start()
             cov.data.lines = node.slaveoutput['cov_slave_lines']
 
         # Record the slave types that contribute to the data file.
         rinfo = node.gateway._rinfo()
-        node_desc = get_node_desc(rinfo.platform, rinfo.version_info)
+        node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info)
         self.node_descs.add(node_desc)
 
     def sessionfinish(self, session, exitstatus):
         self.is_collocated = bool(socket.gethostname() == session.config.slaveinput['cov_master_host'] and
                                   session.config.topdir == session.config.slaveinput['cov_master_topdir'])
 
-        # Our suffix makes us unique from all other slaves, master
-        # will combine our data later.
-        self.data_suffix = session.nodeid
+        # If we are not collocated then rewrite master paths to slave paths.
+        if not self.is_collocated:
+            master_topdir = str(session.config.slaveinput['cov_master_topdir'])
+            slave_topdir = str(session.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' % session.nodeid
 
         # Erase any previous data and start coverage.
         self.cov = coverage.coverage(source=self.cov_source,
                                      data_file=self.cov_data_file,
-                                     data_suffix=self.data_suffix,
                                      config_file=self.cov_config)
         self.cov.erase()
         self.cov.start()
+        self.set_env()
 
     def sessionfinish(self, session, exitstatus):
         """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 save the file ourselves and
-            # inform the master of our suffix to indicate that we have
-            # finished.
-            self.cov.save()
-            session.config.slaveoutput['cov_slave_data_suffix'] = self.data_suffix
+            # If we are collocated then just inform the master of our
+            # data file to indicate that we have finished.
+            session.config.slaveoutput['cov_slave_node_id'] = session.nodeid
         else:
             # If we are not collocated then rewrite the filenames from
             # the slave location to the master location.
             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.
-            session.config.slaveoutput['cov_slave_data_suffix'] = self.data_suffix
+            session.config.slaveoutput['cov_slave_node_id'] = session.nodeid
             session.config.slaveoutput['cov_slave_lines'] = lines
             session.config.slaveoutput['cov_slave_arcs'] = arcs
 
         """Only the master reports so do nothing."""
 
         pass
-
-
-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])
 import setuptools
+import sys
+import os
+
+PTH_FILE = '''\
+import pytest_cov_init; pytest_cov_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 pytest_cov.pth:
+'''
 
 setuptools.setup(name='pytest-cov',
-                 version='0.15',
+                 version='1.0a1',
                  description='py.test plugin for coverage reporting with support for both centralised and distributed testing',
                  long_description=open('README.txt').read().strip(),
                  author='Meme Dough',
                  author_email='memedough@gmail.com',
                  url='http://bitbucket.org/memedough/pytest-cov/overview',
-                 py_modules=['pytest_cov'],
-                 install_requires=['py>=1.3.0',
-                                   'pytest-xdist>=1.2',
+                 py_modules=['pytest_cov',
+                             'pytest_cov_init'],
+                 install_requires=['py>=1.3.2',
+                                   'pytest-xdist>=1.4',
                                    'coverage>=3.3.1'],
                  entry_points={'pytest11': ['pytest_cov = pytest_cov']},
                  license='MIT License',
                               '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, 'pytest_cov.pth'), 'w')
+            pth_file.write(PTH_FILE)
+            pth_file.close()
+            break
+    else:
+        sys.stdout.write(UNKNOWN_SITE_PACKAGES_DIR)
+        sys.stdout.write(PTH_FILE)

test_pytest_cov.py

 
 - For py 3.0 coverage seems to give incorrect results, it reports all
   covered except the one line which it should have actualy covered.
+  Issue reported upstream, also only problem with pass statement and
+  is find with simple assignment statement.
 """
 
 import py
 import sys
+import os
 
 pytest_plugins = 'pytester', 'cov'
 
 def test_foo():
     version = sys.version_info[:2]
     if version == (2, 4):
-        pass
+        a = True
     if version == (2, 5):
-        pass
+        a = True
     if version == (2, 6):
-        pass
+        a = True
     if version == (2, 7):
-        pass
+        a = True
     if version == (3, 0):
-        pass
+        a = True
     if version == (3, 1):
-        pass
+        a = True
 '''
 
-SCRIPT_CMATH = '''
-import cmath
+SCRIPT_CHILD = '''
+import sys
 
-def test_foo():
+idx = int(sys.argv[1])
+
+if idx == 0:
+    pass
+if idx == 1:
     pass
 '''
 
-@py.test.mark.xfail('sys.version_info[:2] == (3, 0)')
+SCRIPT_PARENT = '''
+import subprocess
+import sys
+
+def pytest_generate_tests(metafunc):
+    for i in range(2):
+        metafunc.addcall(funcargs=dict(idx=i))
+
+def test_foo(idx):
+    out, err = subprocess.Popen([sys.executable, 'child_script.py', str(idx)], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+'''
+
+
 def test_central(testdir):
     script = testdir.makepyfile(SCRIPT)
+
     result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename)
+                               '--cov=%s' % script.dirpath(),
+                               '--cov-report=term-missing')
+
     result.stdout.fnmatch_lines([
             '*- coverage: platform *, python * -*',
-            'test_central * 18 * 5 * 72%',
+            'test_central * 18 * 5 * 72%*',
             '*10 passed*'
             ])
     assert result.ret == 0
 
-def test_module_selection(testdir):
-    script = testdir.makepyfile(SCRIPT_CMATH)
+
+def test_dist_collocated(testdir):
+    script = testdir.makepyfile(SCRIPT)
+
     result = testdir.runpytest(script,
-                               '--cov=*cmath*',
-                               '--cov=%s' % script.purebasename)
+                               '--cov=%s' % script.dirpath(),
+                               '--cov-report=term-missing',
+                               '--dist=load',
+                               '--tx=2*popen')
+
     result.stdout.fnmatch_lines([
             '*- coverage: platform *, python * -*',
-            'test_module_selection * 3 * 0 * 100%',
-            '*1 passed*'
-            ])
-    assert result.ret == 0
-    matching_lines = [line for line in result.outlines if 'TokenError' in line]
-    assert not matching_lines
-
-@py.test.mark.xfail('sys.version_info[:2] == (3, 0)')
-def test_dist_load_collocated(testdir):
-    script = testdir.makepyfile(SCRIPT)
-    result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename,
-                               '--dist=load',
-                               '--tx=2*popen')
-    result.stdout.fnmatch_lines([
-            '*- coverage: platform *, python * -*',
-            'test_dist_load_collocated * 18 * 5 * 72%',
+            'test_dist_collocated * 18 * 5 * 72%*',
             '*10 passed*'
             ])
     assert result.ret == 0
 
-@py.test.mark.xfail('sys.version_info[:2] == (3, 0)')
-def test_dist_load_not_collocated(testdir):
+
+def test_dist_not_collocated(testdir):
     script = testdir.makepyfile(SCRIPT)
     dir1 = testdir.mkdir('dir1')
     dir2 = testdir.mkdir('dir2')
+
     result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename,
+                               '--cov=%s' % script.dirpath(),
+                               '--cov-report=term-missing',
                                '--dist=load',
                                '--tx=popen//chdir=%s' % dir1,
                                '--tx=popen//chdir=%s' % dir2,
                                '--rsyncdir=%s' % script.basename)
+
     result.stdout.fnmatch_lines([
             '*- coverage: platform *, python * -*',
-            'test_dist_load_not_collocated * 18 * 5 * 72%',
+            'test_dist_not_collocated * 18 * 5 * 72%*',
             '*10 passed*'
             ])
     assert result.ret == 0
 
-@py.test.mark.skipif('sys.version_info[:2] >= (3, 0)')
-def test_dist_each_one_report_py2(testdir):
-    script = testdir.makepyfile(SCRIPT)
-    result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename,
-                               '--dist=each',
-                               '--tx=popen//python=/usr/local/python246/bin/python',
-                               '--tx=popen//python=/usr/local/python255/bin/python',
-                               '--tx=popen//python=/usr/local/python265/bin/python',
-                               '--tx=popen//python=/usr/local/python27b1/bin/python')
+
+def test_central_subprocess(testdir):
+    scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD)
+    parent_script = scripts.dirpath().join('parent_script.py')
+
+    result = testdir.runpytest(parent_script,
+                               '--cov=%s' % scripts.dirpath(),
+                               '--cov-report=term-missing')
+
     result.stdout.fnmatch_lines([
-            '*- coverage -*',
-            '* platform *, python 2.4.6-final-0 *',
-            '* platform *, python 2.5.5-final-0 *',
-            '* platform *, python 2.6.5-final-0 *',
-            '* platform *, python 2.7.0-beta-1 *',
-            'test_dist_each_one_report_py2 * 18 * 16 * 88%',
-            '*40 passed*'
+            '*- coverage: platform *, python * -*',
+            'child_script * 6 * 0 * 100%*',
+            'parent_script * 7 * 0 * 100%*',
             ])
     assert result.ret == 0
 
-@py.test.mark.skipif('sys.version_info[:2] < (3, 0)')
-@py.test.mark.xfail('sys.version_info[:2] == (3, 1)')
-def test_dist_each_one_report_py3(testdir):
-    script = testdir.makepyfile(SCRIPT)
-    result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename,
-                               '--dist=each',
-                               '--tx=popen//python=/usr/local/python301/bin/python3.0',
-                               '--tx=popen//python=/usr/local/python312/bin/python3.1')
+
+def test_dist_subprocess_collocated(testdir):
+    scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD)
+    parent_script = scripts.dirpath().join('parent_script.py')
+
+    result = testdir.runpytest(parent_script,
+                               '--cov=%s' % scripts.dirpath(),
+                               '--cov-report=term-missing',
+                               '--dist=load',
+                               '--tx=2*popen')
+
     result.stdout.fnmatch_lines([
-            # coverage under python 3.0 seems to produce incorrect
-            # results but ignore for this test as we want to see
-            # multiple reports regardless of results.
-            '*- coverage -*',
-            '* platform *, python 3.0.1-final-0 *',
-            '* platform *, python 3.1.2-final-0 *',
-            'test_dist_each_one_report_py3 * 18 * 17 * 94%',
-            '*20 passed*'
+            '*- coverage: platform *, python * -*',
+            'child_script * 6 * 0 * 100%*',
+            'parent_script * 7 * 0 * 100%*',
             ])
     assert result.ret == 0
 
+
+def test_dist_subprocess_not_collocated(testdir, tmpdir):
+    scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD)
+    parent_script = scripts.dirpath().join('parent_script.py')
+    child_script = scripts.dirpath().join('child_script.py')
+
+    dir1 = tmpdir.mkdir('dir1')
+    dir2 = tmpdir.mkdir('dir2')
+
+    result = testdir.runpytest(parent_script,
+                               '--cov=%s' % scripts.dirpath(),
+                               '--cov-report=term-missing',
+                               '--dist=load',
+                               '--tx=popen//chdir=%s' % dir1,
+                               '--tx=popen//chdir=%s' % dir2,
+                               '--rsyncdir=%s' % child_script,
+                               '--rsyncdir=%s' % parent_script)
+
+    result.stdout.fnmatch_lines([
+            '*- coverage: platform *, python * -*',
+            'child_script * 6 * 0 * 100%*',
+            'parent_script * 7 * 0 * 100%*',
+            ])
+    assert result.ret == 0
+
+
 @py.test.mark.skipif('sys.version_info[:2] >= (3, 0)')
 def test_dist_missing_data(testdir):
+    if not os.path.exists('/usr/local/python255-empty/bin/python'):
+        py.test.skip('this test needs python without pytest-cov installed in /usr/local/python255-empty/bin/python')
+
     script = testdir.makepyfile(SCRIPT)
+
     result = testdir.runpytest(script,
-                               '--cov=%s' % script.purebasename,
+                               '--cov=%s' % script.dirpath(),
+                               '--cov-report=term-missing',
                                '--dist=load',
-                               '--tx=popen//python=/usr/local/env255empty/bin/python')
+                               '--tx=popen//python=/usr/local/python255-empty/bin/python')
+
     result.stdout.fnmatch_lines([
             '*- coverage: failed slaves -*'
             ])
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.