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