1 """Coverage data for Coverage."""
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
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)], ... }
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.
36 self
.collector
= collector
or 'unknown'
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
51 # 'filename1.py': { 12: None, 47: None, ... },
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:
61 # 'filename1.py': { (12,14): None, (47,48): None, ... },
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
72 """Read coverage data from the coverage data file (if it exists)."""
74 self
.lines
, self
.arcs
= self
._read
_file
(self
.filename
)
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
88 filename
= self
.filename
90 filename
+= "." + suffix
91 self
.write_file(filename
)
94 """Erase the data, both in this object, and from its file storage."""
97 file_be_gone(self
.filename
)
102 """Return the map from filenames to lists of line numbers executed."""
104 [(f
, sorted(lmap
.keys())) for f
, lmap
in iitems(self
.lines
)]
108 """Return the map from filenames to lists of line number pairs."""
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.
119 data
['lines'] = self
.line_data()
120 arcs
= self
.arc_data()
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')
133 pickle
.dump(data
, fdata
, 2)
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')
147 data
= pickle
.load(fdata
)
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
162 data
= self
.raw_data(filename
)
163 if isinstance(data
, dict):
164 # Unpack the 'lines' item.
166 (f
, dict.fromkeys(linenos
, None))
167 for f
, linenos
in iitems(data
.get('lines', {}))
169 # Unpack the 'arcs' item.
171 (f
, dict.fromkeys(arcpairs
, None))
172 for f
, arcpairs
in iitems(data
.get('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
)
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.
258 filename_fn
= lambda f
: f
260 filename_fn
= os
.path
.basename
261 for filename
, lines
in iitems(self
.lines
):
262 summ
[filename_fn(filename
)] = len(lines
)
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.
273 covdata
= CoverageData()
277 fname
= covdata
.filename
278 pprint
.pprint(covdata
.raw_data(fname
))