Add a Notification Settings Button to all web notifications behind the web platform...
[chromium-blink-merge.git] / third_party / pycoverage / coverage / data.py
blobfb88c5b1e638cdd5954c3818a08d000a4ec499eb
1 """Coverage data for Coverage."""
3 import os
5 from coverage.backward import iitems, pickle, sorted # pylint: disable=W0622
6 from coverage.files import PathAliases
7 from coverage.misc import file_be_gone
10 class CoverageData(object):
11 """Manages collected coverage data, including file storage.
13 The data file format is a pickled dict, with these keys:
15 * collector: a string identifying the collecting software
17 * lines: a dict mapping filenames to sorted lists of line numbers
18 executed:
19 { 'file1': [17,23,45], 'file2': [1,2,3], ... }
21 * arcs: a dict mapping filenames to sorted lists of line number pairs:
22 { 'file1': [(17,23), (17,25), (25,26)], ... }
24 """
26 def __init__(self, basename=None, collector=None, debug=None):
27 """Create a CoverageData.
29 `basename` is the name of the file to use for storing data.
31 `collector` is a string describing the coverage measurement software.
33 `debug` is a `DebugControl` object for writing debug messages.
35 """
36 self.collector = collector or 'unknown'
37 self.debug = debug
39 self.use_file = True
41 # Construct the filename that will be used for data file storage, if we
42 # ever do any file storage.
43 self.filename = basename or ".coverage"
44 self.filename = os.path.abspath(self.filename)
46 # A map from canonical Python source file name to a dictionary in
47 # which there's an entry for each line number that has been
48 # executed:
50 # {
51 # 'filename1.py': { 12: None, 47: None, ... },
52 # ...
53 # }
55 self.lines = {}
57 # A map from canonical Python source file name to a dictionary with an
58 # entry for each pair of line numbers forming an arc:
60 # {
61 # 'filename1.py': { (12,14): None, (47,48): None, ... },
62 # ...
63 # }
65 self.arcs = {}
67 def usefile(self, use_file=True):
68 """Set whether or not to use a disk file for data."""
69 self.use_file = use_file
71 def read(self):
72 """Read coverage data from the coverage data file (if it exists)."""
73 if self.use_file:
74 self.lines, self.arcs = self._read_file(self.filename)
75 else:
76 self.lines, self.arcs = {}, {}
78 def write(self, suffix=None):
79 """Write the collected coverage data to a file.
81 `suffix` is a suffix to append to the base file name. This can be used
82 for multiple or parallel execution, so that many coverage data files
83 can exist simultaneously. A dot will be used to join the base name and
84 the suffix.
86 """
87 if self.use_file:
88 filename = self.filename
89 if suffix:
90 filename += "." + suffix
91 self.write_file(filename)
93 def erase(self):
94 """Erase the data, both in this object, and from its file storage."""
95 if self.use_file:
96 if self.filename:
97 file_be_gone(self.filename)
98 self.lines = {}
99 self.arcs = {}
101 def line_data(self):
102 """Return the map from filenames to lists of line numbers executed."""
103 return dict(
104 [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)]
107 def arc_data(self):
108 """Return the map from filenames to lists of line number pairs."""
109 return dict(
110 [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)]
113 def write_file(self, filename):
114 """Write the coverage data to `filename`."""
116 # Create the file data.
117 data = {}
119 data['lines'] = self.line_data()
120 arcs = self.arc_data()
121 if arcs:
122 data['arcs'] = arcs
124 if self.collector:
125 data['collector'] = self.collector
127 if self.debug and self.debug.should('dataio'):
128 self.debug.write("Writing data to %r" % (filename,))
130 # Write the pickle to the file.
131 fdata = open(filename, 'wb')
132 try:
133 pickle.dump(data, fdata, 2)
134 finally:
135 fdata.close()
137 def read_file(self, filename):
138 """Read the coverage data from `filename`."""
139 self.lines, self.arcs = self._read_file(filename)
141 def raw_data(self, filename):
142 """Return the raw pickled data from `filename`."""
143 if self.debug and self.debug.should('dataio'):
144 self.debug.write("Reading data from %r" % (filename,))
145 fdata = open(filename, 'rb')
146 try:
147 data = pickle.load(fdata)
148 finally:
149 fdata.close()
150 return data
152 def _read_file(self, filename):
153 """Return the stored coverage data from the given file.
155 Returns two values, suitable for assigning to `self.lines` and
156 `self.arcs`.
159 lines = {}
160 arcs = {}
161 try:
162 data = self.raw_data(filename)
163 if isinstance(data, dict):
164 # Unpack the 'lines' item.
165 lines = dict([
166 (f, dict.fromkeys(linenos, None))
167 for f, linenos in iitems(data.get('lines', {}))
169 # Unpack the 'arcs' item.
170 arcs = dict([
171 (f, dict.fromkeys(arcpairs, None))
172 for f, arcpairs in iitems(data.get('arcs', {}))
174 except Exception:
175 pass
176 return lines, arcs
178 def combine_parallel_data(self, aliases=None):
179 """Combine a number of data files together.
181 Treat `self.filename` as a file prefix, and combine the data from all
182 of the data files starting with that prefix plus a dot.
184 If `aliases` is provided, it's a `PathAliases` object that is used to
185 re-map paths to match the local machine's.
188 aliases = aliases or PathAliases()
189 data_dir, local = os.path.split(self.filename)
190 localdot = local + '.'
191 for f in os.listdir(data_dir or '.'):
192 if f.startswith(localdot):
193 full_path = os.path.join(data_dir, f)
194 new_lines, new_arcs = self._read_file(full_path)
195 for filename, file_data in iitems(new_lines):
196 filename = aliases.map(filename)
197 self.lines.setdefault(filename, {}).update(file_data)
198 for filename, file_data in iitems(new_arcs):
199 filename = aliases.map(filename)
200 self.arcs.setdefault(filename, {}).update(file_data)
201 if f != local:
202 os.remove(full_path)
204 def add_line_data(self, line_data):
205 """Add executed line data.
207 `line_data` is { filename: { lineno: None, ... }, ...}
210 for filename, linenos in iitems(line_data):
211 self.lines.setdefault(filename, {}).update(linenos)
213 def add_arc_data(self, arc_data):
214 """Add measured arc data.
216 `arc_data` is { filename: { (l1,l2): None, ... }, ...}
219 for filename, arcs in iitems(arc_data):
220 self.arcs.setdefault(filename, {}).update(arcs)
222 def touch_file(self, filename):
223 """Ensure that `filename` appears in the data, empty if needed."""
224 self.lines.setdefault(filename, {})
226 def measured_files(self):
227 """A list of all files that had been measured."""
228 return list(self.lines.keys())
230 def executed_lines(self, filename):
231 """A map containing all the line numbers executed in `filename`.
233 If `filename` hasn't been collected at all (because it wasn't executed)
234 then return an empty map.
237 return self.lines.get(filename) or {}
239 def executed_arcs(self, filename):
240 """A map containing all the arcs executed in `filename`."""
241 return self.arcs.get(filename) or {}
243 def add_to_hash(self, filename, hasher):
244 """Contribute `filename`'s data to the Md5Hash `hasher`."""
245 hasher.update(self.executed_lines(filename))
246 hasher.update(self.executed_arcs(filename))
248 def summary(self, fullpath=False):
249 """Return a dict summarizing the coverage data.
251 Keys are based on the filenames, and values are the number of executed
252 lines. If `fullpath` is true, then the keys are the full pathnames of
253 the files, otherwise they are the basenames of the files.
256 summ = {}
257 if fullpath:
258 filename_fn = lambda f: f
259 else:
260 filename_fn = os.path.basename
261 for filename, lines in iitems(self.lines):
262 summ[filename_fn(filename)] = len(lines)
263 return summ
265 def has_arcs(self):
266 """Does this data have arcs?"""
267 return bool(self.arcs)
270 if __name__ == '__main__':
271 # Ad-hoc: show the raw data in a data file.
272 import pprint, sys
273 covdata = CoverageData()
274 if sys.argv[1:]:
275 fname = sys.argv[1]
276 else:
277 fname = covdata.filename
278 pprint.pprint(covdata.raw_data(fname))