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