[PATCH 2 of 3 coverage] run-tests: collect aggregate code coverage

Gregory Szorc gregory.szorc at gmail.com
Sat Mar 28 02:48:45 CDT 2015


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1427528878 25200
#      Sat Mar 28 00:47:58 2015 -0700
# Node ID 6b3bac1eba5e9470690da017224fdcfb80609d45
# Parent  1a91ee1edd9b042b2083880f608dece8a170a82b
run-tests: collect aggregate code coverage

Before this patch, every Python process during a code coverage run was
writing coverage data to the same file. I'm not sure if the coverage
package even tries to obtain a lock on the file. But what I do know is
there was some last write wins leading to loss of code coverage data, at
least with -j > 1.

This patch changes the code coverage mechanism to be multiple process
safe. The mechanism for initializing code coverage via sitecustomize.py
has been tweaked so each Python process will produce a separate coverage
data file on disk. Unless two processes generate the same random value,
there are no race conditions writing to the same file. At the end of the
test run, we combine all written files into an aggregate report.

On my machine, running the full test suite produces a little over
20,000 coverage files consuming ~350 MB. As you can imagine, it takes
several seconds to load and merge these coverage files. But when it is
done, you have an accurate picture of the aggregate code coverage for the
entire test suite, which is ~60% line coverage.

diff --git a/tests/run-tests.py b/tests/run-tests.py
--- a/tests/run-tests.py
+++ b/tests/run-tests.py
@@ -1946,10 +1946,16 @@ class TestRunner(object):
             shutil.copyfile(custom, target)
             rc = os.path.join(self._testdir, '.coveragerc')
             vlog('# Installing coverage rc to %s' % rc)
             os.environ['COVERAGE_PROCESS_START'] = rc
-            fn = os.path.join(self._installdir, '..', '.coverage')
-            os.environ['COVERAGE_FILE'] = fn
+            covdir = os.path.join(self._installdir, '..', 'coverage')
+            try:
+                os.mkdir(covdir)
+            except OSError, e:
+                if e.errno != errno.EEXIST:
+                    raise
+
+            os.environ['COVERAGE_DIR'] = covdir
 
     def _checkhglib(self, verb):
         """Ensure that the 'mercurial' package imported by python is
         the one we expect it to be.  If not, print a warning to stderr."""
@@ -1986,11 +1992,11 @@ class TestRunner(object):
         vlog('# Producing coverage report')
         # chdir is the easiest way to get short, relative paths in the
         # output.
         os.chdir(self._pythondir)
-        covdir = os.path.join(self._installdir, '..')
-        cov = coverage(data_file=os.path.join(covdir, '.coverage'))
-        cov.load()
+        covdir = os.path.join(self._installdir, '..', 'coverage')
+        cov = coverage(data_file=os.path.join(covdir, 'cov'))
+        cov.combine()
 
         omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
         cov.report(ignore_errors=True, omit=omit)
 
diff --git a/tests/sitecustomize.py b/tests/sitecustomize.py
--- a/tests/sitecustomize.py
+++ b/tests/sitecustomize.py
@@ -1,5 +1,16 @@
-try:
-    import coverage
-    getattr(coverage, 'process_startup', lambda: None)()
-except ImportError:
-    pass
+import os
+
+if os.environ.get('COVERAGE_PROCESS_START'):
+    try:
+        import coverage
+        import random
+
+        # uuid is better, but not available in Python 2.4.
+        covpath = os.path.join(os.environ['COVERAGE_DIR'],
+                               'cov.%s' % random.randrange(0, 1000000000000))
+        cov = coverage.coverage(data_file=covpath, auto_data=True)
+        cov._warn_no_data = False
+        cov._warn_unimported_source = False
+        cov.start()
+    except ImportError:
+        pass


More information about the Mercurial-devel mailing list