diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2011-08-21 14:02:20 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2011-08-21 14:02:20 -0400 |
commit | ba69be6407e27201e7c6b54bc9eaa53e1d97bb5b (patch) | |
tree | ff0725a879dc604fe65de5eed1bc3605e172acdf /coverage | |
parent | 1a2ac8c63c4d45a1e3c4864b1170721022faf627 (diff) | |
download | python-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.py | 12 | ||||
-rw-r--r-- | coverage/files.py | 78 |
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): |