|
1 """Coverage data for Coverage.""" |
|
2 |
|
3 import os |
|
4 import cPickle as pickle |
|
5 |
|
6 from backward import sorted # pylint: disable-msg=W0622 |
|
7 |
|
8 |
|
9 class CoverageData: |
|
10 """Manages collected coverage data, including file storage. |
|
11 |
|
12 The data file format is a pickled dict, with these keys: |
|
13 |
|
14 * collector: a string identifying the collecting software |
|
15 |
|
16 * lines: a dict mapping filenames to sorted lists of line numbers |
|
17 executed: |
|
18 { 'file1': [17,23,45], 'file2': [1,2,3], ... } |
|
19 |
|
20 """ |
|
21 |
|
22 # Name of the data file (unless environment variable is set). |
|
23 filename_default = ".coverage" |
|
24 |
|
25 # Environment variable naming the data file. |
|
26 filename_env = "COVERAGE_FILE" |
|
27 |
|
28 def __init__(self, basename=None, suffix=None, collector=None): |
|
29 """Create a CoverageData. |
|
30 |
|
31 `basename` is the name of the file to use for storing data. |
|
32 |
|
33 `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 |
|
35 can exist simultaneously. |
|
36 |
|
37 `collector` is a string describing the coverage measurement software. |
|
38 |
|
39 """ |
|
40 self.basename = basename |
|
41 self.collector = collector |
|
42 self.suffix = suffix |
|
43 |
|
44 self.use_file = True |
|
45 self.filename = None |
|
46 |
|
47 # 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 |
|
49 # executed: |
|
50 # |
|
51 # { |
|
52 # 'filename1.py': { 12: True, 47: True, ... }, |
|
53 # ... |
|
54 # } |
|
55 # |
|
56 self.lines = {} |
|
57 |
|
58 def usefile(self, use_file=True): |
|
59 """Set whether or not to use a disk file for data.""" |
|
60 self.use_file = use_file |
|
61 |
|
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): |
|
73 """Read coverage data from the coverage data file (if it exists).""" |
|
74 data = {} |
|
75 if self.use_file: |
|
76 self._make_filename() |
|
77 data = self._read_file(self.filename) |
|
78 self.lines = data |
|
79 |
|
80 def write(self): |
|
81 """Write the collected coverage data to a file.""" |
|
82 if self.use_file: |
|
83 self._make_filename() |
|
84 self.write_file(self.filename) |
|
85 |
|
86 def erase(self): |
|
87 """Erase the data, both in this object, and from its file storage.""" |
|
88 if self.use_file: |
|
89 self._make_filename() |
|
90 if self.filename and os.path.exists(self.filename): |
|
91 os.remove(self.filename) |
|
92 self.lines = {} |
|
93 |
|
94 def line_data(self): |
|
95 """Return the map from filenames to lists of line numbers executed.""" |
|
96 return dict( |
|
97 [(f, sorted(linemap.keys())) for f, linemap in self.lines.items()] |
|
98 ) |
|
99 |
|
100 def write_file(self, filename): |
|
101 """Write the coverage data to `filename`.""" |
|
102 |
|
103 # Create the file data. |
|
104 data = {} |
|
105 |
|
106 data['lines'] = self.line_data() |
|
107 |
|
108 if self.collector: |
|
109 data['collector'] = self.collector |
|
110 |
|
111 # Write the pickle to the file. |
|
112 fdata = open(filename, 'wb') |
|
113 try: |
|
114 pickle.dump(data, fdata, 2) |
|
115 finally: |
|
116 fdata.close() |
|
117 |
|
118 def read_file(self, filename): |
|
119 """Read the coverage data from `filename`.""" |
|
120 self.lines = self._read_file(filename) |
|
121 |
|
122 def _read_file(self, filename): |
|
123 """Return the stored coverage data from the given file.""" |
|
124 try: |
|
125 fdata = open(filename, 'rb') |
|
126 try: |
|
127 data = pickle.load(fdata) |
|
128 finally: |
|
129 fdata.close() |
|
130 if isinstance(data, dict): |
|
131 # Unpack the 'lines' item. |
|
132 lines = dict([ |
|
133 (f, dict([(l, True) for l in linenos])) |
|
134 for f,linenos in data['lines'].items() |
|
135 ]) |
|
136 return lines |
|
137 else: |
|
138 return {} |
|
139 except Exception: |
|
140 return {} |
|
141 |
|
142 def combine_parallel_data(self): |
|
143 """ Treat self.filename as a file prefix, and combine the data from all |
|
144 of the files starting with that prefix. |
|
145 """ |
|
146 self._make_filename() |
|
147 data_dir, local = os.path.split(self.filename) |
|
148 for f in os.listdir(data_dir or '.'): |
|
149 if f.startswith(local): |
|
150 full_path = os.path.join(data_dir, f) |
|
151 new_data = self._read_file(full_path) |
|
152 for filename, file_data in new_data.items(): |
|
153 self.lines.setdefault(filename, {}).update(file_data) |
|
154 |
|
155 def add_line_data(self, data_points): |
|
156 """Add executed line data. |
|
157 |
|
158 `data_points` is (filename, lineno) pairs. |
|
159 |
|
160 """ |
|
161 for filename, lineno in data_points: |
|
162 self.lines.setdefault(filename, {})[lineno] = True |
|
163 |
|
164 def executed_files(self): |
|
165 """A list of all files that had been measured as executed.""" |
|
166 return self.lines.keys() |
|
167 |
|
168 def executed_lines(self, filename): |
|
169 """A map containing all the line numbers executed in `filename`. |
|
170 |
|
171 If `filename` hasn't been collected at all (because it wasn't executed) |
|
172 then return an empty map. |
|
173 |
|
174 """ |
|
175 return self.lines.get(filename) or {} |
|
176 |
|
177 def summary(self): |
|
178 """Return a dict summarizing the coverage data. |
|
179 |
|
180 Keys are the basename of the filenames, and values are the number of |
|
181 executed lines. This is useful in the unit tests. |
|
182 |
|
183 """ |
|
184 summ = {} |
|
185 for filename, lines in self.lines.items(): |
|
186 summ[os.path.basename(filename)] = len(lines) |
|
187 return summ |