summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2011-08-21 14:02:20 -0400
committerNed Batchelder <ned@nedbatchelder.com>2011-08-21 14:02:20 -0400
commitba69be6407e27201e7c6b54bc9eaa53e1d97bb5b (patch)
treeff0725a879dc604fe65de5eed1bc3605e172acdf /coverage
parent1a2ac8c63c4d45a1e3c4864b1170721022faf627 (diff)
downloadpython-coveragepy-git-ba69be6407e27201e7c6b54bc9eaa53e1d97bb5b.tar.gz
The machinery to map paths through aliases for merging coverage data from disparate machines. Part of fixing #17.
Diffstat (limited to 'coverage')
-rw-r--r--coverage/data.py12
-rw-r--r--coverage/files.py78
2 files changed, 87 insertions, 3 deletions
diff --git a/coverage/data.py b/coverage/data.py
index 3263cb38..1cc4c950 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -1,8 +1,10 @@
"""Coverage data for Coverage."""
-import os
+import fnmatch, os, re
from coverage.backward import pickle, sorted # pylint: disable=W0622
+from coverage.files import PathAliases
+from coverage.misc import CoverageException
class CoverageData(object):
@@ -169,13 +171,17 @@ class CoverageData(object):
pass
return lines, arcs
- def combine_parallel_data(self):
+ def combine_parallel_data(self, aliases=None):
"""Combine a number of data files together.
Treat `self.filename` as a file prefix, and combine the data from all
of the data files starting with that prefix plus a dot.
+ If `aliases` is provided, it's a PathAliases object that is used to
+ re-map paths to match the local machine's.
+
"""
+ aliases = aliases or PathAliases()
data_dir, local = os.path.split(self.filename)
localdot = local + '.'
for f in os.listdir(data_dir or '.'):
@@ -183,8 +189,10 @@ class CoverageData(object):
full_path = os.path.join(data_dir, f)
new_lines, new_arcs = self._read_file(full_path)
for filename, file_data in new_lines.items():
+ filename = aliases.map(filename)
self.lines.setdefault(filename, {}).update(file_data)
for filename, file_data in new_arcs.items():
+ filename = aliases.map(filename)
self.arcs.setdefault(filename, {}).update(file_data)
if f != local:
os.remove(full_path)
diff --git a/coverage/files.py b/coverage/files.py
index a68a0a7f..f1046ed1 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -1,7 +1,8 @@
"""File wrangling."""
from coverage.backward import to_string
-import fnmatch, os, sys
+from coverage.misc import CoverageException
+import fnmatch, os, re, sys
class FileLocator(object):
"""Understand how filenames work."""
@@ -118,6 +119,81 @@ class FnmatchMatcher(object):
return False
+class PathAliases(object):
+ """A collection of aliases for paths.
+
+ When combining data files from remote machines, often the paths to source
+ code are different, for example, due to OS differences, or because of
+ serialized checkouts on continuous integration machines.
+
+ A `PathAliases` object tracks a list of pattern/result pairs, and can
+ map a path through those aliases to produce a unified path.
+
+ """
+ def __init__(self):
+ self.aliases = []
+
+ def _sep(self, s):
+ """Find the path separator used in this string, or os.sep if none."""
+ sep_match = re.search(r"[\\/]", s)
+ if sep_match:
+ sep = sep_match.group(0)
+ else:
+ sep = os.sep
+ return sep
+
+ def add(self, pattern, result):
+ """Add the `pattern`/`result` pair to the list of aliases.
+
+ `pattern` is an `fnmatch`-style pattern. `result` is a simple
+ string. When mapping paths, if a path starts with a match against
+ `pattern`, then that match is replaced with `result`. This models
+ isomorphic source trees being rooted at different places on two
+ different machines.
+
+ `pattern` can't end with a wildcard component, since that would
+ match an entire tree, and not just its root.
+
+ """
+ # The pattern can't end with a wildcard component.
+ pattern = pattern.rstrip(r"\/")
+ if pattern.endswith("*"):
+ raise CoverageException("Pattern must not end with wildcards.")
+ pattern_sep = self._sep(pattern)
+ pattern += pattern_sep
+
+ # Make a regex from the pattern. fnmatch always adds a \Z to match
+ # the whole string, which we don't want.
+ regex_pat = fnmatch.translate(pattern).replace(r'\Z', '')
+ regex = re.compile("(?i)" + regex_pat)
+
+ # Normalize the result: it must end with a path separator.
+ result_sep = self._sep(result)
+ result = result.rstrip(r"\/") + result_sep
+ self.aliases.append((regex, result, pattern_sep, result_sep))
+
+ def map(self, path):
+ """Map `path` through the aliases.
+
+ `path` is checked against all of the patterns. The first pattern to
+ match is used to replace the root of the path with the result root.
+ Only one pattern is ever used. If no patterns match, `path` is
+ returned unchanged.
+
+ The separator style in the result is made to match that of the result
+ in the alias.
+
+ """
+ for regex, result, pattern_sep, result_sep in self.aliases:
+ m = regex.match(path)
+ if m:
+ new = path.replace(m.group(0), result)
+ if pattern_sep != result_sep:
+ new = new.replace(pattern_sep, result_sep)
+ return new
+ return path
+
+
def find_python_files(dirname):
"""Yield all of the importable Python files in `dirname`, recursively."""
for dirpath, dirnames, filenames in os.walk(dirname, topdown=True):