pytest-testmon 2.1.1__tar.gz → 2.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest-testmon-2.1.1/pytest_testmon.egg-info → pytest_testmon-2.1.3}/PKG-INFO +1 -1
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pyproject.toml +1 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3/pytest_testmon.egg-info}/PKG-INFO +1 -1
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/__init__.py +1 -1
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/common.py +42 -5
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/configure.py +1 -1
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/db.py +43 -32
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/process_code.py +24 -10
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/pytest_testmon.py +37 -22
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/testmon_core.py +68 -23
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/tox_testmon.py +1 -3
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/LICENSE +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/MANIFEST.in +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/README.md +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/SOURCES.txt +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/dependency_links.txt +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/entry_points.txt +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/requires.txt +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/top_level.txt +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/setup.cfg +0 -0
- {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/setup.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""PYTEST_DONT_REWRITE"""
|
|
2
|
-
TESTMON_VERSION = "2.1.
|
|
2
|
+
TESTMON_VERSION = "2.1.3"
|
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
|
+
# Python >= 3.8
|
|
6
7
|
import importlib.metadata
|
|
7
8
|
|
|
8
9
|
def get_system_packages_raw():
|
|
@@ -12,14 +13,46 @@ try:
|
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
except ImportError:
|
|
16
|
+
# Python < 3.7
|
|
15
17
|
import pkg_resources
|
|
16
18
|
|
|
17
19
|
def get_system_packages_raw():
|
|
18
|
-
return (
|
|
20
|
+
return (
|
|
21
|
+
(pkg.project_name, pkg.version)
|
|
22
|
+
for pkg in pkg_resources.working_set # pylint: disable=not-an-iterable
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
from pathlib import Path
|
|
22
27
|
|
|
28
|
+
from typing import TypedDict, List, Dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileFp(TypedDict):
|
|
32
|
+
filename: str
|
|
33
|
+
method_checksums: List[int] = None
|
|
34
|
+
mtime: float = None # optimization helper, not really a part of the data structure fundamentally
|
|
35
|
+
fsha: int = None # optimization helper, not really a part of the data structure fundamentally
|
|
36
|
+
fingerprint_id: int = None # optimization helper,
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
TestName = str
|
|
40
|
+
|
|
41
|
+
TestFileFps = Dict[TestName, List[FileFp]]
|
|
42
|
+
|
|
43
|
+
Duration = float
|
|
44
|
+
Failed = bool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DepsNOutcomes(TypedDict):
|
|
48
|
+
deps: List[FileFp]
|
|
49
|
+
failed: Failed
|
|
50
|
+
duration: Duration
|
|
51
|
+
forced: bool = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
TestExecutions = Dict[TestName, DepsNOutcomes]
|
|
55
|
+
|
|
23
56
|
|
|
24
57
|
def dummy():
|
|
25
58
|
pass
|
|
@@ -30,6 +63,7 @@ def get_logger(name):
|
|
|
30
63
|
handler = logging.StreamHandler()
|
|
31
64
|
handler.setFormatter(formatter)
|
|
32
65
|
|
|
66
|
+
# Configure the logger
|
|
33
67
|
tm_logger = logging.getLogger(name)
|
|
34
68
|
tm_logger.setLevel(logging.INFO)
|
|
35
69
|
tm_logger.addHandler(handler)
|
|
@@ -55,16 +89,19 @@ def get_system_packages(ignore=None):
|
|
|
55
89
|
|
|
56
90
|
def drop_patch_version(system_packages):
|
|
57
91
|
return re.sub(
|
|
58
|
-
r"\b([\w_-]+\s\d+\.\d+)\.\w+\b",
|
|
92
|
+
r"\b([\w_-]+\s\d+\.\d+)\.\w+\b", # extract (Package M.N).P / drop .patch
|
|
59
93
|
r"\1",
|
|
60
94
|
system_packages,
|
|
61
95
|
)
|
|
62
96
|
|
|
63
97
|
|
|
64
|
-
|
|
98
|
+
#
|
|
99
|
+
# .git utilities
|
|
100
|
+
#
|
|
101
|
+
def git_path(start_path=None): # parent dirs only
|
|
65
102
|
start_path = Path(start_path or os.getcwd()).resolve()
|
|
66
103
|
current_path = start_path
|
|
67
|
-
while current_path != current_path.parent:
|
|
104
|
+
while current_path != current_path.parent: # '/'.parent == '/'
|
|
68
105
|
path = current_path / ".git"
|
|
69
106
|
if path.exists() and path.is_dir():
|
|
70
107
|
return str(path)
|
|
@@ -81,7 +118,7 @@ def git_current_branch(path=None):
|
|
|
81
118
|
with open(git_head_file, "r", encoding="utf8") as head_file:
|
|
82
119
|
head_content = head_file.read().strip()
|
|
83
120
|
if head_content.startswith("ref:"):
|
|
84
|
-
return head_content.split("/")[-1]
|
|
121
|
+
return head_content.split("/")[-1] # e.g. ref: refs/heads/master
|
|
85
122
|
except FileNotFoundError:
|
|
86
123
|
pass
|
|
87
124
|
return None
|
|
@@ -7,6 +7,8 @@ from functools import lru_cache
|
|
|
7
7
|
|
|
8
8
|
from testmon.process_code import blob_to_checksums, checksums_to_blob
|
|
9
9
|
|
|
10
|
+
from testmon.common import TestExecutions
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
DATA_VERSION = 13
|
|
12
14
|
|
|
@@ -34,7 +36,9 @@ def connection_options(connection):
|
|
|
34
36
|
return connection
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
def check_fingerprint_db(
|
|
39
|
+
def check_fingerprint_db(
|
|
40
|
+
files_methods_checksums, file_name, fingerprint: ChangedFileData
|
|
41
|
+
): # filename name method_checksums id failed
|
|
38
42
|
if file_name in files_methods_checksums and files_methods_checksums[file_name]:
|
|
39
43
|
if set(fingerprint) - set(files_methods_checksums[file_name]):
|
|
40
44
|
return False
|
|
@@ -55,7 +59,7 @@ def check_data_version(connection, datafile, data_version):
|
|
|
55
59
|
return connection, True
|
|
56
60
|
|
|
57
61
|
|
|
58
|
-
class DB:
|
|
62
|
+
class DB: # pylint: disable=too-many-public-methods
|
|
59
63
|
def __init__(self, datafile, readonly=False):
|
|
60
64
|
self._readonly = readonly
|
|
61
65
|
file_exists = os.path.exists(datafile)
|
|
@@ -84,10 +88,10 @@ class DB:
|
|
|
84
88
|
def __exit__(self, *args, **kwargs):
|
|
85
89
|
self.con.__exit__(*args, **kwargs)
|
|
86
90
|
|
|
87
|
-
def _test_execution_fk_column(self):
|
|
91
|
+
def _test_execution_fk_column(self) -> str:
|
|
88
92
|
return "environment_id"
|
|
89
93
|
|
|
90
|
-
def _test_execution_fk_table(self):
|
|
94
|
+
def _test_execution_fk_table(self) -> str:
|
|
91
95
|
return "environment"
|
|
92
96
|
|
|
93
97
|
def update_mtimes(self, new_mtimes):
|
|
@@ -96,7 +100,9 @@ class DB:
|
|
|
96
100
|
"UPDATE file_fp SET mtime=?, fsha=? WHERE id = ?", new_mtimes
|
|
97
101
|
)
|
|
98
102
|
|
|
99
|
-
def finish_execution(
|
|
103
|
+
def finish_execution(
|
|
104
|
+
self, exec_id, duration=None, select=True
|
|
105
|
+
): # pylint: disable=unused-argument
|
|
100
106
|
self.update_saving_stats(exec_id, select)
|
|
101
107
|
self.fetch_or_create_file_fp.cache_clear()
|
|
102
108
|
with self.con as con:
|
|
@@ -187,7 +193,9 @@ class DB:
|
|
|
187
193
|
)
|
|
188
194
|
|
|
189
195
|
@lru_cache(1000)
|
|
190
|
-
def fetch_or_create_file_fp(
|
|
196
|
+
def fetch_or_create_file_fp(
|
|
197
|
+
self, filename, fsha, method_checksums
|
|
198
|
+
): # pylint: disable=R0801
|
|
191
199
|
cursor = self.con.cursor()
|
|
192
200
|
try:
|
|
193
201
|
cursor.execute(
|
|
@@ -200,7 +208,7 @@ class DB:
|
|
|
200
208
|
)
|
|
201
209
|
|
|
202
210
|
fingerprint_id = cursor.lastrowid
|
|
203
|
-
except sqlite3.IntegrityError:
|
|
211
|
+
except sqlite3.IntegrityError: # rather fetching existing fingerprint
|
|
204
212
|
fingerprint_id, *_ = cursor.execute(
|
|
205
213
|
"""
|
|
206
214
|
SELECT
|
|
@@ -215,15 +223,15 @@ class DB:
|
|
|
215
223
|
|
|
216
224
|
return fingerprint_id
|
|
217
225
|
|
|
218
|
-
def _insert_test_execution(
|
|
226
|
+
def _insert_test_execution( # pylint: disable=too-many-arguments
|
|
219
227
|
self,
|
|
220
228
|
con,
|
|
221
|
-
exec_id,
|
|
222
|
-
test_name,
|
|
223
|
-
duration,
|
|
224
|
-
failed,
|
|
225
|
-
forced,
|
|
226
|
-
):
|
|
229
|
+
exec_id: int,
|
|
230
|
+
test_name: "str",
|
|
231
|
+
duration: "float",
|
|
232
|
+
failed: "bool",
|
|
233
|
+
forced: bool,
|
|
234
|
+
) -> int:
|
|
227
235
|
cursor = con.cursor()
|
|
228
236
|
cursor.execute(
|
|
229
237
|
f"""
|
|
@@ -241,7 +249,7 @@ class DB:
|
|
|
241
249
|
)
|
|
242
250
|
return cursor.lastrowid
|
|
243
251
|
|
|
244
|
-
def insert_test_file_fps(self, tests_deps_n_outcomes, exec_id=None):
|
|
252
|
+
def insert_test_file_fps(self, tests_deps_n_outcomes: TestExecutions, exec_id=None):
|
|
245
253
|
assert exec_id
|
|
246
254
|
with self.con as con:
|
|
247
255
|
cursor = con.cursor()
|
|
@@ -306,7 +314,7 @@ class DB:
|
|
|
306
314
|
)
|
|
307
315
|
result = cursor.fetchone()
|
|
308
316
|
if result:
|
|
309
|
-
return json.loads(result[0])
|
|
317
|
+
return json.loads(result[0]) # zlib.decompress(result[0]).decode('utf-8)'))
|
|
310
318
|
return default
|
|
311
319
|
|
|
312
320
|
def increment_attributes(self, attributes_to_increment, exec_id=None):
|
|
@@ -329,10 +337,10 @@ class DB:
|
|
|
329
337
|
],
|
|
330
338
|
)
|
|
331
339
|
|
|
332
|
-
def _create_metadata_statement(self):
|
|
340
|
+
def _create_metadata_statement(self) -> str:
|
|
333
341
|
return """CREATE TABLE metadata (dataid TEXT PRIMARY KEY, data TEXT);"""
|
|
334
342
|
|
|
335
|
-
def _create_environment_statement(self):
|
|
343
|
+
def _create_environment_statement(self) -> str:
|
|
336
344
|
return """
|
|
337
345
|
CREATE TABLE environment (
|
|
338
346
|
id INTEGER PRIMARY KEY ASC,
|
|
@@ -342,7 +350,7 @@ class DB:
|
|
|
342
350
|
UNIQUE (environment_name, system_packages, python_version)
|
|
343
351
|
);"""
|
|
344
352
|
|
|
345
|
-
def _create_test_execution_statement(self):
|
|
353
|
+
def _create_test_execution_statement(self) -> str: # pylint: disable=invalid-name
|
|
346
354
|
return f"""
|
|
347
355
|
CREATE TABLE test_execution (
|
|
348
356
|
id INTEGER PRIMARY KEY ASC,
|
|
@@ -355,10 +363,10 @@ class DB:
|
|
|
355
363
|
CREATE INDEX test_execution_fk_name ON test_execution ({self._test_execution_fk_column()}, test_name);
|
|
356
364
|
"""
|
|
357
365
|
|
|
358
|
-
def _create_temp_tables_statement(self):
|
|
366
|
+
def _create_temp_tables_statement(self) -> str:
|
|
359
367
|
return ""
|
|
360
368
|
|
|
361
|
-
def _local_temp_tables_statement(self):
|
|
369
|
+
def _local_temp_tables_statement(self) -> str:
|
|
362
370
|
return """
|
|
363
371
|
CREATE TEMPORARY TABLE changed_files_fshas (exec_id INTEGER, filename TEXT, fsha TEXT);
|
|
364
372
|
CREATE INDEX changed_files_fshas_mcall ON changed_files_fshas (exec_id, filename, fsha);
|
|
@@ -367,7 +375,7 @@ class DB:
|
|
|
367
375
|
CREATE INDEX changed_files_mhashes_eid ON changed_files_mhashes (exec_id);
|
|
368
376
|
"""
|
|
369
377
|
|
|
370
|
-
def _create_file_fp_statement(self):
|
|
378
|
+
def _create_file_fp_statement(self) -> str:
|
|
371
379
|
return """
|
|
372
380
|
CREATE TABLE file_fp
|
|
373
381
|
(
|
|
@@ -379,9 +387,9 @@ class DB:
|
|
|
379
387
|
UNIQUE (filename, fsha, method_checksums)
|
|
380
388
|
);"""
|
|
381
389
|
|
|
382
|
-
def _create_test_execution_ffp_statement(
|
|
390
|
+
def _create_test_execution_ffp_statement( # pylint: disable=invalid-name
|
|
383
391
|
self,
|
|
384
|
-
):
|
|
392
|
+
) -> str:
|
|
385
393
|
return """
|
|
386
394
|
CREATE TABLE test_execution_file_fp (
|
|
387
395
|
test_execution_id INTEGER,
|
|
@@ -414,7 +422,7 @@ class DB:
|
|
|
414
422
|
|
|
415
423
|
connection.execute(f"PRAGMA user_version = {self.version_compatibility()}")
|
|
416
424
|
|
|
417
|
-
def fetch_changed_file_data(self, changed_fingerprints, exec_id):
|
|
425
|
+
def fetch_changed_file_data(self, changed_fingerprints, exec_id) -> []:
|
|
418
426
|
in_clause_questionsmarks = ", ".join("?" * len(changed_fingerprints))
|
|
419
427
|
result = []
|
|
420
428
|
for row in self.con.execute(
|
|
@@ -451,7 +459,9 @@ class DB:
|
|
|
451
459
|
|
|
452
460
|
return result
|
|
453
461
|
|
|
454
|
-
def fetch_unknown_files(
|
|
462
|
+
def fetch_unknown_files(
|
|
463
|
+
self, files_fshas, exec_id
|
|
464
|
+
) -> []: # exec_id is environment_id in this module
|
|
455
465
|
with self.con as con:
|
|
456
466
|
con.execute("DELETE FROM changed_files_fshas WHERE exec_id = ?", (exec_id,))
|
|
457
467
|
con.executemany(
|
|
@@ -598,6 +608,7 @@ class DB:
|
|
|
598
608
|
|
|
599
609
|
return [row[0] for row in cursor]
|
|
600
610
|
|
|
611
|
+
# TODO unify with filenames? Restrict not to go into ancient history, but not miss combinations?
|
|
601
612
|
def all_filenames(self):
|
|
602
613
|
cursor = self.con.execute(
|
|
603
614
|
"""
|
|
@@ -685,13 +696,13 @@ class DB:
|
|
|
685
696
|
|
|
686
697
|
return environment_id, packages_changed
|
|
687
698
|
|
|
688
|
-
def initiate_execution(
|
|
699
|
+
def initiate_execution( # pylint: disable= R0913 W0613
|
|
689
700
|
self,
|
|
690
|
-
environment_name,
|
|
691
|
-
system_packages,
|
|
692
|
-
python_version,
|
|
693
|
-
execution_metadata,
|
|
694
|
-
):
|
|
701
|
+
environment_name: str,
|
|
702
|
+
system_packages: str,
|
|
703
|
+
python_version: str,
|
|
704
|
+
execution_metadata: dict, # pylint: disable=unused-argument
|
|
705
|
+
) -> [int, list]: # exec_id # changed_file_data # future_string2
|
|
695
706
|
exec_id, packages_changed = self.fetch_or_create_environment(
|
|
696
707
|
environment_name, system_packages, python_version
|
|
697
708
|
)
|
|
@@ -31,7 +31,7 @@ def debug_blob_to_code(blob):
|
|
|
31
31
|
return blob.split(";\n")
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def methods_to_checksums(blocks):
|
|
34
|
+
def methods_to_checksums(blocks) -> [int]:
|
|
35
35
|
checksums = []
|
|
36
36
|
for block in blocks:
|
|
37
37
|
checksums.append(to_signed(zlib.crc32(block.encode("UTF-8"))))
|
|
@@ -39,7 +39,7 @@ def methods_to_checksums(blocks):
|
|
|
39
39
|
return checksums
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def checksums_to_blob(checksums):
|
|
42
|
+
def checksums_to_blob(checksums: [int]) -> sqlite3.Binary:
|
|
43
43
|
blob = array(CHECKUMS_ARRAY_TYPE, checksums)
|
|
44
44
|
data = blob.tobytes()
|
|
45
45
|
return sqlite3.Binary(data)
|
|
@@ -59,6 +59,7 @@ INVERTED_GAP_MARKS_CHECKSUMS = {
|
|
|
59
59
|
|
|
60
60
|
class Block:
|
|
61
61
|
def __init__(self, start, end, code=0, name=""):
|
|
62
|
+
# assert start <= end
|
|
62
63
|
self.start = start
|
|
63
64
|
self.end = end
|
|
64
65
|
self.name = name
|
|
@@ -84,7 +85,8 @@ class Block:
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
@lru_cache(300)
|
|
87
|
-
def bytes_to_string_and_fsha(byte_stream):
|
|
88
|
+
def bytes_to_string_and_fsha(byte_stream: bytes) -> Union[str, bytes]:
|
|
89
|
+
# Replace \f because of http://bugs.python.org/issue19035
|
|
88
90
|
byte_stream = byte_stream.replace(b"\f", b" ")
|
|
89
91
|
byte_stream = byte_stream.replace(b"\r\n", b"\n")
|
|
90
92
|
byte_string = byte_stream.decode(source_encoding(byte_stream), "replace")
|
|
@@ -107,7 +109,7 @@ def _next_lineno(nodes, i, end):
|
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
class Module:
|
|
110
|
-
def __init__(
|
|
112
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
111
113
|
self,
|
|
112
114
|
source_code=None,
|
|
113
115
|
mtime=None,
|
|
@@ -130,6 +132,16 @@ class Module:
|
|
|
130
132
|
self.ext = ext
|
|
131
133
|
|
|
132
134
|
def dump_and_block(self, node, end, name="unknown", into_block=False):
|
|
135
|
+
"""Frame of this method is taken from ast.dump
|
|
136
|
+
Objective is to return a representation of python source code where
|
|
137
|
+
all of the bodies of functions are replaced with 'transformed_into_block'
|
|
138
|
+
string. The rest of the syntax tree is represented in the same way as
|
|
139
|
+
in ast.dump(tree, annotate_fields=False). Of course the bodies of functions
|
|
140
|
+
are not completely thrown away, they are transformed into Block() objects
|
|
141
|
+
and appended to self.blocks. More can be probably understood from
|
|
142
|
+
(at the time rather messy) test_process_code.py examples.
|
|
143
|
+
"""
|
|
144
|
+
|
|
133
145
|
if isinstance(node, ast.AST):
|
|
134
146
|
class_name = node.__class__.__name__
|
|
135
147
|
fields = []
|
|
@@ -183,6 +195,8 @@ class Module:
|
|
|
183
195
|
tree = ast.parse(self.source_code, filename="<unknown>")
|
|
184
196
|
self.dump_and_block(tree, len(lines), name="<module>")
|
|
185
197
|
except SyntaxError:
|
|
198
|
+
# We can continue without blocks because no tests depending on this file will ever get executed,
|
|
199
|
+
# so no node depending on this checksum and mtime will ever be written to db.
|
|
186
200
|
pass
|
|
187
201
|
else:
|
|
188
202
|
self._blocks = [Block(1, len(lines), self.source_code)]
|
|
@@ -199,8 +213,8 @@ class Module:
|
|
|
199
213
|
return methods_to_checksums([block.checksum for block in self.blocks])
|
|
200
214
|
|
|
201
215
|
|
|
202
|
-
def read_source_sha(filename):
|
|
203
|
-
|
|
216
|
+
def read_source_sha(filename: str):
|
|
217
|
+
source_bytes: Optional[bytes]
|
|
204
218
|
|
|
205
219
|
try:
|
|
206
220
|
with open(filename, "rb") as file:
|
|
@@ -242,7 +256,7 @@ def get_files_shas(directory):
|
|
|
242
256
|
return noncached_get_files_shas(directory)
|
|
243
257
|
|
|
244
258
|
|
|
245
|
-
def get_source_sha(directory, filename):
|
|
259
|
+
def get_source_sha(directory: "str", filename: "str"):
|
|
246
260
|
try:
|
|
247
261
|
sha = get_files_shas(directory)[filename]
|
|
248
262
|
return (None, sha)
|
|
@@ -256,7 +270,7 @@ def match_fingerprint_source(source_code, fingerprint, ext="py"):
|
|
|
256
270
|
return match_fingerprint(module, fingerprint)
|
|
257
271
|
|
|
258
272
|
|
|
259
|
-
def match_fingerprint(module, fingerprint):
|
|
273
|
+
def match_fingerprint(module: Module, fingerprint):
|
|
260
274
|
if set(fingerprint) - set(module.checksums):
|
|
261
275
|
return False
|
|
262
276
|
return True
|
|
@@ -267,8 +281,8 @@ def create_fingerprint_source(source_code, lines, ext="py"):
|
|
|
267
281
|
return create_fingerprint(module, lines)
|
|
268
282
|
|
|
269
283
|
|
|
270
|
-
def create_fingerprint(module, covered_lines):
|
|
271
|
-
blocks = module.blocks
|
|
284
|
+
def create_fingerprint(module, covered_lines) -> [int]:
|
|
285
|
+
blocks: [Block] = module.blocks
|
|
272
286
|
method_reprs = []
|
|
273
287
|
line_index = 0
|
|
274
288
|
sorted_lines = sorted(covered_lines)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Main module of testmon pytest plugin.
|
|
4
|
+
"""
|
|
1
5
|
import time
|
|
2
6
|
import xmlrpc.client
|
|
3
7
|
import os
|
|
@@ -5,6 +9,7 @@ import os
|
|
|
5
9
|
from collections import defaultdict
|
|
6
10
|
from datetime import date, timedelta
|
|
7
11
|
|
|
12
|
+
from pathlib import Path
|
|
8
13
|
import pytest
|
|
9
14
|
|
|
10
15
|
from _pytest.config import ExitCode, Config
|
|
@@ -133,7 +138,7 @@ def testmon_options(config):
|
|
|
133
138
|
return result
|
|
134
139
|
|
|
135
140
|
|
|
136
|
-
def init_testmon_data(config):
|
|
141
|
+
def init_testmon_data(config: Config):
|
|
137
142
|
environment = config.getoption("environment_expression") or eval_environment(
|
|
138
143
|
config.getini("environment_expression")
|
|
139
144
|
)
|
|
@@ -217,7 +222,7 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
|
|
|
217
222
|
def pytest_configure(config):
|
|
218
223
|
coverage_stack = None
|
|
219
224
|
try:
|
|
220
|
-
from tmnet.testmon_core import (
|
|
225
|
+
from tmnet.testmon_core import ( # pylint: disable=import-outside-toplevel
|
|
221
226
|
Testmon as UberTestmon,
|
|
222
227
|
)
|
|
223
228
|
|
|
@@ -231,7 +236,7 @@ def pytest_configure(config):
|
|
|
231
236
|
tm_conf = configure.header_collect_select(
|
|
232
237
|
config, coverage_stack, cov_plugin=cov_plugin
|
|
233
238
|
)
|
|
234
|
-
config.testmon_config = tm_conf
|
|
239
|
+
config.testmon_config: TmConf = tm_conf
|
|
235
240
|
if tm_conf.select or tm_conf.collect:
|
|
236
241
|
try:
|
|
237
242
|
init_testmon_data(config)
|
|
@@ -277,7 +282,7 @@ def pytest_report_header(config):
|
|
|
277
282
|
if show_survey_notification:
|
|
278
283
|
tm_conf.message += (
|
|
279
284
|
"\nWe'd like to hear from testmon users! "
|
|
280
|
-
"
|
|
285
|
+
"Please go to https://testmon.org/survey to leave feedback."
|
|
281
286
|
)
|
|
282
287
|
return tm_conf.message
|
|
283
288
|
|
|
@@ -317,8 +322,8 @@ def pytest_unconfigure(config):
|
|
|
317
322
|
|
|
318
323
|
class TestmonCollect:
|
|
319
324
|
def __init__(self, testmon, testmon_data, running_as="single", cov_plugin=None):
|
|
320
|
-
self.testmon_data = testmon_data
|
|
321
|
-
self.testmon = testmon
|
|
325
|
+
self.testmon_data: TestmonData = testmon_data
|
|
326
|
+
self.testmon: TestmonCollector = testmon
|
|
322
327
|
self._running_as = running_as
|
|
323
328
|
|
|
324
329
|
self.reports = defaultdict(lambda: {})
|
|
@@ -327,18 +332,22 @@ class TestmonCollect:
|
|
|
327
332
|
self._sessionstarttime = time.time()
|
|
328
333
|
|
|
329
334
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
330
|
-
def pytest_pycollect_makeitem(
|
|
335
|
+
def pytest_pycollect_makeitem(
|
|
336
|
+
self, collector, name, obj
|
|
337
|
+
): # pylint: disable=unused-argument
|
|
331
338
|
makeitem_result = yield
|
|
332
339
|
items = makeitem_result.get_result() or []
|
|
333
340
|
try:
|
|
334
341
|
self.raw_test_names.extend(
|
|
335
342
|
[item.nodeid for item in items if isinstance(item, pytest.Item)]
|
|
336
343
|
)
|
|
337
|
-
except TypeError:
|
|
344
|
+
except TypeError: # 'Class' object is not iterable
|
|
338
345
|
pass
|
|
339
346
|
|
|
340
347
|
@pytest.hookimpl(tryfirst=True)
|
|
341
|
-
def pytest_collection_modifyitems(
|
|
348
|
+
def pytest_collection_modifyitems(
|
|
349
|
+
self, session, config, items
|
|
350
|
+
): # pylint: disable=unused-argument
|
|
342
351
|
should_sync = not session.testsfailed and self._running_as in (
|
|
343
352
|
"single",
|
|
344
353
|
"controller",
|
|
@@ -347,14 +356,16 @@ class TestmonCollect:
|
|
|
347
356
|
config.testmon_data.sync_db_fs_tests(retain=set(self.raw_test_names))
|
|
348
357
|
|
|
349
358
|
@pytest.hookimpl(hookwrapper=True)
|
|
350
|
-
def pytest_runtest_protocol(
|
|
359
|
+
def pytest_runtest_protocol(
|
|
360
|
+
self, item, nextitem
|
|
361
|
+
): # pylint: disable=unused-argument
|
|
351
362
|
self.testmon.start_testmon(item.nodeid, nextitem.nodeid if nextitem else None)
|
|
352
363
|
result = yield
|
|
353
364
|
if result.excinfo and issubclass(result.excinfo[0], BaseException):
|
|
354
365
|
self.testmon.discard_current()
|
|
355
366
|
|
|
356
367
|
@pytest.hookimpl(hookwrapper=True)
|
|
357
|
-
def pytest_runtest_makereport(self, item, call):
|
|
368
|
+
def pytest_runtest_makereport(self, item, call): # pylint: disable=unused-argument
|
|
358
369
|
result = yield
|
|
359
370
|
|
|
360
371
|
if call.when == "teardown":
|
|
@@ -377,7 +388,7 @@ class TestmonCollect:
|
|
|
377
388
|
test_executions_fingerprints
|
|
378
389
|
)
|
|
379
390
|
|
|
380
|
-
def pytest_keyboard_interrupt(self, excinfo):
|
|
391
|
+
def pytest_keyboard_interrupt(self, excinfo): # pylint: disable=unused-argument
|
|
381
392
|
if self._running_as == "single":
|
|
382
393
|
nodes_files_lines = self.testmon.get_batch_coverage_data()
|
|
383
394
|
|
|
@@ -387,7 +398,7 @@ class TestmonCollect:
|
|
|
387
398
|
self.testmon_data.save_test_execution_file_fps(test_executions_fingerprints)
|
|
388
399
|
self.testmon.close()
|
|
389
400
|
|
|
390
|
-
def pytest_sessionfinish(self, session):
|
|
401
|
+
def pytest_sessionfinish(self, session): # pylint: disable=unused-argument
|
|
391
402
|
if self._running_as in ("single", "controller"):
|
|
392
403
|
self.testmon_data.db.finish_execution(
|
|
393
404
|
self.testmon_data.exec_id,
|
|
@@ -401,10 +412,12 @@ class TestmonXdistSync:
|
|
|
401
412
|
def __init__(self):
|
|
402
413
|
self.await_nodes = 0
|
|
403
414
|
|
|
404
|
-
def pytest_testnodeready(self, node):
|
|
415
|
+
def pytest_testnodeready(self, node): # pylint: disable=unused-argument
|
|
405
416
|
self.await_nodes += 1
|
|
406
417
|
|
|
407
|
-
def pytest_xdist_node_collection_finished(
|
|
418
|
+
def pytest_xdist_node_collection_finished(
|
|
419
|
+
self, node, ids
|
|
420
|
+
): # pylint: disable=invalid-name
|
|
408
421
|
self.await_nodes += -1
|
|
409
422
|
if self.await_nodes == 0:
|
|
410
423
|
node.config.testmon_data.sync_db_fs_tests(retain=set(ids))
|
|
@@ -423,7 +436,7 @@ def get_failing(all_test_executions):
|
|
|
423
436
|
return failing_files, failing_tests
|
|
424
437
|
|
|
425
438
|
|
|
426
|
-
def sort_items_by_duration(items, avg_durations):
|
|
439
|
+
def sort_items_by_duration(items, avg_durations) -> None:
|
|
427
440
|
items.sort(key=lambda item: avg_durations[item.nodeid])
|
|
428
441
|
items.sort(
|
|
429
442
|
key=lambda item: avg_durations[get_test_execution_class_name(item.nodeid)]
|
|
@@ -443,7 +456,7 @@ def format_time_saved(seconds):
|
|
|
443
456
|
|
|
444
457
|
class TestmonSelect:
|
|
445
458
|
def __init__(self, config, testmon_data):
|
|
446
|
-
self.testmon_data = testmon_data
|
|
459
|
+
self.testmon_data: TestmonData = testmon_data
|
|
447
460
|
self.config = config
|
|
448
461
|
|
|
449
462
|
failing_files, failing_test_names = get_failing(testmon_data.all_tests)
|
|
@@ -458,14 +471,16 @@ class TestmonSelect:
|
|
|
458
471
|
]
|
|
459
472
|
self._interrupted = False
|
|
460
473
|
|
|
461
|
-
def pytest_ignore_collect(self,
|
|
462
|
-
strpath = cached_relpath(
|
|
474
|
+
def pytest_ignore_collect(self, collection_path: Path, config):
|
|
475
|
+
strpath = cached_relpath(str(collection_path), config.rootdir.strpath)
|
|
463
476
|
if strpath in self.deselected_files and self.config.testmon_config.select:
|
|
464
477
|
return True
|
|
465
478
|
return None
|
|
466
479
|
|
|
467
480
|
@pytest.hookimpl(trylast=True)
|
|
468
|
-
def pytest_collection_modifyitems(
|
|
481
|
+
def pytest_collection_modifyitems(
|
|
482
|
+
self, session, config, items
|
|
483
|
+
): # pylint: disable=unused-argument
|
|
469
484
|
selected = []
|
|
470
485
|
deselected = []
|
|
471
486
|
for item in items:
|
|
@@ -539,10 +554,10 @@ class TestmonSelect:
|
|
|
539
554
|
)
|
|
540
555
|
terminal_reporter.write_line(msg)
|
|
541
556
|
|
|
542
|
-
def pytest_keyboard_interrupt(self, excinfo):
|
|
557
|
+
def pytest_keyboard_interrupt(self, excinfo): # pylint: disable=unused-argument
|
|
543
558
|
self._interrupted = True
|
|
544
559
|
|
|
545
560
|
|
|
546
|
-
class FakeItemFromTestmon:
|
|
561
|
+
class FakeItemFromTestmon: # pylint: disable=too-few-public-methods
|
|
547
562
|
def __init__(self, config):
|
|
548
563
|
self.config = config
|
|
@@ -9,6 +9,13 @@ from collections import defaultdict
|
|
|
9
9
|
from xmlrpc.client import Fault, ProtocolError
|
|
10
10
|
from socket import gaierror
|
|
11
11
|
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from pytest_cov.plugin import CovPlugin
|
|
16
|
+
except ImportError:
|
|
17
|
+
pass
|
|
18
|
+
|
|
12
19
|
import pytest
|
|
13
20
|
from coverage import Coverage, CoverageData
|
|
14
21
|
|
|
@@ -29,6 +36,9 @@ from testmon.process_code import (
|
|
|
29
36
|
Module,
|
|
30
37
|
)
|
|
31
38
|
|
|
39
|
+
from testmon.common import DepsNOutcomes, TestExecutions
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
32
42
|
|
|
33
43
|
TEST_BATCH_SIZE = 250
|
|
34
44
|
|
|
@@ -55,10 +65,21 @@ class TestmonException(Exception):
|
|
|
55
65
|
|
|
56
66
|
|
|
57
67
|
class SourceTree:
|
|
68
|
+
"""
|
|
69
|
+
- reads files from file_system and caches them in memory
|
|
70
|
+
(if the file was once read, let's use the on memory
|
|
71
|
+
copy for further operations. Also caches the mtime.)
|
|
72
|
+
- store rootdir and convert between relative and absolute paths
|
|
73
|
+
- implement the check if the file is changed based on mtime, fsha.
|
|
74
|
+
- implement the ckeck if the node is stable based on fingerprint
|
|
75
|
+
- mockability (unit tests without filesystem, check if the file is changed
|
|
76
|
+
based on mtime, fsha)
|
|
77
|
+
"""
|
|
78
|
+
|
|
58
79
|
def __init__(self, rootdir, packages=None):
|
|
59
80
|
self.rootdir = rootdir
|
|
60
81
|
self.packages = packages
|
|
61
|
-
self.cache = {}
|
|
82
|
+
self.cache: dict = {}
|
|
62
83
|
|
|
63
84
|
def get_file(self, filename):
|
|
64
85
|
if filename not in self.cache:
|
|
@@ -81,7 +102,7 @@ class SourceTree:
|
|
|
81
102
|
return self.cache[filename]
|
|
82
103
|
|
|
83
104
|
|
|
84
|
-
def check_mtime(file_system, record):
|
|
105
|
+
def check_mtime(file_system: SourceTree, record):
|
|
85
106
|
absfilename = os.path.join(file_system.rootdir, record["filename"])
|
|
86
107
|
|
|
87
108
|
cache_module = file_system.cache.get(record["filename"], None)
|
|
@@ -99,7 +120,9 @@ def check_fsha(file_system, record):
|
|
|
99
120
|
return record["fsha"] == fs_fsha
|
|
100
121
|
|
|
101
122
|
|
|
102
|
-
def check_fingerprint(
|
|
123
|
+
def check_fingerprint(
|
|
124
|
+
disk, record: db.ChangedFileData
|
|
125
|
+
): # filename name method_fshas id failed
|
|
103
126
|
file = record[0]
|
|
104
127
|
fingerprint = record[2]
|
|
105
128
|
|
|
@@ -107,7 +130,7 @@ def check_fingerprint(disk, record):
|
|
|
107
130
|
return module and match_fingerprint(module, fingerprint)
|
|
108
131
|
|
|
109
132
|
|
|
110
|
-
def split_filter(disk, function, records):
|
|
133
|
+
def split_filter(disk, function, records: [T]) -> ([T], [T]):
|
|
111
134
|
first = []
|
|
112
135
|
second = []
|
|
113
136
|
for record in records:
|
|
@@ -131,10 +154,10 @@ def collect_mhashes(source_tree, new_changed_file_data):
|
|
|
131
154
|
return files_mhashes
|
|
132
155
|
|
|
133
156
|
|
|
134
|
-
class TestmonData:
|
|
157
|
+
class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
135
158
|
__test__ = False
|
|
136
159
|
|
|
137
|
-
def __init__(
|
|
160
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
138
161
|
self,
|
|
139
162
|
rootdir,
|
|
140
163
|
database=None,
|
|
@@ -145,7 +168,7 @@ class TestmonData:
|
|
|
145
168
|
):
|
|
146
169
|
self.rootdir = rootdir
|
|
147
170
|
self.environment = environment if environment else "default"
|
|
148
|
-
self.source_tree = SourceTree(rootdir=rootdir)
|
|
171
|
+
self.source_tree = SourceTree(rootdir=self.rootdir)
|
|
149
172
|
if system_packages is None:
|
|
150
173
|
system_packages = get_system_packages()
|
|
151
174
|
system_packages = drop_patch_version(system_packages)
|
|
@@ -153,11 +176,11 @@ class TestmonData:
|
|
|
153
176
|
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
154
177
|
|
|
155
178
|
if database:
|
|
156
|
-
self.db = database
|
|
179
|
+
self.db = database # pylint: disable=invalid-name
|
|
157
180
|
else:
|
|
158
181
|
self.db = db.DB(
|
|
159
182
|
os.path.join(self.rootdir, get_data_file_path()), readonly=readonly
|
|
160
|
-
)
|
|
183
|
+
) # pylint: disable=invalid-name
|
|
161
184
|
|
|
162
185
|
try:
|
|
163
186
|
result = self.db.initiate_execution(
|
|
@@ -178,7 +201,9 @@ class TestmonData:
|
|
|
178
201
|
),
|
|
179
202
|
exc,
|
|
180
203
|
)
|
|
181
|
-
self.db = db.DB(
|
|
204
|
+
self.db = db.DB(
|
|
205
|
+
os.path.join(self.rootdir, get_data_file_path())
|
|
206
|
+
) # pylint: disable=invalid-name
|
|
182
207
|
result = self.db.initiate_execution(
|
|
183
208
|
self.environment, system_packages, python_version, {}
|
|
184
209
|
)
|
|
@@ -200,15 +225,17 @@ class TestmonData:
|
|
|
200
225
|
|
|
201
226
|
def close_connection(self):
|
|
202
227
|
pass
|
|
228
|
+
# TODO this is why there are "too many open files" on MacOS.
|
|
229
|
+
# we need the leave the connection open for tests
|
|
203
230
|
|
|
204
231
|
@property
|
|
205
232
|
def all_tests(self):
|
|
206
233
|
return self.db.all_test_executions(self.exec_id)
|
|
207
234
|
|
|
208
|
-
def get_tests_fingerprints(self, nodes_files_lines, reports):
|
|
235
|
+
def get_tests_fingerprints(self, nodes_files_lines, reports) -> TestExecutions:
|
|
209
236
|
test_executions_fingerprints = {}
|
|
210
237
|
for context in nodes_files_lines:
|
|
211
|
-
deps_n_outcomes = {"deps": []}
|
|
238
|
+
deps_n_outcomes: DepsNOutcomes = {"deps": []}
|
|
212
239
|
|
|
213
240
|
for filename, covered in nodes_files_lines[context].items():
|
|
214
241
|
if os.path.exists(os.path.join(self.rootdir, filename)):
|
|
@@ -262,8 +289,11 @@ class TestmonData:
|
|
|
262
289
|
if module:
|
|
263
290
|
files_fshas[filename] = module.fs_fsha
|
|
264
291
|
|
|
292
|
+
# Compare the fshas from disk to the fshas in the database and get files
|
|
293
|
+
# where the fsha is not in database.
|
|
265
294
|
new_changed_file_data = self.db.fetch_unknown_files(files_fshas, self.exec_id)
|
|
266
295
|
|
|
296
|
+
# Get the mhashes for the files from above
|
|
267
297
|
files_mhashes = collect_mhashes(self.source_tree, new_changed_file_data)
|
|
268
298
|
|
|
269
299
|
tests = self.db.determine_tests(self.exec_id, files_mhashes)
|
|
@@ -288,13 +318,24 @@ class TestmonData:
|
|
|
288
318
|
|
|
289
319
|
_, fsha_misses = split_filter(
|
|
290
320
|
self.source_tree, check_fsha, filenames_fingerprints
|
|
291
|
-
)
|
|
321
|
+
) # check 2. fsha vs filesystem
|
|
322
|
+
|
|
323
|
+
# with the list of fingerprint_ids go to the database
|
|
324
|
+
# and fetch all the data needed for next step
|
|
292
325
|
|
|
293
326
|
changed_file_data = self.db.fetch_changed_file_data(
|
|
294
327
|
[fsha_miss["fingerprint_id"] for fsha_miss in (fsha_misses)],
|
|
295
328
|
self.exec_id,
|
|
296
329
|
)
|
|
297
330
|
|
|
331
|
+
# changed_file_data:
|
|
332
|
+
# [(filename, test_name, method_fshas, fingerprint_id, failed )]
|
|
333
|
+
# All the test_names in this list have a dependency on one
|
|
334
|
+
# or more changed files. And we also have the fingerprints
|
|
335
|
+
# of data content which they depend on. So it’s possible to
|
|
336
|
+
# filter out the node_ids where the content of the changed file
|
|
337
|
+
# still matches the fingerprint
|
|
338
|
+
|
|
298
339
|
_, fingerprint_misses = split_filter(
|
|
299
340
|
self.source_tree, check_fingerprint, changed_file_data
|
|
300
341
|
)
|
|
@@ -314,13 +355,13 @@ class TestmonData:
|
|
|
314
355
|
print("\n".join(sorted(new_fingerprint_misses)))
|
|
315
356
|
|
|
316
357
|
@property
|
|
317
|
-
def avg_durations(self):
|
|
358
|
+
def avg_durations(self) -> dict:
|
|
318
359
|
stats = defaultdict(lambda: {"test_execution": 0, "sum_duration": 0})
|
|
319
360
|
|
|
320
361
|
for (
|
|
321
362
|
test_execution_id,
|
|
322
363
|
report,
|
|
323
|
-
) in self.all_tests.items():
|
|
364
|
+
) in self.all_tests.items(): # pylint: disable=no-member
|
|
324
365
|
if report:
|
|
325
366
|
class_name = get_test_execution_class_name(test_execution_id)
|
|
326
367
|
module_name = get_test_execution_module_name(test_execution_id)
|
|
@@ -351,6 +392,7 @@ class TestmonData:
|
|
|
351
392
|
|
|
352
393
|
|
|
353
394
|
def get_new_mtimes(filesystem, hits):
|
|
395
|
+
"""hits: [(filename, _, _, fingerprint_id)]"""
|
|
354
396
|
try:
|
|
355
397
|
for hit in hits:
|
|
356
398
|
module = filesystem.get_file(hit[0])
|
|
@@ -379,24 +421,26 @@ def cached_relpath(path, basepath):
|
|
|
379
421
|
|
|
380
422
|
|
|
381
423
|
class TestmonCollector:
|
|
382
|
-
coverage_stack = []
|
|
424
|
+
coverage_stack: [Coverage] = []
|
|
383
425
|
|
|
384
|
-
def __init__(
|
|
426
|
+
def __init__(
|
|
427
|
+
self, rootdir, testmon_labels=None, cov_plugin=None
|
|
428
|
+
): # TODO remove cov_plugin
|
|
385
429
|
try:
|
|
386
|
-
from testmon.testmon_core import (
|
|
430
|
+
from testmon.testmon_core import ( # pylint: disable=import-outside-toplevel
|
|
387
431
|
Testmon as UberTestmon,
|
|
388
432
|
)
|
|
389
433
|
|
|
390
|
-
TestmonCollector.coverage_stack = UberTestmon.coverage_stack
|
|
434
|
+
TestmonCollector.coverage_stack: [Coverage] = UberTestmon.coverage_stack
|
|
391
435
|
except ImportError:
|
|
392
436
|
pass
|
|
393
437
|
if testmon_labels is None:
|
|
394
438
|
testmon_labels = {"singleprocess"}
|
|
395
439
|
self.rootdir = rootdir
|
|
396
440
|
self.testmon_labels = testmon_labels
|
|
397
|
-
self.cov = None
|
|
441
|
+
self.cov: Coverage = None
|
|
398
442
|
self.sub_cov_file = None
|
|
399
|
-
self.cov_plugin = cov_plugin
|
|
443
|
+
self.cov_plugin: CovPlugin = cov_plugin
|
|
400
444
|
self._test_name = None
|
|
401
445
|
self._next_test_name = None
|
|
402
446
|
self.batched_test_names = set()
|
|
@@ -449,6 +493,7 @@ class TestmonCollector:
|
|
|
449
493
|
params["include"] = list(
|
|
450
494
|
set(cov.config.run_include + params["include"])
|
|
451
495
|
)
|
|
496
|
+
# params["omit"] = cov.config.run_omit
|
|
452
497
|
if cov.config.branch:
|
|
453
498
|
raise TestmonException(
|
|
454
499
|
"testmon doesn't support simultaneous run with pytest-cov when "
|
|
@@ -519,7 +564,7 @@ class TestmonCollector:
|
|
|
519
564
|
return nodes_files_lines
|
|
520
565
|
|
|
521
566
|
def get_nodes_files_lines(self, dont_include):
|
|
522
|
-
cov_data = self.cov.get_data()
|
|
567
|
+
cov_data: CoverageData = self.cov.get_data()
|
|
523
568
|
files = cov_data.measured_files()
|
|
524
569
|
nodes_files_lines = {}
|
|
525
570
|
files_lines = {}
|
|
@@ -573,7 +618,7 @@ def eval_environment(environment, **kwargs):
|
|
|
573
618
|
|
|
574
619
|
try:
|
|
575
620
|
return str(eval(environment, eval_globals))
|
|
576
|
-
except Exception as error:
|
|
621
|
+
except Exception as error: # pylint: disable=broad-except
|
|
577
622
|
return repr(error)
|
|
578
623
|
|
|
579
624
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
1
|
from pathlib import Path
|
|
4
2
|
import pluggy
|
|
5
3
|
|
|
@@ -21,7 +19,7 @@ def touch_stampfile(venv):
|
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
def installed_testmon(venv):
|
|
24
|
-
return
|
|
22
|
+
return Path(venv.path.join(".testmon_installed")).exists()
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
@hookimpl
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|