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