Coverage for coverage / sqldata.py: 100.000%
465 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""SQLite coverage data."""
6from __future__ import annotations 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
8import collections 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
9import datetime 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
10import functools 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
11import glob 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
12import itertools 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
13import os 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
14import random 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
15import socket 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
16import sqlite3 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
17import string 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
18import sys 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
19import textwrap 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
20import threading 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
21import uuid 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
22import zlib 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
23from collections.abc import Collection, Mapping, Sequence 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
24from typing import Any, Callable, cast 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
26from coverage.debug import NoDebugging, auto_repr, file_summary 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
27from coverage.exceptions import CoverageException, DataError 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
28from coverage.misc import file_be_gone, isolate_module 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
29from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
30from coverage.sqlitedb import SqliteDb 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
31from coverage.types import AnyCallable, FilePath, TArc, TDebugCtl, TLineNo, TWarnFn 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
32from coverage.version import __version__ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
34os = isolate_module(os) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
36# If you change the schema: increment the SCHEMA_VERSION and update the
37# docs in docs/dbschema.rst by running "make cogdoc".
39SCHEMA_VERSION = 7 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
41# Schema versions:
42# 1: Released in 5.0a2
43# 2: Added contexts in 5.0a3.
44# 3: Replaced line table with line_map table.
45# 4: Changed line_map.bitmap to line_map.numbits.
46# 5: Added foreign key declarations.
47# 6: Key-value in meta.
48# 7: line_map -> line_bits
50SCHEMA = """\ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
51CREATE TABLE coverage_schema (
52 -- One row, to record the version of the schema in this db.
53 version integer
54);
56CREATE TABLE meta (
57 -- Key-value pairs, to record metadata about the data
58 key text,
59 value text,
60 unique (key)
61 -- Possible keys:
62 -- 'has_arcs' boolean -- Is this data recording branches?
63 -- 'sys_argv' text -- The coverage command line that recorded the data.
64 -- 'version' text -- The version of coverage.py that made the file.
65 -- 'when' text -- Datetime when the file was created.
66);
68CREATE TABLE file (
69 -- A row per file measured.
70 id integer primary key,
71 path text,
72 unique (path)
73);
75CREATE TABLE context (
76 -- A row per context measured.
77 id integer primary key,
78 context text,
79 unique (context)
80);
82CREATE TABLE line_bits (
83 -- If recording lines, a row per context per file executed.
84 -- All of the line numbers for that file/context are in one numbits.
85 file_id integer, -- foreign key to `file`.
86 context_id integer, -- foreign key to `context`.
87 numbits blob, -- see the numbits functions in coverage.numbits
88 foreign key (file_id) references file (id),
89 foreign key (context_id) references context (id),
90 unique (file_id, context_id)
91);
93CREATE TABLE arc (
94 -- If recording branches, a row per context per from/to line transition executed.
95 file_id integer, -- foreign key to `file`.
96 context_id integer, -- foreign key to `context`.
97 fromno integer, -- line number jumped from.
98 tono integer, -- line number jumped to.
99 foreign key (file_id) references file (id),
100 foreign key (context_id) references context (id),
101 unique (file_id, context_id, fromno, tono)
102);
104CREATE TABLE tracer (
105 -- A row per file indicating the tracer used for that file.
106 file_id integer primary key,
107 tracer text,
108 foreign key (file_id) references file (id)
109);
110"""
113def _locked(method: AnyCallable) -> AnyCallable: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
114 """A decorator for methods that should hold self._lock."""
116 @functools.wraps(method) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
117 def _wrapped(self: CoverageData, *args: Any, **kwargs: Any) -> Any: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
118 if self._debug.should("lock"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
119 self._debug.write(f"Locking {self._lock!r} for {method.__name__}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
120 with self._lock: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
121 if self._debug.should("lock"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
122 self._debug.write(f"Locked {self._lock!r} for {method.__name__}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
123 return method(self, *args, **kwargs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
125 return _wrapped 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
128class NumbitsUnionAgg: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
129 """SQLite aggregate function for computing union of numbits."""
131 def __init__(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
132 self.result = b"" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
134 def step(self, value: bytes) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
135 """Process one value in the aggregation."""
136 self.result = numbits_union(self.result, value) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
138 def finalize(self) -> bytes: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
139 """Return the final aggregated result."""
140 return self.result 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
143class CoverageData: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
144 """Manages collected coverage data, including file storage.
146 This class is the public supported API to the data that coverage.py
147 collects during program execution. It includes information about what code
148 was executed. It does not include information from the analysis phase, to
149 determine what lines could have been executed, or what lines were not
150 executed.
152 .. note::
154 The data file is currently a SQLite database file, with a
155 :ref:`documented schema <dbschema>`. The schema is subject to change
156 though, so be careful about querying it directly. Use this API if you
157 can to isolate yourself from changes.
159 There are a number of kinds of data that can be collected:
161 * **lines**: the line numbers of source lines that were executed.
162 These are always available.
164 * **arcs**: pairs of source and destination line numbers for transitions
165 between source lines. These are only available if branch coverage was
166 used.
168 * **file tracer names**: the module names of the file tracer plugins that
169 handled each file in the data.
171 Lines, arcs, and file tracer names are stored for each source file. File
172 names in this API are case-sensitive, even on platforms with
173 case-insensitive file systems.
175 A data file either stores lines, or arcs, but not both.
177 A data file is associated with the data when the :class:`CoverageData`
178 is created, using the parameters `basename`, `suffix`, and `no_disk`. The
179 base name can be queried with :meth:`base_filename`, and the actual file
180 name being used is available from :meth:`data_filename`.
182 To read an existing coverage.py data file, use :meth:`read`. You can then
183 access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`,
184 or :meth:`file_tracer`.
186 The :meth:`has_arcs` method indicates whether arc data is available. You
187 can get a set of the files in the data with :meth:`measured_files`. As
188 with most Python containers, you can determine if there is any data at all
189 by using this object as a boolean value.
191 The contexts for each line in a file can be read with
192 :meth:`contexts_by_lineno`.
194 To limit querying to certain contexts, use :meth:`set_query_context` or
195 :meth:`set_query_contexts`. These will narrow the focus of subsequent
196 :meth:`lines`, :meth:`arcs`, and :meth:`contexts_by_lineno` calls. The set
197 of all measured context names can be retrieved with
198 :meth:`measured_contexts`.
200 Most data files will be created by coverage.py itself, but you can use
201 methods here to create data files if you like. The :meth:`add_lines`,
202 :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways
203 that are convenient for coverage.py.
205 To record data for contexts, use :meth:`set_context` to set a context to
206 be used for subsequent :meth:`add_lines` and :meth:`add_arcs` calls.
208 To add a source file without any measured data, use :meth:`touch_file`,
209 or :meth:`touch_files` for a list of such files.
211 Write the data to its file with :meth:`write`.
213 You can clear the data in memory with :meth:`erase`. Data for specific
214 files can be removed from the database with :meth:`purge_files`.
216 Two data collections can be combined by using :meth:`update` on one
217 :class:`CoverageData`, passing it the other.
219 Data in a :class:`CoverageData` can be serialized and deserialized with
220 :meth:`dumps` and :meth:`loads`.
222 The methods used during the coverage.py collection phase
223 (:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and
224 :meth:`add_file_tracers`) are thread-safe. Other methods may not be.
226 """
228 def __init__( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(
229 self,
230 basename: FilePath | None = None,
231 suffix: str | bool | None = None,
232 no_disk: bool = False,
233 warn: TWarnFn | None = None,
234 debug: TDebugCtl | None = None,
235 ) -> None:
236 """Create a :class:`CoverageData` object to hold coverage-measured data.
238 Arguments:
239 basename (str): the base name of the data file, defaulting to
240 ".coverage". This can be a path to a file in another directory.
241 suffix (str or bool): has the same meaning as the `data_suffix`
242 argument to :class:`coverage.Coverage`.
243 no_disk (bool): if True, keep all data in memory, and don't
244 write any disk file.
245 warn: a warning callback function, accepting a warning message
246 argument.
247 debug: a `DebugControl` object (optional)
249 """
250 self._no_disk = no_disk 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
251 self._basename = os.path.abspath(basename or ".coverage") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
252 self._suffix = suffix 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
253 self._warn = warn 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
254 self._debug = debug or NoDebugging() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
256 self._choose_filename() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
257 # Maps filenames to row ids.
258 self._file_map: dict[str, int] = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
259 # Maps thread ids to SqliteDb objects.
260 self._dbs: dict[int, SqliteDb] = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
261 self._pid = os.getpid() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
262 # Synchronize the operations used during collection.
263 self._lock = threading.RLock() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
265 # Are we in sync with the data file?
266 self._have_used = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
268 self._has_lines = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
269 self._has_arcs = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
271 self._current_context: str | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
272 self._current_context_id: int | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
273 self._query_context_ids: list[int] | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
275 __repr__ = auto_repr 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
277 def _debug_dataio(self, msg: str, filename: str) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
278 """A helper for debug messages which are all similar."""
279 if self._debug.should("dataio"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
280 self._debug.write(f"{msg} {filename!r} ({file_summary(filename)})") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
282 def _choose_filename(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
283 """Set self._filename based on inited attributes."""
284 if self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
285 self._filename = f"file:coverage-{uuid.uuid4()}?mode=memory&cache=shared" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
286 else:
287 self._filename = self._basename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
288 suffix = filename_suffix(self._suffix) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
289 if suffix: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
290 self._filename += f".{suffix}" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
292 def _reset(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
293 """Reset our attributes."""
294 if not self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
295 self.close() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
296 self._file_map = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
297 self._have_used = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
298 self._current_context_id = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
300 def close(self, force: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
301 """Really close all the database objects."""
302 if self._debug.should("dataio"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
303 self._debug.write(f"Closing dbs, force={force}: {self._dbs}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
304 for db in self._dbs.values(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
305 db.close(force=force) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
306 self._dbs = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
308 def _open_db(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
309 """Open an existing db file, and read its metadata."""
310 self._debug_dataio("Opening data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
311 self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug, self._no_disk) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
312 self._read_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
314 def _read_db(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
315 """Read the metadata from a database so that we are ready to use it."""
316 with self._dbs[threading.get_ident()] as db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
317 try: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
318 row = db.execute_one("select version from coverage_schema") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
319 assert row is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
320 except Exception as exc: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
321 if "no such table: coverage_schema" in str(exc): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
322 self._init_db(db) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
323 else:
324 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
325 "Data file {!r} doesn't seem to be a coverage data file: {}".format(
326 self._filename,
327 exc,
328 ),
329 ) from exc
330 else:
331 schema_version = row[0] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
332 if schema_version != SCHEMA_VERSION: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
333 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
334 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format(
335 self._filename,
336 schema_version,
337 SCHEMA_VERSION,
338 ),
339 )
341 row = db.execute_one("select value from meta where key = 'has_arcs'") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
342 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
343 self._has_arcs = bool(int(row[0])) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
344 self._has_lines = not self._has_arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
346 with db.execute("select id, path from file") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
347 for file_id, path in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
348 self._file_map[path] = file_id 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
350 def _init_db(self, db: SqliteDb) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
351 """Write the initial contents of the database."""
352 self._debug_dataio("Initing data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
353 db.executescript(SCHEMA) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
354 db.execute_void("INSERT INTO coverage_schema (version) VALUES (?)", (SCHEMA_VERSION,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
356 # When writing metadata, avoid information that will needlessly change
357 # the hash of the data file, unless we're debugging processes.
358 meta_data = [ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
359 ("version", __version__),
360 ]
361 if self._debug.should("process"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
362 meta_data.extend( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nF$Q1oG7pH#R2qI6rJ8S3TU4
363 [
364 ("sys_argv", str(getattr(sys, "argv", None))),
365 ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
366 ]
367 )
368 db.executemany_void("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)", meta_data) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
370 def _connect(self) -> SqliteDb: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
371 """Get the SqliteDb object to use."""
372 if threading.get_ident() not in self._dbs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
373 self._open_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
374 return self._dbs[threading.get_ident()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
376 def __bool__(self) -> bool: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
377 if threading.get_ident() not in self._dbs and not os.path.exists(self._filename): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
378 return False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
379 try: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
380 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
381 with con.execute("SELECT * FROM file LIMIT 1") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
382 return bool(list(cur)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
383 except CoverageException: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
384 return False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
386 def dumps(self) -> bytes: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
387 """Serialize the current data to a byte string.
389 The format of the serialized data is not documented. It is only
390 suitable for use with :meth:`loads` in the same version of
391 coverage.py.
393 Note that this serialization is not what gets stored in coverage data
394 files. This method is meant to produce bytes that can be transmitted
395 elsewhere and then deserialized with :meth:`loads`.
397 Returns:
398 A byte string of serialized data.
400 .. versionadded:: 5.0
402 """
403 self._debug_dataio("Dumping data from data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
404 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
405 script = con.dump() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
406 return b"z" + zlib.compress(script.encode("utf-8")) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
408 def loads(self, data: bytes) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
409 """Deserialize data from :meth:`dumps`.
411 Use with a newly-created empty :class:`CoverageData` object. It's
412 undefined what happens if the object already has data in it.
414 Note that this is not for reading data from a coverage data file. It
415 is only for use on data you produced with :meth:`dumps`.
417 Arguments:
418 data: A byte string of serialized data produced by :meth:`dumps`.
420 .. versionadded:: 5.0
422 """
423 self._debug_dataio("Loading data into data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
424 if data[:1] != b"z": 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
425 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
426 f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)",
427 )
428 script = zlib.decompress(data[1:]).decode("utf-8") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
429 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug, self._no_disk) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
430 with db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
431 db.executescript(script) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
432 self._read_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
433 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
435 def _file_id(self, filename: str, add: bool = False) -> int | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
436 """Get the file id for `filename`.
438 If filename is not in the database yet, add it if `add` is True.
439 If `add` is not True, return None.
440 """
441 if filename not in self._file_map: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
442 if add: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
443 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
444 self._file_map[filename] = con.execute_for_rowid( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
445 "INSERT OR REPLACE INTO file (path) VALUES (?)",
446 (filename,),
447 )
448 return self._file_map.get(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
450 def _context_id(self, context: str) -> int | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
451 """Get the id for a context."""
452 assert context is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
453 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
454 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
455 row = con.execute_one("SELECT id FROM context WHERE context = ?", (context,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
456 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
457 return cast(int, row[0]) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
458 else:
459 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
461 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
462 def set_context(self, context: str | None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
463 """Set the current context for future :meth:`add_lines` etc.
465 `context` is a str, the name of the context to use for the next data
466 additions. The context persists until the next :meth:`set_context`.
468 .. versionadded:: 5.0
470 """
471 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
472 self._debug.write(f"Setting coverage context: {context!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
473 self._current_context = context 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
474 self._current_context_id = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
476 def _set_context_id(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
477 """Use the _current_context to set _current_context_id."""
478 context = self._current_context or "" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
479 context_id = self._context_id(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
480 if context_id is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
481 self._current_context_id = context_id 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
482 else:
483 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
484 self._current_context_id = con.execute_for_rowid( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
485 "INSERT INTO context (context) VALUES (?)",
486 (context,),
487 )
489 def base_filename(self) -> str: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
490 """The base filename for storing data.
492 .. versionadded:: 5.0
494 """
495 return self._basename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
497 def data_filename(self) -> str: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
498 """Where is the data stored?
500 .. versionadded:: 5.0
502 """
503 return self._filename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
505 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
506 def add_lines(self, line_data: Mapping[str, Collection[TLineNo]]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
507 """Add measured line data.
509 `line_data` is a dictionary mapping file names to iterables of ints::
511 { filename: { line1, line2, ... }, ...}
513 """
514 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
515 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
516 "Adding lines: %d files, %d lines total"
517 % (
518 len(line_data),
519 sum(len(lines) for lines in line_data.values()),
520 )
521 )
522 if self._debug.should("dataop2"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4
523 for filename, linenos in sorted(line_data.items()): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4
524 self._debug.write(f" {filename}: {linenos}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4
525 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
526 self._choose_lines_or_arcs(lines=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
527 if not line_data: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
528 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0)mE9nFQ1%oG7pHR2'qI6rJS3(TU4
529 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
530 self._set_context_id() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
531 for filename, linenos in line_data.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
532 line_bits = nums_to_numbits(linenos) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
533 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
534 query = "SELECT numbits FROM line_bits WHERE file_id = ? AND context_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
535 with con.execute(query, (file_id, self._current_context_id)) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
536 existing = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
537 if existing: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
538 line_bits = numbits_union(line_bits, existing[0][0]) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
540 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
541 """
542 INSERT OR REPLACE INTO line_bits
543 (file_id, context_id, numbits) VALUES (?, ?, ?)
544 """,
545 (file_id, self._current_context_id, line_bits),
546 )
548 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
549 def add_arcs(self, arc_data: Mapping[str, Collection[TArc]]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
550 """Add measured arc data.
552 `arc_data` is a dictionary mapping file names to iterables of pairs of
553 ints::
555 { filename: { (l1,l2), (l1,l2), ... }, ...}
557 """
558 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
559 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
560 "Adding arcs: %d files, %d arcs total"
561 % (
562 len(arc_data),
563 sum(len(arcs) for arcs in arc_data.values()),
564 )
565 )
566 if self._debug.should("dataop2"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
567 for filename, arcs in sorted(arc_data.items()): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
568 self._debug.write(f" {filename}: {arcs}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
569 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
570 self._choose_lines_or_arcs(arcs=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
571 if not arc_data: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
572 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClD!P0mEnF$Q1%oGpHR2'qIrJ8S3(TU4
573 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
574 self._set_context_id() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
575 for filename, arcs in arc_data.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
576 if not arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
577 continue 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
578 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
579 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
580 con.executemany_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
581 """
582 INSERT OR IGNORE INTO arc
583 (file_id, context_id, fromno, tono) VALUES (?, ?, ?, ?)
584 """,
585 data,
586 )
588 def _choose_lines_or_arcs(self, lines: bool = False, arcs: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
589 """Force the data file to choose between lines and arcs."""
590 assert lines or arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
591 assert not (lines and arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
592 if lines and self._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
593 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
594 self._debug.write("Error: Can't add line measurements to existing branch data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
595 raise DataError("Can't add line measurements to existing branch data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
596 if arcs and self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
597 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
598 self._debug.write("Error: Can't add branch measurements to existing line data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
599 raise DataError("Can't add branch measurements to existing line data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
600 if not self._has_arcs and not self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
601 self._has_lines = lines 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
602 self._has_arcs = arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
603 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
604 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
605 "INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)",
606 ("has_arcs", str(int(arcs))),
607 )
609 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
610 def add_file_tracers(self, file_tracers: Mapping[str, str]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
611 """Add per-file plugin information.
613 `file_tracers` is { filename: plugin_name, ... }
615 """
616 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
617 self._debug.write(f"Adding file tracers: {len(file_tracers)} files") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
618 if not file_tracers: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
619 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
620 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
621 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
622 for filename, plugin_name in file_tracers.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
623 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
624 existing_plugin = self.file_tracer(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
625 if existing_plugin: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
626 if existing_plugin != plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
627 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
628 "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
629 filename,
630 existing_plugin,
631 plugin_name,
632 ),
633 )
634 elif plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
635 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
636 "INSERT INTO TRACER (file_id, tracer) VALUES (?, ?)",
637 (file_id, plugin_name),
638 )
640 def touch_file(self, filename: str, plugin_name: str = "") -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
641 """Ensure that `filename` appears in the data, empty if needed.
643 `plugin_name` is the name of the plugin responsible for this file.
644 It is used to associate the right filereporter, etc.
645 """
646 self.touch_files([filename], plugin_name) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
648 def touch_files(self, filenames: Collection[str], plugin_name: str | None = None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
649 """Ensure that `filenames` appear in the data, empty if needed.
651 `plugin_name` is the name of the plugin responsible for these files.
652 It is used to associate the right filereporter, etc.
653 """
654 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
655 self._debug.write(f"Touching {filenames!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qIrJS3TU4
656 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
657 with self._connect(): # Use this to get one transaction. 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
658 if not self._has_arcs and not self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
659 raise DataError("Can't touch files in an empty CoverageData") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4
661 for filename in filenames: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
662 self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
663 if plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
664 # Set the tracer for this file
665 self.add_file_tracers({filename: plugin_name}) 1abKcdLefMghNijOklPmnQopRqrS
667 def purge_files(self, filenames: Collection[str]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
668 """Purge any existing coverage data for the given `filenames`.
670 .. versionadded:: 7.2
672 """
673 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
674 self._debug.write(f"Purging data for {filenames!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
675 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
676 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
677 if self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
678 sql = "DELETE FROM line_bits WHERE file_id=?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
679 elif self._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
680 sql = "DELETE FROM arc WHERE file_id=?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
681 else:
682 raise DataError("Can't purge files in an empty CoverageData") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
684 for filename in filenames: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
685 file_id = self._file_id(filename, add=False) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
686 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
687 continue 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
688 con.execute_void(sql, (file_id,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
690 def update( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(
691 self,
692 other_data: CoverageData,
693 map_path: Callable[[str], str] | None = None,
694 ) -> None:
695 """Update this data with data from another :class:`CoverageData`.
697 If `map_path` is provided, it's a function that re-map paths to match
698 the local machine's. Note: `map_path` is None only when called
699 directly from the test suite.
701 """
702 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
703 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qIrJS3TU4
704 "Updating with data from {!r}".format(
705 getattr(other_data, "_filename", "???"),
706 )
707 )
708 if self._has_lines and other_data._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
709 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4
710 "Can't combine branch coverage data with statement data", slug="cant-combine"
711 )
712 if self._has_arcs and other_data._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
713 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4
714 "Can't combine statement coverage data with branch data", slug="cant-combine"
715 )
717 map_path = map_path or (lambda p: p) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
719 # Force the database we're writing to to exist before we start nesting contexts.
720 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
721 other_data.read() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
723 # Ensure other_data has a properly initialized database
724 with other_data._connect(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
725 pass 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
727 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
728 assert con.con is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
729 con.con.isolation_level = "IMMEDIATE" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
731 # Register functions for SQLite
732 con.con.create_function("numbits_union", 2, numbits_union) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
733 con.con.create_function("map_path", 1, map_path) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
734 con.con.create_aggregate( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
735 "numbits_union_agg",
736 1,
737 NumbitsUnionAgg, # type: ignore[arg-type]
738 )
740 # Attach the other database
741 con.execute_void("ATTACH DATABASE ? AS other_db", (other_data.data_filename(),)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
743 # Create temporary table with mapped file paths to avoid repeated map_path() calls
744 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
745 CREATE TEMP TABLE other_file_mapped AS
746 SELECT
747 other_file.id as other_file_id,
748 map_path(other_file.path) as mapped_path
749 FROM other_db.file AS other_file
750 """)
752 # Check for tracer conflicts before proceeding
753 with con.execute(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
754 SELECT other_file_mapped.mapped_path,
755 COALESCE(main.tracer.tracer, ''),
756 COALESCE(other_db.tracer.tracer, '')
757 FROM main.file
758 LEFT JOIN main.tracer ON main.file.id = main.tracer.file_id
759 INNER JOIN other_file_mapped ON main.file.path = other_file_mapped.mapped_path
760 LEFT JOIN other_db.tracer ON other_file_mapped.other_file_id = other_db.tracer.file_id
761 WHERE COALESCE(main.tracer.tracer, '') != COALESCE(other_db.tracer.tracer, '')
762 """) as cur:
763 conflicts = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
764 if conflicts: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
765 path, this_tracer, other_tracer = conflicts[0] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4
766 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4
767 "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
768 path,
769 this_tracer,
770 other_tracer,
771 ),
772 )
774 # Insert missing files from other_db (with map_path applied)
775 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
776 INSERT OR IGNORE INTO main.file (path)
777 SELECT DISTINCT mapped_path FROM other_file_mapped
778 """)
780 # Insert missing contexts from other_db
781 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
782 INSERT OR IGNORE INTO main.context (context)
783 SELECT context FROM other_db.context
784 """)
786 # Update file_map with any new files
787 with con.execute("SELECT id, path FROM file") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
788 self._file_map.update({path: id for id, path in cur}) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
790 with con.execute(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
791 SELECT
792 EXISTS(SELECT 1 FROM other_db.arc),
793 EXISTS(SELECT 1 FROM other_db.line_bits)
794 """) as cur:
795 has_arcs, has_lines = cur.fetchone() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
797 # Handle arcs if present in other_db
798 if has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
799 self._choose_lines_or_arcs(arcs=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
801 # Create context mapping table for faster lookups
802 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
803 CREATE TEMP TABLE context_mapping AS
804 SELECT
805 other_context.id as other_id,
806 main_context.id as main_id
807 FROM other_db.context AS other_context
808 INNER JOIN main.context AS main_context ON other_context.context = main_context.context
809 """)
811 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
812 INSERT OR IGNORE INTO main.arc (file_id, context_id, fromno, tono)
813 SELECT
814 main_file.id,
815 context_mapping.main_id,
816 other_arc.fromno,
817 other_arc.tono
818 FROM other_db.arc AS other_arc
819 INNER JOIN other_file_mapped ON other_arc.file_id = other_file_mapped.other_file_id
820 INNER JOIN context_mapping ON other_arc.context_id = context_mapping.other_id
821 INNER JOIN main.file AS main_file ON other_file_mapped.mapped_path = main_file.path
822 """)
824 # Handle line_bits if present in other_db
825 if has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
826 self._choose_lines_or_arcs(lines=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
828 # Handle line_bits by aggregating other_db data by mapped target,
829 # then inserting/updating
830 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
831 INSERT OR REPLACE INTO main.line_bits (file_id, context_id, numbits)
832 SELECT
833 main_file.id,
834 main_context.id,
835 numbits_union(
836 COALESCE((
837 SELECT numbits FROM main.line_bits
838 WHERE file_id = main_file.id AND context_id = main_context.id
839 ), X''),
840 aggregated.combined_numbits
841 )
842 FROM (
843 SELECT
844 other_file_mapped.mapped_path,
845 other_context.context,
846 numbits_union_agg(other_line_bits.numbits) as combined_numbits
847 FROM other_db.line_bits AS other_line_bits
848 INNER JOIN other_file_mapped ON other_line_bits.file_id = other_file_mapped.other_file_id
849 INNER JOIN other_db.context AS other_context ON other_line_bits.context_id = other_context.id
850 GROUP BY other_file_mapped.mapped_path, other_context.context
851 ) AS aggregated
852 INNER JOIN main.file AS main_file ON aggregated.mapped_path = main_file.path
853 INNER JOIN main.context AS main_context ON aggregated.context = main_context.context
854 """)
856 # Insert tracers from other_db (avoiding conflicts we already checked)
857 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
858 INSERT OR IGNORE INTO main.tracer (file_id, tracer)
859 SELECT
860 main_file.id,
861 other_tracer.tracer
862 FROM other_db.tracer AS other_tracer
863 INNER JOIN other_file_mapped ON other_tracer.file_id = other_file_mapped.other_file_id
864 INNER JOIN main.file AS main_file ON other_file_mapped.mapped_path = main_file.path
865 """)
867 if not self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
868 # Update all internal cache data.
869 self._reset() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
870 self.read() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
872 def erase(self, parallel: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
873 """Erase the data in this object.
875 If `parallel` is true, then also deletes data files created from the
876 basename by parallel-mode.
878 """
879 self._reset() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
880 if self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
881 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
882 self._debug_dataio("Erasing data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
883 file_be_gone(self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
884 if parallel: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
885 data_dir, local = os.path.split(self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
886 local_abs_path = os.path.join(os.path.abspath(data_dir), local) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
887 pattern = glob.escape(local_abs_path) + ".*" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
888 for filename in glob.glob(pattern): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
889 self._debug_dataio("Erasing parallel data file", filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
890 file_be_gone(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
892 def read(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
893 """Start using an existing data file."""
894 if os.path.exists(self._filename): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
895 with self._connect(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
896 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
898 def write(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
899 """Ensure the data is written to the data file."""
900 self._debug_dataio("Writing (no-op) data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
902 def _start_using(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
903 """Call this before using the database at all."""
904 if self._pid != os.getpid(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
905 # Looks like we forked! Have to start a new data file.
906 self._reset() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU
907 self._choose_filename() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU
908 self._pid = os.getpid() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU
909 if not self._have_used: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
910 self.erase() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
911 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
913 def has_arcs(self) -> bool: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
914 """Does the database have arcs (True) or lines (False)."""
915 return bool(self._has_arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
917 def measured_files(self) -> set[str]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
918 """A set of all files that have been measured.
920 Note that a file may be mentioned as measured even though no lines or
921 arcs for that file are present in the data.
923 """
924 return set(self._file_map) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
926 def measured_contexts(self) -> set[str]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
927 """A set of all contexts that have been measured.
929 .. versionadded:: 5.0
931 """
932 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
933 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
934 with con.execute("SELECT DISTINCT(context) FROM context") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
935 contexts = {row[0] for row in cur} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
936 return contexts 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
938 def file_tracer(self, filename: str) -> str | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
939 """Get the plugin name of the file tracer for a file.
941 Returns the name of the plugin that handles this file. If the file was
942 measured, but didn't use a plugin, then "" is returned. If the file
943 was not measured, then None is returned.
945 """
946 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
947 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
948 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
949 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
950 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4
951 row = con.execute_one("SELECT tracer FROM tracer WHERE file_id = ?", (file_id,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
952 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
953 return row[0] or "" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4
954 return "" # File was measured, but no tracer associated. 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
956 def set_query_context(self, context: str) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
957 """Set a context for subsequent querying.
959 The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno`
960 calls will be limited to only one context. `context` is a string which
961 must match a context exactly. If it does not, no exception is raised,
962 but queries will return no data.
964 .. versionadded:: 5.0
966 """
967 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
968 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
969 with con.execute("SELECT id FROM context WHERE context = ?", (context,)) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
970 self._query_context_ids = [row[0] for row in cur.fetchall()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
972 def set_query_contexts(self, contexts: Sequence[str] | None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
973 """Set a number of contexts for subsequent querying.
975 The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno`
976 calls will be limited to the specified contexts. `contexts` is a list
977 of Python regular expressions. Contexts will be matched using
978 :func:`re.search <python:re.search>`. Data will be included in query
979 results if they are part of any of the contexts matched.
981 .. versionadded:: 5.0
983 """
984 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
985 if contexts: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
986 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4
987 context_clause = " or ".join(["context REGEXP ?"] * len(contexts)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4
988 with con.execute("SELECT id FROM context WHERE " + context_clause, contexts) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4
989 self._query_context_ids = [row[0] for row in cur.fetchall()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4
990 else:
991 self._query_context_ids = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
993 def lines(self, filename: str) -> list[TLineNo] | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
994 """Get the list of lines executed for a source file.
996 If the file was not measured, returns None. A file might be measured,
997 and have no lines executed, in which case an empty list is returned.
999 If the file was executed, returns a list of integers, the line numbers
1000 executed in the file. The list is in no particular order.
1002 """
1003 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1004 if self.has_arcs(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1005 arcs = self.arcs(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1006 if arcs is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1007 all_lines = itertools.chain.from_iterable(arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1008 return list({l for l in all_lines if l > 0}) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1010 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1011 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1012 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1013 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
1014 else:
1015 query = "SELECT numbits FROM line_bits WHERE file_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1016 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1017 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1018 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4
1019 query += " AND context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4
1020 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4
1021 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1022 bitmaps = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1023 nums = set() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1024 for row in bitmaps: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1025 nums.update(numbits_to_nums(row[0])) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1026 return list(nums) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1028 def arcs(self, filename: str) -> list[TArc] | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1029 """Get the list of arcs executed for a file.
1031 If the file was not measured, returns None. A file might be measured,
1032 and have no arcs executed, in which case an empty list is returned.
1034 If the file was executed, returns a list of 2-tuples of integers. Each
1035 pair is a starting line number and an ending line number for a
1036 transition from one line to another. The list is in no particular
1037 order.
1039 Negative numbers have special meaning. If the starting line number is
1040 -N, it represents an entry to the code object that starts at line N.
1041 If the ending ling number is -N, it's an exit from the code object that
1042 starts at line N.
1044 """
1045 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1046 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1047 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1048 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1049 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
1050 else:
1051 query = "SELECT DISTINCT fromno, tono FROM arc WHERE file_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1052 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1053 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1054 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4
1055 query += " AND context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4
1056 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4
1057 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1058 return list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1060 def contexts_by_lineno(self, filename: str) -> dict[TLineNo, list[str]]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1061 """Get the contexts for each line in a file.
1063 Returns:
1064 A dict mapping line numbers to a list of context names.
1066 .. versionadded:: 5.0
1068 """
1069 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1070 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1071 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1072 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1073 return {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1075 lineno_contexts_map = collections.defaultdict(set) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1076 if self.has_arcs(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1077 query = """ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1078 SELECT arc.fromno, arc.tono, context.context
1079 FROM arc, context
1080 WHERE arc.file_id = ? AND arc.context_id = context.id
1081 """
1082 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1083 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1084 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1085 query += " AND arc.context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1086 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1087 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1088 for fromno, tono, context in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1089 if fromno > 0: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1090 lineno_contexts_map[fromno].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1091 if tono > 0: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1092 lineno_contexts_map[tono].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1093 else:
1094 query = """ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1095 SELECT l.numbits, c.context FROM line_bits l, context c
1096 WHERE l.context_id = c.id
1097 AND file_id = ?
1098 """
1099 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1100 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1101 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1102 query += " AND l.context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1103 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1104 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1105 for numbits, context in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1106 for lineno in numbits_to_nums(numbits): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1107 lineno_contexts_map[lineno].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1109 return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1111 @classmethod 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1112 def sys_info(cls) -> list[tuple[str, Any]]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1113 """Our information for `Coverage.sys_info`.
1115 Returns a list of (key, value) pairs.
1117 """
1118 with SqliteDb(":memory:", debug=NoDebugging()) as db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1119 with db.execute("PRAGMA temp_store") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1120 temp_store = [row[0] for row in cur] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1121 with db.execute("PRAGMA compile_options") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1122 copts = [row[0] for row in cur] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1123 copts = textwrap.wrap(", ".join(copts), width=75) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1125 return [ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1126 ("sqlite3_sqlite_version", sqlite3.sqlite_version),
1127 ("sqlite3_temp_store", temp_store),
1128 ("sqlite3_compile_options", copts),
1129 ]
1132def filename_suffix(suffix: str | bool | None) -> str | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1133 """Compute a filename suffix for a data file.
1135 If `suffix` is a string or None, simply return it. If `suffix` is True,
1136 then build a suffix incorporating the hostname, process id, and a random
1137 number.
1139 Returns a string or None.
1141 """
1142 if suffix is True: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1143 # If data_suffix was a simple true value, then make a suffix with
1144 # plenty of distinguishing information. We do this here in
1145 # `save()` at the last minute so that the pid will be correct even
1146 # if the process forks.
1147 die = random.Random(os.urandom(8)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1148 letters = string.ascii_uppercase + string.ascii_lowercase 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1149 rolls = "".join(die.choice(letters) for _ in range(6)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1150 suffix = f"{socket.gethostname()}.{os.getpid()}.X{rolls}x" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1151 elif suffix is False: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4
1152 suffix = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4
1153 return suffix 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4