blob: 3263cb38883b5d1a4f3e4f80c1fed72ed93b4f59 [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001"""Coverage data for Coverage."""
2
3import os
4
5from coverage.backward import pickle, sorted # pylint: disable=W0622
6
7
8class CoverageData(object):
9 """Manages collected coverage data, including file storage.
10
11 The data file format is a pickled dict, with these keys:
12
13 * collector: a string identifying the collecting software
14
15 * lines: a dict mapping filenames to sorted lists of line numbers
16 executed:
17 { 'file1': [17,23,45], 'file2': [1,2,3], ... }
18
19 * arcs: a dict mapping filenames to sorted lists of line number pairs:
20 { 'file1': [(17,23), (17,25), (25,26)], ... }
21
22 """
23
24 def __init__(self, basename=None, collector=None):
25 """Create a CoverageData.
26
27 `basename` is the name of the file to use for storing data.
28
29 `collector` is a string describing the coverage measurement software.
30
31 """
32 self.collector = collector or 'unknown'
33
34 self.use_file = True
35
36 # Construct the filename that will be used for data file storage, if we
37 # ever do any file storage.
38 self.filename = basename or ".coverage"
39 self.filename = os.path.abspath(self.filename)
40
41 # A map from canonical Python source file name to a dictionary in
42 # which there's an entry for each line number that has been
43 # executed:
44 #
45 # {
46 # 'filename1.py': { 12: None, 47: None, ... },
47 # ...
48 # }
49 #
50 self.lines = {}
51
52 # A map from canonical Python source file name to a dictionary with an
53 # entry for each pair of line numbers forming an arc:
54 #
55 # {
56 # 'filename1.py': { (12,14): None, (47,48): None, ... },
57 # ...
58 # }
59 #
60 self.arcs = {}
61
62 self.os = os
63 self.sorted = sorted
64 self.pickle = pickle
65
66 def usefile(self, use_file=True):
67 """Set whether or not to use a disk file for data."""
68 self.use_file = use_file
69
70 def read(self):
71 """Read coverage data from the coverage data file (if it exists)."""
72 if self.use_file:
73 self.lines, self.arcs = self._read_file(self.filename)
74 else:
75 self.lines, self.arcs = {}, {}
76
77 def write(self, suffix=None):
78 """Write the collected coverage data to a file.
79
80 `suffix` is a suffix to append to the base file name. This can be used
81 for multiple or parallel execution, so that many coverage data files
82 can exist simultaneously. A dot will be used to join the base name and
83 the suffix.
84
85 """
86 if self.use_file:
87 filename = self.filename
88 if suffix:
89 filename += "." + suffix
90 self.write_file(filename)
91
92 def erase(self):
93 """Erase the data, both in this object, and from its file storage."""
94 if self.use_file:
95 if self.filename and os.path.exists(self.filename):
96 os.remove(self.filename)
97 self.lines = {}
98 self.arcs = {}
99
100 def line_data(self):
101 """Return the map from filenames to lists of line numbers executed."""
102 return dict(
103 [(f, self.sorted(lmap.keys())) for f, lmap in self.lines.items()]
104 )
105
106 def arc_data(self):
107 """Return the map from filenames to lists of line number pairs."""
108 return dict(
109 [(f, self.sorted(amap.keys())) for f, amap in self.arcs.items()]
110 )
111
112 def write_file(self, filename):
113 """Write the coverage data to `filename`."""
114
115 # Create the file data.
116 data = {}
117
118 data['lines'] = self.line_data()
119 arcs = self.arc_data()
120 if arcs:
121 data['arcs'] = arcs
122
123 if self.collector:
124 data['collector'] = self.collector
125
126 # Write the pickle to the file.
127 fdata = open(filename, 'wb')
128 try:
129 self.pickle.dump(data, fdata, 2)
130 finally:
131 fdata.close()
132
133 def read_file(self, filename):
134 """Read the coverage data from `filename`."""
135 self.lines, self.arcs = self._read_file(filename)
136
137 def raw_data(self, filename):
138 """Return the raw pickled data from `filename`."""
139 fdata = open(filename, 'rb')
140 try:
141 data = pickle.load(fdata)
142 finally:
143 fdata.close()
144 return data
145
146 def _read_file(self, filename):
147 """Return the stored coverage data from the given file.
148
149 Returns two values, suitable for assigning to `self.lines` and
150 `self.arcs`.
151
152 """
153 lines = {}
154 arcs = {}
155 try:
156 data = self.raw_data(filename)
157 if isinstance(data, dict):
158 # Unpack the 'lines' item.
159 lines = dict([
160 (f, dict.fromkeys(linenos, None))
161 for f, linenos in data.get('lines', {}).items()
162 ])
163 # Unpack the 'arcs' item.
164 arcs = dict([
165 (f, dict.fromkeys(arcpairs, None))
166 for f, arcpairs in data.get('arcs', {}).items()
167 ])
168 except Exception:
169 pass
170 return lines, arcs
171
172 def combine_parallel_data(self):
173 """Combine a number of data files together.
174
175 Treat `self.filename` as a file prefix, and combine the data from all
176 of the data files starting with that prefix plus a dot.
177
178 """
179 data_dir, local = os.path.split(self.filename)
180 localdot = local + '.'
181 for f in os.listdir(data_dir or '.'):
182 if f.startswith(localdot):
183 full_path = os.path.join(data_dir, f)
184 new_lines, new_arcs = self._read_file(full_path)
185 for filename, file_data in new_lines.items():
186 self.lines.setdefault(filename, {}).update(file_data)
187 for filename, file_data in new_arcs.items():
188 self.arcs.setdefault(filename, {}).update(file_data)
189 if f != local:
190 os.remove(full_path)
191
192 def add_line_data(self, line_data):
193 """Add executed line data.
194
195 `line_data` is { filename: { lineno: None, ... }, ...}
196
197 """
198 for filename, linenos in line_data.items():
199 self.lines.setdefault(filename, {}).update(linenos)
200
201 def add_arc_data(self, arc_data):
202 """Add measured arc data.
203
204 `arc_data` is { filename: { (l1,l2): None, ... }, ...}
205
206 """
207 for filename, arcs in arc_data.items():
208 self.arcs.setdefault(filename, {}).update(arcs)
209
210 def touch_file(self, filename):
211 """Ensure that `filename` appears in the data, empty if needed."""
212 self.lines.setdefault(filename, {})
213
214 def measured_files(self):
215 """A list of all files that had been measured."""
216 return list(self.lines.keys())
217
218 def executed_lines(self, filename):
219 """A map containing all the line numbers executed in `filename`.
220
221 If `filename` hasn't been collected at all (because it wasn't executed)
222 then return an empty map.
223
224 """
225 return self.lines.get(filename) or {}
226
227 def executed_arcs(self, filename):
228 """A map containing all the arcs executed in `filename`."""
229 return self.arcs.get(filename) or {}
230
231 def add_to_hash(self, filename, hasher):
232 """Contribute `filename`'s data to the Md5Hash `hasher`."""
233 hasher.update(self.executed_lines(filename))
234 hasher.update(self.executed_arcs(filename))
235
236 def summary(self, fullpath=False):
237 """Return a dict summarizing the coverage data.
238
239 Keys are based on the filenames, and values are the number of executed
240 lines. If `fullpath` is true, then the keys are the full pathnames of
241 the files, otherwise they are the basenames of the files.
242
243 """
244 summ = {}
245 if fullpath:
246 filename_fn = lambda f: f
247 else:
248 filename_fn = self.os.path.basename
249 for filename, lines in self.lines.items():
250 summ[filename_fn(filename)] = len(lines)
251 return summ
252
253 def has_arcs(self):
254 """Does this data have arcs?"""
255 return bool(self.arcs)
256
257
258if __name__ == '__main__':
259 # Ad-hoc: show the raw data in a data file.
260 import pprint, sys
261 covdata = CoverageData()
262 if sys.argv[1:]:
263 fname = sys.argv[1]
264 else:
265 fname = covdata.filename
266 pprint.pprint(covdata.raw_data(fname))