6 # TODO: factor out dataop debugging to a wrapper class? |
6 # TODO: factor out dataop debugging to a wrapper class? |
7 # TODO: make sure all dataop debugging is in place somehow |
7 # TODO: make sure all dataop debugging is in place somehow |
8 |
8 |
9 import collections |
9 import collections |
10 import datetime |
10 import datetime |
|
11 import functools |
11 import glob |
12 import glob |
12 import itertools |
13 import itertools |
13 import os |
14 import os |
14 import re |
15 import re |
15 import sqlite3 |
16 import sqlite3 |
16 import sys |
17 import sys |
|
18 import threading |
17 import zlib |
19 import zlib |
18 |
20 |
19 from coverage import env |
|
20 from coverage.backward import get_thread_id, iitems, to_bytes, to_string |
|
21 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr |
21 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr |
|
22 from coverage.exceptions import CoverageException |
22 from coverage.files import PathAliases |
23 from coverage.files import PathAliases |
23 from coverage.misc import CoverageException, contract, file_be_gone, filename_suffix, isolate_module |
24 from coverage.misc import contract, file_be_gone, filename_suffix, isolate_module |
24 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits |
25 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits |
25 from coverage.version import __version__ |
26 from coverage.version import __version__ |
26 |
27 |
27 os = isolate_module(os) |
28 os = isolate_module(os) |
28 |
29 |
205 self._choose_filename() |
210 self._choose_filename() |
206 self._file_map = {} |
211 self._file_map = {} |
207 # Maps thread ids to SqliteDb objects. |
212 # Maps thread ids to SqliteDb objects. |
208 self._dbs = {} |
213 self._dbs = {} |
209 self._pid = os.getpid() |
214 self._pid = os.getpid() |
|
215 # Synchronize the operations used during collection. |
|
216 self._lock = threading.Lock() |
210 |
217 |
211 # Are we in sync with the data file? |
218 # Are we in sync with the data file? |
212 self._have_used = False |
219 self._have_used = False |
213 |
220 |
214 self._has_lines = False |
221 self._has_lines = False |
215 self._has_arcs = False |
222 self._has_arcs = False |
216 |
223 |
217 self._current_context = None |
224 self._current_context = None |
218 self._current_context_id = None |
225 self._current_context_id = None |
219 self._query_context_ids = None |
226 self._query_context_ids = None |
|
227 |
|
228 def _locked(method): # pylint: disable=no-self-argument |
|
229 """A decorator for methods that should hold self._lock.""" |
|
230 @functools.wraps(method) |
|
231 def _wrapped(self, *args, **kwargs): |
|
232 with self._lock: |
|
233 # pylint: disable=not-callable |
|
234 return method(self, *args, **kwargs) |
|
235 return _wrapped |
220 |
236 |
221 def _choose_filename(self): |
237 def _choose_filename(self): |
222 """Set self._filename based on inited attributes.""" |
238 """Set self._filename based on inited attributes.""" |
223 if self._no_disk: |
239 if self._no_disk: |
224 self._filename = ":memory:" |
240 self._filename = ":memory:" |
241 def _create_db(self): |
257 def _create_db(self): |
242 """Create a db file that doesn't exist yet. |
258 """Create a db file that doesn't exist yet. |
243 |
259 |
244 Initializes the schema and certain metadata. |
260 Initializes the schema and certain metadata. |
245 """ |
261 """ |
246 if self._debug.should('dataio'): |
262 if self._debug.should("dataio"): |
247 self._debug.write("Creating data file {!r}".format(self._filename)) |
263 self._debug.write(f"Creating data file {self._filename!r}") |
248 self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) |
264 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug) |
249 with db: |
265 with db: |
250 db.executescript(SCHEMA) |
266 db.executescript(SCHEMA) |
251 db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) |
267 db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) |
252 db.executemany( |
268 db.executemany( |
253 "insert into meta (key, value) values (?, ?)", |
269 "insert into meta (key, value) values (?, ?)", |
254 [ |
270 [ |
255 ('sys_argv', str(getattr(sys, 'argv', None))), |
271 ("sys_argv", str(getattr(sys, "argv", None))), |
256 ('version', __version__), |
272 ("version", __version__), |
257 ('when', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), |
273 ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), |
258 ] |
274 ] |
259 ) |
275 ) |
260 |
276 |
261 def _open_db(self): |
277 def _open_db(self): |
262 """Open an existing db file, and read its metadata.""" |
278 """Open an existing db file, and read its metadata.""" |
263 if self._debug.should('dataio'): |
279 if self._debug.should("dataio"): |
264 self._debug.write("Opening data file {!r}".format(self._filename)) |
280 self._debug.write(f"Opening data file {self._filename!r}") |
265 self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) |
281 self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug) |
266 self._read_db() |
282 self._read_db() |
267 |
283 |
268 def _read_db(self): |
284 def _read_db(self): |
269 """Read the metadata from a database so that we are ready to use it.""" |
285 """Read the metadata from a database so that we are ready to use it.""" |
270 with self._dbs[get_thread_id()] as db: |
286 with self._dbs[threading.get_ident()] as db: |
271 try: |
287 try: |
272 schema_version, = db.execute_one("select version from coverage_schema") |
288 schema_version, = db.execute_one("select version from coverage_schema") |
273 except Exception as exc: |
289 except Exception as exc: |
274 raise CoverageException( |
290 raise CoverageException( |
275 "Data file {!r} doesn't seem to be a coverage data file: {}".format( |
291 "Data file {!r} doesn't seem to be a coverage data file: {}".format( |
276 self._filename, exc |
292 self._filename, exc |
277 ) |
293 ) |
278 ) |
294 ) from exc |
279 else: |
295 else: |
280 if schema_version != SCHEMA_VERSION: |
296 if schema_version != SCHEMA_VERSION: |
281 raise CoverageException( |
297 raise CoverageException( |
282 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( |
298 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( |
283 self._filename, schema_version, SCHEMA_VERSION |
299 self._filename, schema_version, SCHEMA_VERSION |
291 for path, file_id in db.execute("select path, id from file"): |
307 for path, file_id in db.execute("select path, id from file"): |
292 self._file_map[path] = file_id |
308 self._file_map[path] = file_id |
293 |
309 |
294 def _connect(self): |
310 def _connect(self): |
295 """Get the SqliteDb object to use.""" |
311 """Get the SqliteDb object to use.""" |
296 if get_thread_id() not in self._dbs: |
312 if threading.get_ident() not in self._dbs: |
297 if os.path.exists(self._filename): |
313 if os.path.exists(self._filename): |
298 self._open_db() |
314 self._open_db() |
299 else: |
315 else: |
300 self._create_db() |
316 self._create_db() |
301 return self._dbs[get_thread_id()] |
317 return self._dbs[threading.get_ident()] |
302 |
318 |
303 def __nonzero__(self): |
319 def __nonzero__(self): |
304 if (get_thread_id() not in self._dbs and not os.path.exists(self._filename)): |
320 if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)): |
305 return False |
321 return False |
306 try: |
322 try: |
307 with self._connect() as con: |
323 with self._connect() as con: |
308 rows = con.execute("select * from file limit 1") |
324 rows = con.execute("select * from file limit 1") |
309 return bool(list(rows)) |
325 return bool(list(rows)) |
310 except CoverageException: |
326 except CoverageException: |
311 return False |
327 return False |
312 |
328 |
313 __bool__ = __nonzero__ |
329 __bool__ = __nonzero__ |
314 |
330 |
315 @contract(returns='bytes') |
331 @contract(returns="bytes") |
316 def dumps(self): |
332 def dumps(self): |
317 """Serialize the current data to a byte string. |
333 """Serialize the current data to a byte string. |
318 |
334 |
319 The format of the serialized data is not documented. It is only |
335 The format of the serialized data is not documented. It is only |
320 suitable for use with :meth:`loads` in the same version of |
336 suitable for use with :meth:`loads` in the same version of |
321 coverage.py. |
337 coverage.py. |
322 |
338 |
|
339 Note that this serialization is not what gets stored in coverage data |
|
340 files. This method is meant to produce bytes that can be transmitted |
|
341 elsewhere and then deserialized with :meth:`loads`. |
|
342 |
323 Returns: |
343 Returns: |
324 A byte string of serialized data. |
344 A byte string of serialized data. |
325 |
345 |
326 .. versionadded:: 5.0 |
346 .. versionadded:: 5.0 |
327 |
347 |
328 """ |
348 """ |
329 if self._debug.should('dataio'): |
349 if self._debug.should("dataio"): |
330 self._debug.write("Dumping data from data file {!r}".format(self._filename)) |
350 self._debug.write(f"Dumping data from data file {self._filename!r}") |
331 with self._connect() as con: |
351 with self._connect() as con: |
332 return b'z' + zlib.compress(to_bytes(con.dump())) |
352 return b"z" + zlib.compress(con.dump().encode("utf-8")) |
333 |
353 |
334 @contract(data='bytes') |
354 @contract(data="bytes") |
335 def loads(self, data): |
355 def loads(self, data): |
336 """Deserialize data from :meth:`dumps` |
356 """Deserialize data from :meth:`dumps`. |
337 |
357 |
338 Use with a newly-created empty :class:`CoverageData` object. It's |
358 Use with a newly-created empty :class:`CoverageData` object. It's |
339 undefined what happens if the object already has data in it. |
359 undefined what happens if the object already has data in it. |
340 |
360 |
|
361 Note that this is not for reading data from a coverage data file. It |
|
362 is only for use on data you produced with :meth:`dumps`. |
|
363 |
341 Arguments: |
364 Arguments: |
342 data: A byte string of serialized data produced by :meth:`dumps`. |
365 data: A byte string of serialized data produced by :meth:`dumps`. |
343 |
366 |
344 .. versionadded:: 5.0 |
367 .. versionadded:: 5.0 |
345 |
368 |
346 """ |
369 """ |
347 if self._debug.should('dataio'): |
370 if self._debug.should("dataio"): |
348 self._debug.write("Loading data into data file {!r}".format(self._filename)) |
371 self._debug.write(f"Loading data into data file {self._filename!r}") |
349 if data[:1] != b'z': |
372 if data[:1] != b"z": |
350 raise CoverageException( |
373 raise CoverageException( |
351 "Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data)) |
374 f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)" |
352 ) |
375 ) |
353 script = to_string(zlib.decompress(data[1:])) |
376 script = zlib.decompress(data[1:]).decode("utf-8") |
354 self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) |
377 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug) |
355 with db: |
378 with db: |
356 db.executescript(script) |
379 db.executescript(script) |
357 self._read_db() |
380 self._read_db() |
358 self._have_used = True |
381 self._have_used = True |
359 |
382 |
420 .. versionadded:: 5.0 |
444 .. versionadded:: 5.0 |
421 |
445 |
422 """ |
446 """ |
423 return self._filename |
447 return self._filename |
424 |
448 |
|
449 @_locked |
425 def add_lines(self, line_data): |
450 def add_lines(self, line_data): |
426 """Add measured line data. |
451 """Add measured line data. |
427 |
452 |
428 `line_data` is a dictionary mapping file names to dictionaries:: |
453 `line_data` is a dictionary mapping file names to iterables of ints:: |
429 |
454 |
430 { filename: { lineno: None, ... }, ...} |
455 { filename: { line1, line2, ... }, ...} |
431 |
456 |
432 """ |
457 """ |
433 if self._debug.should('dataop'): |
458 if self._debug.should("dataop"): |
434 self._debug.write("Adding lines: %d files, %d lines total" % ( |
459 self._debug.write("Adding lines: %d files, %d lines total" % ( |
435 len(line_data), sum(len(lines) for lines in line_data.values()) |
460 len(line_data), sum(len(lines) for lines in line_data.values()) |
436 )) |
461 )) |
437 self._start_using() |
462 self._start_using() |
438 self._choose_lines_or_arcs(lines=True) |
463 self._choose_lines_or_arcs(lines=True) |
439 if not line_data: |
464 if not line_data: |
440 return |
465 return |
441 with self._connect() as con: |
466 with self._connect() as con: |
442 self._set_context_id() |
467 self._set_context_id() |
443 for filename, linenos in iitems(line_data): |
468 for filename, linenos in line_data.items(): |
444 linemap = nums_to_numbits(linenos) |
469 linemap = nums_to_numbits(linenos) |
445 file_id = self._file_id(filename, add=True) |
470 file_id = self._file_id(filename, add=True) |
446 query = "select numbits from line_bits where file_id = ? and context_id = ?" |
471 query = "select numbits from line_bits where file_id = ? and context_id = ?" |
447 existing = list(con.execute(query, (file_id, self._current_context_id))) |
472 existing = list(con.execute(query, (file_id, self._current_context_id))) |
448 if existing: |
473 if existing: |
449 linemap = numbits_union(linemap, existing[0][0]) |
474 linemap = numbits_union(linemap, existing[0][0]) |
450 |
475 |
451 con.execute( |
476 con.execute( |
452 "insert or replace into line_bits " |
477 "insert or replace into line_bits " + |
453 " (file_id, context_id, numbits) values (?, ?, ?)", |
478 " (file_id, context_id, numbits) values (?, ?, ?)", |
454 (file_id, self._current_context_id, linemap), |
479 (file_id, self._current_context_id, linemap), |
455 ) |
480 ) |
456 |
481 |
|
482 @_locked |
457 def add_arcs(self, arc_data): |
483 def add_arcs(self, arc_data): |
458 """Add measured arc data. |
484 """Add measured arc data. |
459 |
485 |
460 `arc_data` is a dictionary mapping file names to dictionaries:: |
486 `arc_data` is a dictionary mapping file names to iterables of pairs of |
461 |
487 ints:: |
462 { filename: { (l1,l2): None, ... }, ...} |
488 |
463 |
489 { filename: { (l1,l2), (l1,l2), ... }, ...} |
464 """ |
490 |
465 if self._debug.should('dataop'): |
491 """ |
|
492 if self._debug.should("dataop"): |
466 self._debug.write("Adding arcs: %d files, %d arcs total" % ( |
493 self._debug.write("Adding arcs: %d files, %d arcs total" % ( |
467 len(arc_data), sum(len(arcs) for arcs in arc_data.values()) |
494 len(arc_data), sum(len(arcs) for arcs in arc_data.values()) |
468 )) |
495 )) |
469 self._start_using() |
496 self._start_using() |
470 self._choose_lines_or_arcs(arcs=True) |
497 self._choose_lines_or_arcs(arcs=True) |
471 if not arc_data: |
498 if not arc_data: |
472 return |
499 return |
473 with self._connect() as con: |
500 with self._connect() as con: |
474 self._set_context_id() |
501 self._set_context_id() |
475 for filename, arcs in iitems(arc_data): |
502 for filename, arcs in arc_data.items(): |
476 file_id = self._file_id(filename, add=True) |
503 file_id = self._file_id(filename, add=True) |
477 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] |
504 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] |
478 con.executemany( |
505 con.executemany( |
479 "insert or ignore into arc " |
506 "insert or ignore into arc " + |
480 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", |
507 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", |
481 data, |
508 data, |
482 ) |
509 ) |
483 |
510 |
484 def _choose_lines_or_arcs(self, lines=False, arcs=False): |
511 def _choose_lines_or_arcs(self, lines=False, arcs=False): |
493 self._has_lines = lines |
520 self._has_lines = lines |
494 self._has_arcs = arcs |
521 self._has_arcs = arcs |
495 with self._connect() as con: |
522 with self._connect() as con: |
496 con.execute( |
523 con.execute( |
497 "insert into meta (key, value) values (?, ?)", |
524 "insert into meta (key, value) values (?, ?)", |
498 ('has_arcs', str(int(arcs))) |
525 ("has_arcs", str(int(arcs))) |
499 ) |
526 ) |
500 |
527 |
|
528 @_locked |
501 def add_file_tracers(self, file_tracers): |
529 def add_file_tracers(self, file_tracers): |
502 """Add per-file plugin information. |
530 """Add per-file plugin information. |
503 |
531 |
504 `file_tracers` is { filename: plugin_name, ... } |
532 `file_tracers` is { filename: plugin_name, ... } |
505 |
533 |
506 """ |
534 """ |
507 if self._debug.should('dataop'): |
535 if self._debug.should("dataop"): |
508 self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) |
536 self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) |
509 if not file_tracers: |
537 if not file_tracers: |
510 return |
538 return |
511 self._start_using() |
539 self._start_using() |
512 with self._connect() as con: |
540 with self._connect() as con: |
513 for filename, plugin_name in iitems(file_tracers): |
541 for filename, plugin_name in file_tracers.items(): |
514 file_id = self._file_id(filename) |
542 file_id = self._file_id(filename) |
515 if file_id is None: |
543 if file_id is None: |
516 raise CoverageException( |
544 raise CoverageException( |
517 "Can't add file tracer data for unmeasured file '%s'" % (filename,) |
545 f"Can't add file tracer data for unmeasured file '{filename}'" |
518 ) |
546 ) |
519 |
547 |
520 existing_plugin = self.file_tracer(filename) |
548 existing_plugin = self.file_tracer(filename) |
521 if existing_plugin: |
549 if existing_plugin: |
522 if existing_plugin != plugin_name: |
550 if existing_plugin != plugin_name: |
523 raise CoverageException( |
551 raise CoverageException( |
524 "Conflicting file tracer name for '%s': %r vs %r" % ( |
552 "Conflicting file tracer name for '{}': {!r} vs {!r}".format( |
525 filename, existing_plugin, plugin_name, |
553 filename, existing_plugin, plugin_name, |
526 ) |
554 ) |
527 ) |
555 ) |
528 elif plugin_name: |
556 elif plugin_name: |
529 con.execute( |
557 con.execute( |
581 |
609 |
582 # Collector for all arcs, lines and tracers |
610 # Collector for all arcs, lines and tracers |
583 other_data.read() |
611 other_data.read() |
584 with other_data._connect() as conn: |
612 with other_data._connect() as conn: |
585 # Get files data. |
613 # Get files data. |
586 cur = conn.execute('select path from file') |
614 cur = conn.execute("select path from file") |
587 files = {path: aliases.map(path) for (path,) in cur} |
615 files = {path: aliases.map(path) for (path,) in cur} |
588 cur.close() |
616 cur.close() |
589 |
617 |
590 # Get contexts data. |
618 # Get contexts data. |
591 cur = conn.execute('select context from context') |
619 cur = conn.execute("select context from context") |
592 contexts = [context for (context,) in cur] |
620 contexts = [context for (context,) in cur] |
593 cur.close() |
621 cur.close() |
594 |
622 |
595 # Get arc data. |
623 # Get arc data. |
596 cur = conn.execute( |
624 cur = conn.execute( |
597 'select file.path, context.context, arc.fromno, arc.tono ' |
625 "select file.path, context.context, arc.fromno, arc.tono " + |
598 'from arc ' |
626 "from arc " + |
599 'inner join file on file.id = arc.file_id ' |
627 "inner join file on file.id = arc.file_id " + |
600 'inner join context on context.id = arc.context_id' |
628 "inner join context on context.id = arc.context_id" |
601 ) |
629 ) |
602 arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur] |
630 arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur] |
603 cur.close() |
631 cur.close() |
604 |
632 |
605 # Get line data. |
633 # Get line data. |
606 cur = conn.execute( |
634 cur = conn.execute( |
607 'select file.path, context.context, line_bits.numbits ' |
635 "select file.path, context.context, line_bits.numbits " + |
608 'from line_bits ' |
636 "from line_bits " + |
609 'inner join file on file.id = line_bits.file_id ' |
637 "inner join file on file.id = line_bits.file_id " + |
610 'inner join context on context.id = line_bits.context_id' |
638 "inner join context on context.id = line_bits.context_id" |
611 ) |
639 ) |
612 lines = { |
640 lines = {(files[path], context): numbits for (path, context, numbits) in cur} |
613 (files[path], context): numbits |
|
614 for (path, context, numbits) in cur |
|
615 } |
|
616 cur.close() |
641 cur.close() |
617 |
642 |
618 # Get tracer data. |
643 # Get tracer data. |
619 cur = conn.execute( |
644 cur = conn.execute( |
620 'select file.path, tracer ' |
645 "select file.path, tracer " + |
621 'from tracer ' |
646 "from tracer " + |
622 'inner join file on file.id = tracer.file_id' |
647 "inner join file on file.id = tracer.file_id" |
623 ) |
648 ) |
624 tracers = {files[path]: tracer for (path, tracer) in cur} |
649 tracers = {files[path]: tracer for (path, tracer) in cur} |
625 cur.close() |
650 cur.close() |
626 |
651 |
627 with self._connect() as conn: |
652 with self._connect() as conn: |
628 conn.con.isolation_level = 'IMMEDIATE' |
653 conn.con.isolation_level = "IMMEDIATE" |
629 |
654 |
630 # Get all tracers in the DB. Files not in the tracers are assumed |
655 # Get all tracers in the DB. Files not in the tracers are assumed |
631 # to have an empty string tracer. Since Sqlite does not support |
656 # to have an empty string tracer. Since Sqlite does not support |
632 # full outer joins, we have to make two queries to fill the |
657 # full outer joins, we have to make two queries to fill the |
633 # dictionary. |
658 # dictionary. |
634 this_tracers = {path: '' for path, in conn.execute('select path from file')} |
659 this_tracers = {path: "" for path, in conn.execute("select path from file")} |
635 this_tracers.update({ |
660 this_tracers.update({ |
636 aliases.map(path): tracer |
661 aliases.map(path): tracer |
637 for path, tracer in conn.execute( |
662 for path, tracer in conn.execute( |
638 'select file.path, tracer from tracer ' |
663 "select file.path, tracer from tracer " + |
639 'inner join file on file.id = tracer.file_id' |
664 "inner join file on file.id = tracer.file_id" |
640 ) |
665 ) |
641 }) |
666 }) |
642 |
667 |
643 # Create all file and context rows in the DB. |
668 # Create all file and context rows in the DB. |
644 conn.executemany( |
669 conn.executemany( |
645 'insert or ignore into file (path) values (?)', |
670 "insert or ignore into file (path) values (?)", |
646 ((file,) for file in files.values()) |
671 ((file,) for file in files.values()) |
647 ) |
672 ) |
648 file_ids = { |
673 file_ids = { |
649 path: id |
674 path: id |
650 for id, path in conn.execute('select id, path from file') |
675 for id, path in conn.execute("select id, path from file") |
651 } |
676 } |
652 conn.executemany( |
677 conn.executemany( |
653 'insert or ignore into context (context) values (?)', |
678 "insert or ignore into context (context) values (?)", |
654 ((context,) for context in contexts) |
679 ((context,) for context in contexts) |
655 ) |
680 ) |
656 context_ids = { |
681 context_ids = { |
657 context: id |
682 context: id |
658 for id, context in conn.execute('select id, context from context') |
683 for id, context in conn.execute("select id, context from context") |
659 } |
684 } |
660 |
685 |
661 # Prepare tracers and fail, if a conflict is found. |
686 # Prepare tracers and fail, if a conflict is found. |
662 # tracer_paths is used to ensure consistency over the tracer data |
687 # tracer_paths is used to ensure consistency over the tracer data |
663 # and tracer_map tracks the tracers to be inserted. |
688 # and tracer_map tracks the tracers to be inserted. |
664 tracer_map = {} |
689 tracer_map = {} |
665 for path in files.values(): |
690 for path in files.values(): |
666 this_tracer = this_tracers.get(path) |
691 this_tracer = this_tracers.get(path) |
667 other_tracer = tracers.get(path, '') |
692 other_tracer = tracers.get(path, "") |
668 # If there is no tracer, there is always the None tracer. |
693 # If there is no tracer, there is always the None tracer. |
669 if this_tracer is not None and this_tracer != other_tracer: |
694 if this_tracer is not None and this_tracer != other_tracer: |
670 raise CoverageException( |
695 raise CoverageException( |
671 "Conflicting file tracer name for '%s': %r vs %r" % ( |
696 "Conflicting file tracer name for '{}': {!r} vs {!r}".format( |
672 path, this_tracer, other_tracer |
697 path, this_tracer, other_tracer |
673 ) |
698 ) |
674 ) |
699 ) |
675 tracer_map[path] = other_tracer |
700 tracer_map[path] = other_tracer |
676 |
701 |
699 if arcs: |
724 if arcs: |
700 self._choose_lines_or_arcs(arcs=True) |
725 self._choose_lines_or_arcs(arcs=True) |
701 |
726 |
702 # Write the combined data. |
727 # Write the combined data. |
703 conn.executemany( |
728 conn.executemany( |
704 'insert or ignore into arc ' |
729 "insert or ignore into arc " + |
705 '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', |
730 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", |
706 arc_rows |
731 arc_rows |
707 ) |
732 ) |
708 |
733 |
709 if lines: |
734 if lines: |
710 self._choose_lines_or_arcs(lines=True) |
735 self._choose_lines_or_arcs(lines=True) |
711 conn.execute("delete from line_bits") |
736 conn.execute("delete from line_bits") |
712 conn.executemany( |
737 conn.executemany( |
713 "insert into line_bits " |
738 "insert into line_bits " + |
714 "(file_id, context_id, numbits) values (?, ?, ?)", |
739 "(file_id, context_id, numbits) values (?, ?, ?)", |
715 [ |
740 [ |
716 (file_ids[file], context_ids[context], numbits) |
741 (file_ids[file], context_ids[context], numbits) |
717 for (file, context), numbits in lines.items() |
742 for (file, context), numbits in lines.items() |
718 ] |
743 ] |
719 ) |
744 ) |
720 conn.executemany( |
745 conn.executemany( |
721 'insert or ignore into tracer (file_id, tracer) values (?, ?)', |
746 "insert or ignore into tracer (file_id, tracer) values (?, ?)", |
722 ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) |
747 ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) |
723 ) |
748 ) |
724 |
749 |
725 # Update all internal cache data. |
750 # Update all internal cache data. |
726 self._reset() |
751 self._reset() |
734 |
759 |
735 """ |
760 """ |
736 self._reset() |
761 self._reset() |
737 if self._no_disk: |
762 if self._no_disk: |
738 return |
763 return |
739 if self._debug.should('dataio'): |
764 if self._debug.should("dataio"): |
740 self._debug.write("Erasing data file {!r}".format(self._filename)) |
765 self._debug.write(f"Erasing data file {self._filename!r}") |
741 file_be_gone(self._filename) |
766 file_be_gone(self._filename) |
742 if parallel: |
767 if parallel: |
743 data_dir, local = os.path.split(self._filename) |
768 data_dir, local = os.path.split(self._filename) |
744 localdot = local + '.*' |
769 localdot = local + ".*" |
745 pattern = os.path.join(os.path.abspath(data_dir), localdot) |
770 pattern = os.path.join(os.path.abspath(data_dir), localdot) |
746 for filename in glob.glob(pattern): |
771 for filename in glob.glob(pattern): |
747 if self._debug.should('dataio'): |
772 if self._debug.should("dataio"): |
748 self._debug.write("Erasing parallel data file {!r}".format(filename)) |
773 self._debug.write(f"Erasing parallel data file {filename!r}") |
749 file_be_gone(filename) |
774 file_be_gone(filename) |
750 |
775 |
751 def read(self): |
776 def read(self): |
752 """Start using an existing data file.""" |
777 """Start using an existing data file.""" |
753 with self._connect(): # TODO: doesn't look right |
778 with self._connect(): # TODO: doesn't look right |
834 |
859 |
835 """ |
860 """ |
836 self._start_using() |
861 self._start_using() |
837 if contexts: |
862 if contexts: |
838 with self._connect() as con: |
863 with self._connect() as con: |
839 context_clause = ' or '.join(['context regexp ?'] * len(contexts)) |
864 context_clause = " or ".join(["context regexp ?"] * len(contexts)) |
840 cur = con.execute("select id from context where " + context_clause, contexts) |
865 cur = con.execute("select id from context where " + context_clause, contexts) |
841 self._query_context_ids = [row[0] for row in cur.fetchall()] |
866 self._query_context_ids = [row[0] for row in cur.fetchall()] |
842 else: |
867 else: |
843 self._query_context_ids = None |
868 self._query_context_ids = None |
844 |
869 |
845 def lines(self, filename): |
870 def lines(self, filename): |
846 """Get the list of lines executed for a file. |
871 """Get the list of lines executed for a source file. |
847 |
872 |
848 If the file was not measured, returns None. A file might be measured, |
873 If the file was not measured, returns None. A file might be measured, |
849 and have no lines executed, in which case an empty list is returned. |
874 and have no lines executed, in which case an empty list is returned. |
850 |
875 |
851 If the file was executed, returns a list of integers, the line numbers |
876 If the file was executed, returns a list of integers, the line numbers |
915 A dict mapping line numbers to a list of context names. |
940 A dict mapping line numbers to a list of context names. |
916 |
941 |
917 .. versionadded:: 5.0 |
942 .. versionadded:: 5.0 |
918 |
943 |
919 """ |
944 """ |
920 lineno_contexts_map = collections.defaultdict(list) |
|
921 self._start_using() |
945 self._start_using() |
922 with self._connect() as con: |
946 with self._connect() as con: |
923 file_id = self._file_id(filename) |
947 file_id = self._file_id(filename) |
924 if file_id is None: |
948 if file_id is None: |
925 return lineno_contexts_map |
949 return {} |
|
950 |
|
951 lineno_contexts_map = collections.defaultdict(set) |
926 if self.has_arcs(): |
952 if self.has_arcs(): |
927 query = ( |
953 query = ( |
928 "select arc.fromno, arc.tono, context.context " |
954 "select arc.fromno, arc.tono, context.context " + |
929 "from arc, context " |
955 "from arc, context " + |
930 "where arc.file_id = ? and arc.context_id = context.id" |
956 "where arc.file_id = ? and arc.context_id = context.id" |
931 ) |
957 ) |
932 data = [file_id] |
958 data = [file_id] |
933 if self._query_context_ids is not None: |
959 if self._query_context_ids is not None: |
934 ids_array = ', '.join('?' * len(self._query_context_ids)) |
960 ids_array = ", ".join("?" * len(self._query_context_ids)) |
935 query += " and arc.context_id in (" + ids_array + ")" |
961 query += " and arc.context_id in (" + ids_array + ")" |
936 data += self._query_context_ids |
962 data += self._query_context_ids |
937 for fromno, tono, context in con.execute(query, data): |
963 for fromno, tono, context in con.execute(query, data): |
938 if context not in lineno_contexts_map[fromno]: |
964 if fromno > 0: |
939 lineno_contexts_map[fromno].append(context) |
965 lineno_contexts_map[fromno].add(context) |
940 if context not in lineno_contexts_map[tono]: |
966 if tono > 0: |
941 lineno_contexts_map[tono].append(context) |
967 lineno_contexts_map[tono].add(context) |
942 else: |
968 else: |
943 query = ( |
969 query = ( |
944 "select l.numbits, c.context from line_bits l, context c " |
970 "select l.numbits, c.context from line_bits l, context c " + |
945 "where l.context_id = c.id " |
971 "where l.context_id = c.id " + |
946 "and file_id = ?" |
972 "and file_id = ?" |
947 ) |
973 ) |
948 data = [file_id] |
974 data = [file_id] |
949 if self._query_context_ids is not None: |
975 if self._query_context_ids is not None: |
950 ids_array = ', '.join('?' * len(self._query_context_ids)) |
976 ids_array = ", ".join("?" * len(self._query_context_ids)) |
951 query += " and l.context_id in (" + ids_array + ")" |
977 query += " and l.context_id in (" + ids_array + ")" |
952 data += self._query_context_ids |
978 data += self._query_context_ids |
953 for numbits, context in con.execute(query, data): |
979 for numbits, context in con.execute(query, data): |
954 for lineno in numbits_to_nums(numbits): |
980 for lineno in numbits_to_nums(numbits): |
955 lineno_contexts_map[lineno].append(context) |
981 lineno_contexts_map[lineno].add(context) |
956 return lineno_contexts_map |
982 |
|
983 return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()} |
957 |
984 |
958 @classmethod |
985 @classmethod |
959 def sys_info(cls): |
986 def sys_info(cls): |
960 """Our information for `Coverage.sys_info`. |
987 """Our information for `Coverage.sys_info`. |
961 |
988 |
962 Returns a list of (key, value) pairs. |
989 Returns a list of (key, value) pairs. |
963 |
990 |
964 """ |
991 """ |
965 with SqliteDb(":memory:", debug=NoDebugging()) as db: |
992 with SqliteDb(":memory:", debug=NoDebugging()) as db: |
966 temp_store = [row[0] for row in db.execute("pragma temp_store")] |
993 temp_store = [row[0] for row in db.execute("pragma temp_store")] |
967 compile_options = [row[0] for row in db.execute("pragma compile_options")] |
994 copts = [row[0] for row in db.execute("pragma compile_options")] |
|
995 # Yes, this is overkill. I don't like the long list of options |
|
996 # at the end of "debug sys", but I don't want to omit information. |
|
997 copts = ["; ".join(copts[i:i + 3]) for i in range(0, len(copts), 3)] |
968 |
998 |
969 return [ |
999 return [ |
970 ('sqlite3_version', sqlite3.version), |
1000 ("sqlite3_version", sqlite3.version), |
971 ('sqlite3_sqlite_version', sqlite3.sqlite_version), |
1001 ("sqlite3_sqlite_version", sqlite3.sqlite_version), |
972 ('sqlite3_temp_store', temp_store), |
1002 ("sqlite3_temp_store", temp_store), |
973 ('sqlite3_compile_options', compile_options), |
1003 ("sqlite3_compile_options", copts), |
974 ] |
1004 ] |
975 |
1005 |
976 |
1006 |
977 class SqliteDb(SimpleReprMixin): |
1007 class SqliteDb(SimpleReprMixin): |
978 """A simple abstraction over a SQLite database. |
1008 """A simple abstraction over a SQLite database. |
983 with SqliteDb(filename, debug_control) as db: |
1013 with SqliteDb(filename, debug_control) as db: |
984 db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,)) |
1014 db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,)) |
985 |
1015 |
986 """ |
1016 """ |
987 def __init__(self, filename, debug): |
1017 def __init__(self, filename, debug): |
988 self.debug = debug if debug.should('sql') else None |
1018 self.debug = debug if debug.should("sql") else None |
989 self.filename = filename |
1019 self.filename = filename |
990 self.nest = 0 |
1020 self.nest = 0 |
991 self.con = None |
1021 self.con = None |
992 |
1022 |
993 def _connect(self): |
1023 def _connect(self): |
994 """Connect to the db and do universal initialization.""" |
1024 """Connect to the db and do universal initialization.""" |
995 if self.con is not None: |
1025 if self.con is not None: |
996 return |
1026 return |
997 |
|
998 # SQLite on Windows on py2 won't open a file if the filename argument |
|
999 # has non-ascii characters in it. Opening a relative file name avoids |
|
1000 # a problem if the current directory has non-ascii. |
|
1001 filename = self.filename |
|
1002 if env.WINDOWS and env.PY2: |
|
1003 try: |
|
1004 filename = os.path.relpath(self.filename) |
|
1005 except ValueError: |
|
1006 # ValueError can be raised under Windows when os.getcwd() returns a |
|
1007 # folder from a different drive than the drive of self.filename in |
|
1008 # which case we keep the original value of self.filename unchanged, |
|
1009 # hoping that we won't face the non-ascii directory problem. |
|
1010 pass |
|
1011 |
1027 |
1012 # It can happen that Python switches threads while the tracer writes |
1028 # It can happen that Python switches threads while the tracer writes |
1013 # data. The second thread will also try to write to the data, |
1029 # data. The second thread will also try to write to the data, |
1014 # effectively causing a nested context. However, given the idempotent |
1030 # effectively causing a nested context. However, given the idempotent |
1015 # nature of the tracer operations, sharing a connection among threads |
1031 # nature of the tracer operations, sharing a connection among threads |
1016 # is not a problem. |
1032 # is not a problem. |
1017 if self.debug: |
1033 if self.debug: |
1018 self.debug.write("Connecting to {!r}".format(self.filename)) |
1034 self.debug.write(f"Connecting to {self.filename!r}") |
1019 self.con = sqlite3.connect(filename, check_same_thread=False) |
1035 try: |
1020 self.con.create_function('REGEXP', 2, _regexp) |
1036 self.con = sqlite3.connect(self.filename, check_same_thread=False) |
|
1037 except sqlite3.Error as exc: |
|
1038 raise CoverageException(f"Couldn't use data file {self.filename!r}: {exc}") from exc |
|
1039 |
|
1040 self.con.create_function("REGEXP", 2, _regexp) |
1021 |
1041 |
1022 # This pragma makes writing faster. It disables rollbacks, but we never need them. |
1042 # This pragma makes writing faster. It disables rollbacks, but we never need them. |
1023 # PyPy needs the .close() calls here, or sqlite gets twisted up: |
1043 # PyPy needs the .close() calls here, or sqlite gets twisted up: |
1024 # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on |
1044 # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on |
1025 self.execute("pragma journal_mode=off").close() |
1045 self.execute("pragma journal_mode=off").close() |
1070 # hard to provide useful hints if something goes wrong now. |
1090 # hard to provide useful hints if something goes wrong now. |
1071 with open(self.filename, "rb") as bad_file: |
1091 with open(self.filename, "rb") as bad_file: |
1072 cov4_sig = b"!coverage.py: This is a private format" |
1092 cov4_sig = b"!coverage.py: This is a private format" |
1073 if bad_file.read(len(cov4_sig)) == cov4_sig: |
1093 if bad_file.read(len(cov4_sig)) == cov4_sig: |
1074 msg = ( |
1094 msg = ( |
1075 "Looks like a coverage 4.x data file. " |
1095 "Looks like a coverage 4.x data file. " + |
1076 "Are you mixing versions of coverage?" |
1096 "Are you mixing versions of coverage?" |
1077 ) |
1097 ) |
1078 except Exception: |
1098 except Exception: # pragma: cant happen |
1079 pass |
1099 pass |
1080 if self.debug: |
1100 if self.debug: |
1081 self.debug.write("EXCEPTION from execute: {}".format(msg)) |
1101 self.debug.write(f"EXCEPTION from execute: {msg}") |
1082 raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, msg)) |
1102 raise CoverageException(f"Couldn't use data file {self.filename!r}: {msg}") from exc |
1083 |
1103 |
1084 def execute_one(self, sql, parameters=()): |
1104 def execute_one(self, sql, parameters=()): |
1085 """Execute a statement and return the one row that results. |
1105 """Execute a statement and return the one row that results. |
1086 |
1106 |
1087 This is like execute(sql, parameters).fetchone(), except it is |
1107 This is like execute(sql, parameters).fetchone(), except it is |
1094 if len(rows) == 0: |
1114 if len(rows) == 0: |
1095 return None |
1115 return None |
1096 elif len(rows) == 1: |
1116 elif len(rows) == 1: |
1097 return rows[0] |
1117 return rows[0] |
1098 else: |
1118 else: |
1099 raise CoverageException("Sql {!r} shouldn't return {} rows".format(sql, len(rows))) |
1119 raise AssertionError(f"SQL {sql!r} shouldn't return {len(rows)} rows") |
1100 |
1120 |
1101 def executemany(self, sql, data): |
1121 def executemany(self, sql, data): |
1102 """Same as :meth:`python:sqlite3.Connection.executemany`.""" |
1122 """Same as :meth:`python:sqlite3.Connection.executemany`.""" |
1103 if self.debug: |
1123 if self.debug: |
1104 data = list(data) |
1124 data = list(data) |
1105 self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) |
1125 self.debug.write(f"Executing many {sql!r} with {len(data)} rows") |
1106 return self.con.executemany(sql, data) |
1126 try: |
|
1127 return self.con.executemany(sql, data) |
|
1128 except Exception: # pragma: cant happen |
|
1129 # In some cases, an error might happen that isn't really an |
|
1130 # error. Try again immediately. |
|
1131 # https://github.com/nedbat/coveragepy/issues/1010 |
|
1132 return self.con.executemany(sql, data) |
1107 |
1133 |
1108 def executescript(self, script): |
1134 def executescript(self, script): |
1109 """Same as :meth:`python:sqlite3.Connection.executescript`.""" |
1135 """Same as :meth:`python:sqlite3.Connection.executescript`.""" |
1110 if self.debug: |
1136 if self.debug: |
1111 self.debug.write("Executing script with {} chars: {}".format( |
1137 self.debug.write("Executing script with {} chars: {}".format( |