pytest-testmon 2.1.1.dev0__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.dev0/pytest_testmon.egg-info → pytest_testmon-2.1.3}/PKG-INFO +2 -2
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pyproject.toml +2 -1
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3/pytest_testmon.egg-info}/PKG-INFO +2 -2
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/requires.txt +1 -1
- pytest_testmon-2.1.3/testmon/__init__.py +2 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/common.py +53 -8
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/configure.py +1 -1
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/db.py +75 -62
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/process_code.py +24 -10
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/pytest_testmon.py +38 -23
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/testmon_core.py +72 -23
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/testmon/tox_testmon.py +1 -3
- pytest-testmon-2.1.1.dev0/testmon/__init__.py +0 -2
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/LICENSE +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/MANIFEST.in +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/README.md +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/SOURCES.txt +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/dependency_links.txt +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/entry_points.txt +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/top_level.txt +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/setup.cfg +0 -0
- {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-testmon
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: selects tests affected by changed files and methods
|
|
5
5
|
Author-email: Tibor Arpas <tibor@testmon.org>
|
|
6
6
|
License: MIT
|
|
@@ -18,7 +18,7 @@ Classifier: Framework :: Pytest
|
|
|
18
18
|
Requires-Python: >=3.8
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: pytest<
|
|
21
|
+
Requires-Dist: pytest<9,>=5
|
|
22
22
|
Requires-Dist: coverage<8,>=6
|
|
23
23
|
|
|
24
24
|
<img src=https://user-images.githubusercontent.com/135344/219700265-0a9b152f-7285-4607-bbce-0c9aeddd520b.svg width=300>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-testmon
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: selects tests affected by changed files and methods
|
|
5
5
|
Author-email: Tibor Arpas <tibor@testmon.org>
|
|
6
6
|
License: MIT
|
|
@@ -18,7 +18,7 @@ Classifier: Framework :: Pytest
|
|
|
18
18
|
Requires-Python: >=3.8
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: pytest<
|
|
21
|
+
Requires-Dist: pytest<9,>=5
|
|
22
22
|
Requires-Dist: coverage<8,>=6
|
|
23
23
|
|
|
24
24
|
<img src=https://user-images.githubusercontent.com/135344/219700265-0a9b152f-7285-4607-bbce-0c9aeddd520b.svg width=300>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
pytest<
|
|
1
|
+
pytest<9,>=5
|
|
2
2
|
coverage<8,>=6
|
|
@@ -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,34 +13,75 @@ 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
|
|
26
59
|
|
|
27
60
|
|
|
28
61
|
def get_logger(name):
|
|
29
|
-
logging.
|
|
30
|
-
|
|
62
|
+
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
63
|
+
handler = logging.StreamHandler()
|
|
64
|
+
handler.setFormatter(formatter)
|
|
65
|
+
|
|
66
|
+
# Configure the logger
|
|
67
|
+
tm_logger = logging.getLogger(name)
|
|
68
|
+
tm_logger.setLevel(logging.INFO)
|
|
69
|
+
tm_logger.addHandler(handler)
|
|
70
|
+
return tm_logger
|
|
31
71
|
|
|
32
72
|
|
|
33
73
|
logger = get_logger(__name__)
|
|
34
74
|
|
|
35
75
|
|
|
36
76
|
def get_system_packages(ignore=None):
|
|
77
|
+
if not ignore:
|
|
78
|
+
ignore = set(("pytest-testmon", "pytest-testmon"))
|
|
37
79
|
return ", ".join(
|
|
38
80
|
sorted(
|
|
39
81
|
{
|
|
40
82
|
f"{package} {version}"
|
|
41
83
|
for (package, version) in get_system_packages_raw()
|
|
42
|
-
if not
|
|
84
|
+
if not package in ignore
|
|
43
85
|
}
|
|
44
86
|
)
|
|
45
87
|
)
|
|
@@ -47,16 +89,19 @@ def get_system_packages(ignore=None):
|
|
|
47
89
|
|
|
48
90
|
def drop_patch_version(system_packages):
|
|
49
91
|
return re.sub(
|
|
50
|
-
r"\b([\w_-]+\s\d+\.\d+)\.\w+\b",
|
|
92
|
+
r"\b([\w_-]+\s\d+\.\d+)\.\w+\b", # extract (Package M.N).P / drop .patch
|
|
51
93
|
r"\1",
|
|
52
94
|
system_packages,
|
|
53
95
|
)
|
|
54
96
|
|
|
55
97
|
|
|
56
|
-
|
|
98
|
+
#
|
|
99
|
+
# .git utilities
|
|
100
|
+
#
|
|
101
|
+
def git_path(start_path=None): # parent dirs only
|
|
57
102
|
start_path = Path(start_path or os.getcwd()).resolve()
|
|
58
103
|
current_path = start_path
|
|
59
|
-
while current_path != current_path.parent:
|
|
104
|
+
while current_path != current_path.parent: # '/'.parent == '/'
|
|
60
105
|
path = current_path / ".git"
|
|
61
106
|
if path.exists() and path.is_dir():
|
|
62
107
|
return str(path)
|
|
@@ -73,7 +118,7 @@ def git_current_branch(path=None):
|
|
|
73
118
|
with open(git_head_file, "r", encoding="utf8") as head_file:
|
|
74
119
|
head_content = head_file.read().strip()
|
|
75
120
|
if head_content.startswith("ref:"):
|
|
76
|
-
return head_content.split("/")[-1]
|
|
121
|
+
return head_content.split("/")[-1] # e.g. ref: refs/heads/master
|
|
77
122
|
except FileNotFoundError:
|
|
78
123
|
pass
|
|
79
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
|
|
|
@@ -20,10 +22,12 @@ class TestmonDbException(Exception):
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def connect(datafile, readonly=False):
|
|
23
|
-
|
|
25
|
+
return sqlite3.connect(
|
|
24
26
|
f"file:{datafile}{'?mode=ro' if readonly else ''}", uri=True, timeout=60
|
|
25
27
|
)
|
|
26
28
|
|
|
29
|
+
|
|
30
|
+
def connection_options(connection):
|
|
27
31
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
28
32
|
connection.execute("PRAGMA synchronous = OFF")
|
|
29
33
|
connection.execute("PRAGMA foreign_keys = TRUE ")
|
|
@@ -32,45 +36,51 @@ def connect(datafile, readonly=False):
|
|
|
32
36
|
return connection
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
def check_fingerprint_db(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if set(fingerprint) - set(files_methods_checksums[file]):
|
|
39
|
+
def check_fingerprint_db(
|
|
40
|
+
files_methods_checksums, file_name, fingerprint: ChangedFileData
|
|
41
|
+
): # filename name method_checksums id failed
|
|
42
|
+
if file_name in files_methods_checksums and files_methods_checksums[file_name]:
|
|
43
|
+
if set(fingerprint) - set(files_methods_checksums[file_name]):
|
|
41
44
|
return False
|
|
42
45
|
return True
|
|
43
46
|
return False
|
|
44
47
|
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
def check_data_version(connection, datafile, data_version):
|
|
50
|
+
stored_data_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
|
51
|
+
|
|
52
|
+
if int(stored_data_version) == data_version:
|
|
53
|
+
return connection, False
|
|
54
|
+
|
|
55
|
+
connection.close()
|
|
56
|
+
os.remove(datafile)
|
|
57
|
+
connection = connect(datafile)
|
|
58
|
+
connection = connection_options(connection)
|
|
59
|
+
return connection, True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DB: # pylint: disable=too-many-public-methods
|
|
47
63
|
def __init__(self, datafile, readonly=False):
|
|
48
64
|
self._readonly = readonly
|
|
49
|
-
|
|
65
|
+
file_exists = os.path.exists(datafile)
|
|
50
66
|
|
|
51
67
|
connection = connect(datafile, readonly)
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
connection, old_format = check_data_version(
|
|
69
|
+
connection, datafile, self.version_compatibility()
|
|
70
|
+
)
|
|
71
|
+
self.con = connection_options(connection)
|
|
54
72
|
|
|
55
|
-
if
|
|
73
|
+
if (not file_exists) or old_format:
|
|
56
74
|
self.init_tables()
|
|
75
|
+
self.file_created = True
|
|
76
|
+
else:
|
|
77
|
+
self.file_created = False
|
|
57
78
|
|
|
58
79
|
self.con.executescript(self._local_temp_tables_statement())
|
|
59
80
|
|
|
60
81
|
def version_compatibility(self):
|
|
61
82
|
return DATA_VERSION
|
|
62
83
|
|
|
63
|
-
def _check_data_version(self, datafile):
|
|
64
|
-
stored_data_version = self._fetch_data_version()
|
|
65
|
-
|
|
66
|
-
if int(stored_data_version) == self.version_compatibility():
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
self.con.close()
|
|
70
|
-
os.remove(datafile)
|
|
71
|
-
self.con = connect(datafile)
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
84
|
def __enter__(self):
|
|
75
85
|
self.con = self.con.__enter__()
|
|
76
86
|
return self
|
|
@@ -78,10 +88,10 @@ class DB:
|
|
|
78
88
|
def __exit__(self, *args, **kwargs):
|
|
79
89
|
self.con.__exit__(*args, **kwargs)
|
|
80
90
|
|
|
81
|
-
def _test_execution_fk_column(self):
|
|
91
|
+
def _test_execution_fk_column(self) -> str:
|
|
82
92
|
return "environment_id"
|
|
83
93
|
|
|
84
|
-
def _test_execution_fk_table(self):
|
|
94
|
+
def _test_execution_fk_table(self) -> str:
|
|
85
95
|
return "environment"
|
|
86
96
|
|
|
87
97
|
def update_mtimes(self, new_mtimes):
|
|
@@ -90,7 +100,9 @@ class DB:
|
|
|
90
100
|
"UPDATE file_fp SET mtime=?, fsha=? WHERE id = ?", new_mtimes
|
|
91
101
|
)
|
|
92
102
|
|
|
93
|
-
def finish_execution(
|
|
103
|
+
def finish_execution(
|
|
104
|
+
self, exec_id, duration=None, select=True
|
|
105
|
+
): # pylint: disable=unused-argument
|
|
94
106
|
self.update_saving_stats(exec_id, select)
|
|
95
107
|
self.fetch_or_create_file_fp.cache_clear()
|
|
96
108
|
with self.con as con:
|
|
@@ -181,7 +193,9 @@ class DB:
|
|
|
181
193
|
)
|
|
182
194
|
|
|
183
195
|
@lru_cache(1000)
|
|
184
|
-
def fetch_or_create_file_fp(
|
|
196
|
+
def fetch_or_create_file_fp(
|
|
197
|
+
self, filename, fsha, method_checksums
|
|
198
|
+
): # pylint: disable=R0801
|
|
185
199
|
cursor = self.con.cursor()
|
|
186
200
|
try:
|
|
187
201
|
cursor.execute(
|
|
@@ -194,7 +208,7 @@ class DB:
|
|
|
194
208
|
)
|
|
195
209
|
|
|
196
210
|
fingerprint_id = cursor.lastrowid
|
|
197
|
-
except sqlite3.IntegrityError:
|
|
211
|
+
except sqlite3.IntegrityError: # rather fetching existing fingerprint
|
|
198
212
|
fingerprint_id, *_ = cursor.execute(
|
|
199
213
|
"""
|
|
200
214
|
SELECT
|
|
@@ -209,15 +223,15 @@ class DB:
|
|
|
209
223
|
|
|
210
224
|
return fingerprint_id
|
|
211
225
|
|
|
212
|
-
def _insert_test_execution(
|
|
226
|
+
def _insert_test_execution( # pylint: disable=too-many-arguments
|
|
213
227
|
self,
|
|
214
228
|
con,
|
|
215
|
-
exec_id,
|
|
216
|
-
test_name,
|
|
217
|
-
duration,
|
|
218
|
-
failed,
|
|
219
|
-
forced,
|
|
220
|
-
):
|
|
229
|
+
exec_id: int,
|
|
230
|
+
test_name: "str",
|
|
231
|
+
duration: "float",
|
|
232
|
+
failed: "bool",
|
|
233
|
+
forced: bool,
|
|
234
|
+
) -> int:
|
|
221
235
|
cursor = con.cursor()
|
|
222
236
|
cursor.execute(
|
|
223
237
|
f"""
|
|
@@ -235,7 +249,7 @@ class DB:
|
|
|
235
249
|
)
|
|
236
250
|
return cursor.lastrowid
|
|
237
251
|
|
|
238
|
-
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):
|
|
239
253
|
assert exec_id
|
|
240
254
|
with self.con as con:
|
|
241
255
|
cursor = con.cursor()
|
|
@@ -285,11 +299,6 @@ class DB:
|
|
|
285
299
|
def insert_into_suite_files_fshas(self, con, exec_id, files_fshas):
|
|
286
300
|
pass
|
|
287
301
|
|
|
288
|
-
def _fetch_data_version(self):
|
|
289
|
-
con = self.con
|
|
290
|
-
|
|
291
|
-
return con.execute("PRAGMA user_version").fetchone()[0]
|
|
292
|
-
|
|
293
302
|
def write_attribute(self, attribute, data, exec_id=None):
|
|
294
303
|
dataid = f"{exec_id}:{attribute}"
|
|
295
304
|
with self.con as con:
|
|
@@ -305,7 +314,7 @@ class DB:
|
|
|
305
314
|
)
|
|
306
315
|
result = cursor.fetchone()
|
|
307
316
|
if result:
|
|
308
|
-
return json.loads(result[0])
|
|
317
|
+
return json.loads(result[0]) # zlib.decompress(result[0]).decode('utf-8)'))
|
|
309
318
|
return default
|
|
310
319
|
|
|
311
320
|
def increment_attributes(self, attributes_to_increment, exec_id=None):
|
|
@@ -328,10 +337,10 @@ class DB:
|
|
|
328
337
|
],
|
|
329
338
|
)
|
|
330
339
|
|
|
331
|
-
def _create_metadata_statement(self):
|
|
340
|
+
def _create_metadata_statement(self) -> str:
|
|
332
341
|
return """CREATE TABLE metadata (dataid TEXT PRIMARY KEY, data TEXT);"""
|
|
333
342
|
|
|
334
|
-
def _create_environment_statement(self):
|
|
343
|
+
def _create_environment_statement(self) -> str:
|
|
335
344
|
return """
|
|
336
345
|
CREATE TABLE environment (
|
|
337
346
|
id INTEGER PRIMARY KEY ASC,
|
|
@@ -341,7 +350,7 @@ class DB:
|
|
|
341
350
|
UNIQUE (environment_name, system_packages, python_version)
|
|
342
351
|
);"""
|
|
343
352
|
|
|
344
|
-
def _create_test_execution_statement(self):
|
|
353
|
+
def _create_test_execution_statement(self) -> str: # pylint: disable=invalid-name
|
|
345
354
|
return f"""
|
|
346
355
|
CREATE TABLE test_execution (
|
|
347
356
|
id INTEGER PRIMARY KEY ASC,
|
|
@@ -354,10 +363,10 @@ class DB:
|
|
|
354
363
|
CREATE INDEX test_execution_fk_name ON test_execution ({self._test_execution_fk_column()}, test_name);
|
|
355
364
|
"""
|
|
356
365
|
|
|
357
|
-
def _create_temp_tables_statement(self):
|
|
366
|
+
def _create_temp_tables_statement(self) -> str:
|
|
358
367
|
return ""
|
|
359
368
|
|
|
360
|
-
def _local_temp_tables_statement(self):
|
|
369
|
+
def _local_temp_tables_statement(self) -> str:
|
|
361
370
|
return """
|
|
362
371
|
CREATE TEMPORARY TABLE changed_files_fshas (exec_id INTEGER, filename TEXT, fsha TEXT);
|
|
363
372
|
CREATE INDEX changed_files_fshas_mcall ON changed_files_fshas (exec_id, filename, fsha);
|
|
@@ -366,7 +375,7 @@ class DB:
|
|
|
366
375
|
CREATE INDEX changed_files_mhashes_eid ON changed_files_mhashes (exec_id);
|
|
367
376
|
"""
|
|
368
377
|
|
|
369
|
-
def _create_file_fp_statement(self):
|
|
378
|
+
def _create_file_fp_statement(self) -> str:
|
|
370
379
|
return """
|
|
371
380
|
CREATE TABLE file_fp
|
|
372
381
|
(
|
|
@@ -378,9 +387,9 @@ class DB:
|
|
|
378
387
|
UNIQUE (filename, fsha, method_checksums)
|
|
379
388
|
);"""
|
|
380
389
|
|
|
381
|
-
def _create_test_execution_ffp_statement(
|
|
390
|
+
def _create_test_execution_ffp_statement( # pylint: disable=invalid-name
|
|
382
391
|
self,
|
|
383
|
-
):
|
|
392
|
+
) -> str:
|
|
384
393
|
return """
|
|
385
394
|
CREATE TABLE test_execution_file_fp (
|
|
386
395
|
test_execution_id INTEGER,
|
|
@@ -413,7 +422,7 @@ class DB:
|
|
|
413
422
|
|
|
414
423
|
connection.execute(f"PRAGMA user_version = {self.version_compatibility()}")
|
|
415
424
|
|
|
416
|
-
def fetch_changed_file_data(self, changed_fingerprints, exec_id):
|
|
425
|
+
def fetch_changed_file_data(self, changed_fingerprints, exec_id) -> []:
|
|
417
426
|
in_clause_questionsmarks = ", ".join("?" * len(changed_fingerprints))
|
|
418
427
|
result = []
|
|
419
428
|
for row in self.con.execute(
|
|
@@ -450,7 +459,9 @@ class DB:
|
|
|
450
459
|
|
|
451
460
|
return result
|
|
452
461
|
|
|
453
|
-
def fetch_unknown_files(
|
|
462
|
+
def fetch_unknown_files(
|
|
463
|
+
self, files_fshas, exec_id
|
|
464
|
+
) -> []: # exec_id is environment_id in this module
|
|
454
465
|
with self.con as con:
|
|
455
466
|
con.execute("DELETE FROM changed_files_fshas WHERE exec_id = ?", (exec_id,))
|
|
456
467
|
con.executemany(
|
|
@@ -525,10 +536,10 @@ class DB:
|
|
|
525
536
|
]
|
|
526
537
|
)
|
|
527
538
|
|
|
528
|
-
|
|
539
|
+
method_misses = []
|
|
529
540
|
for result in results:
|
|
530
|
-
if not check_fingerprint_db(files_mhashes, result):
|
|
531
|
-
|
|
541
|
+
if not check_fingerprint_db(files_mhashes, result[0], result[2]):
|
|
542
|
+
method_misses.append(result[1])
|
|
532
543
|
|
|
533
544
|
failing_tests = [
|
|
534
545
|
row["test_name"]
|
|
@@ -545,7 +556,7 @@ class DB:
|
|
|
545
556
|
)
|
|
546
557
|
]
|
|
547
558
|
|
|
548
|
-
return {"affected":
|
|
559
|
+
return {"affected": method_misses, "failing": failing_tests}
|
|
549
560
|
|
|
550
561
|
def delete_test_executions(self, test_names, exec_id):
|
|
551
562
|
self.con.executemany(
|
|
@@ -597,6 +608,7 @@ class DB:
|
|
|
597
608
|
|
|
598
609
|
return [row[0] for row in cursor]
|
|
599
610
|
|
|
611
|
+
# TODO unify with filenames? Restrict not to go into ancient history, but not miss combinations?
|
|
600
612
|
def all_filenames(self):
|
|
601
613
|
cursor = self.con.execute(
|
|
602
614
|
"""
|
|
@@ -636,6 +648,7 @@ class DB:
|
|
|
636
648
|
self, environment_name, system_packages, python_version
|
|
637
649
|
):
|
|
638
650
|
with self.con as con:
|
|
651
|
+
con.execute("BEGIN IMMEDIATE TRANSACTION")
|
|
639
652
|
cursor = con.cursor()
|
|
640
653
|
environment = cursor.execute(
|
|
641
654
|
"""
|
|
@@ -683,13 +696,13 @@ class DB:
|
|
|
683
696
|
|
|
684
697
|
return environment_id, packages_changed
|
|
685
698
|
|
|
686
|
-
def initiate_execution(
|
|
699
|
+
def initiate_execution( # pylint: disable= R0913 W0613
|
|
687
700
|
self,
|
|
688
|
-
environment_name,
|
|
689
|
-
system_packages,
|
|
690
|
-
python_version,
|
|
691
|
-
execution_metadata,
|
|
692
|
-
):
|
|
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
|
|
693
706
|
exec_id, packages_changed = self.fetch_or_create_environment(
|
|
694
707
|
environment_name, system_packages, python_version
|
|
695
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
|
|
|
@@ -296,7 +301,7 @@ def changed_message(
|
|
|
296
301
|
if changed_files_msg == "" or len(changed_files_msg) > 100:
|
|
297
302
|
changed_files_msg = str(len(config.testmon_data.unstable_files))
|
|
298
303
|
|
|
299
|
-
if
|
|
304
|
+
if config.testmon_data.new_db:
|
|
300
305
|
message += "new DB, "
|
|
301
306
|
else:
|
|
302
307
|
message += (
|
|
@@ -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
|
)
|
|
@@ -194,17 +219,23 @@ class TestmonData:
|
|
|
194
219
|
self.stable_files = None
|
|
195
220
|
self.failing_tests = None
|
|
196
221
|
|
|
222
|
+
@property
|
|
223
|
+
def new_db(self):
|
|
224
|
+
return self.db.file_created
|
|
225
|
+
|
|
197
226
|
def close_connection(self):
|
|
198
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
|
|
199
230
|
|
|
200
231
|
@property
|
|
201
232
|
def all_tests(self):
|
|
202
233
|
return self.db.all_test_executions(self.exec_id)
|
|
203
234
|
|
|
204
|
-
def get_tests_fingerprints(self, nodes_files_lines, reports):
|
|
235
|
+
def get_tests_fingerprints(self, nodes_files_lines, reports) -> TestExecutions:
|
|
205
236
|
test_executions_fingerprints = {}
|
|
206
237
|
for context in nodes_files_lines:
|
|
207
|
-
deps_n_outcomes = {"deps": []}
|
|
238
|
+
deps_n_outcomes: DepsNOutcomes = {"deps": []}
|
|
208
239
|
|
|
209
240
|
for filename, covered in nodes_files_lines[context].items():
|
|
210
241
|
if os.path.exists(os.path.join(self.rootdir, filename)):
|
|
@@ -258,8 +289,11 @@ class TestmonData:
|
|
|
258
289
|
if module:
|
|
259
290
|
files_fshas[filename] = module.fs_fsha
|
|
260
291
|
|
|
292
|
+
# Compare the fshas from disk to the fshas in the database and get files
|
|
293
|
+
# where the fsha is not in database.
|
|
261
294
|
new_changed_file_data = self.db.fetch_unknown_files(files_fshas, self.exec_id)
|
|
262
295
|
|
|
296
|
+
# Get the mhashes for the files from above
|
|
263
297
|
files_mhashes = collect_mhashes(self.source_tree, new_changed_file_data)
|
|
264
298
|
|
|
265
299
|
tests = self.db.determine_tests(self.exec_id, files_mhashes)
|
|
@@ -284,13 +318,24 @@ class TestmonData:
|
|
|
284
318
|
|
|
285
319
|
_, fsha_misses = split_filter(
|
|
286
320
|
self.source_tree, check_fsha, filenames_fingerprints
|
|
287
|
-
)
|
|
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
|
|
288
325
|
|
|
289
326
|
changed_file_data = self.db.fetch_changed_file_data(
|
|
290
327
|
[fsha_miss["fingerprint_id"] for fsha_miss in (fsha_misses)],
|
|
291
328
|
self.exec_id,
|
|
292
329
|
)
|
|
293
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
|
+
|
|
294
339
|
_, fingerprint_misses = split_filter(
|
|
295
340
|
self.source_tree, check_fingerprint, changed_file_data
|
|
296
341
|
)
|
|
@@ -310,13 +355,13 @@ class TestmonData:
|
|
|
310
355
|
print("\n".join(sorted(new_fingerprint_misses)))
|
|
311
356
|
|
|
312
357
|
@property
|
|
313
|
-
def avg_durations(self):
|
|
358
|
+
def avg_durations(self) -> dict:
|
|
314
359
|
stats = defaultdict(lambda: {"test_execution": 0, "sum_duration": 0})
|
|
315
360
|
|
|
316
361
|
for (
|
|
317
362
|
test_execution_id,
|
|
318
363
|
report,
|
|
319
|
-
) in self.all_tests.items():
|
|
364
|
+
) in self.all_tests.items(): # pylint: disable=no-member
|
|
320
365
|
if report:
|
|
321
366
|
class_name = get_test_execution_class_name(test_execution_id)
|
|
322
367
|
module_name = get_test_execution_module_name(test_execution_id)
|
|
@@ -347,6 +392,7 @@ class TestmonData:
|
|
|
347
392
|
|
|
348
393
|
|
|
349
394
|
def get_new_mtimes(filesystem, hits):
|
|
395
|
+
"""hits: [(filename, _, _, fingerprint_id)]"""
|
|
350
396
|
try:
|
|
351
397
|
for hit in hits:
|
|
352
398
|
module = filesystem.get_file(hit[0])
|
|
@@ -375,24 +421,26 @@ def cached_relpath(path, basepath):
|
|
|
375
421
|
|
|
376
422
|
|
|
377
423
|
class TestmonCollector:
|
|
378
|
-
coverage_stack = []
|
|
424
|
+
coverage_stack: [Coverage] = []
|
|
379
425
|
|
|
380
|
-
def __init__(
|
|
426
|
+
def __init__(
|
|
427
|
+
self, rootdir, testmon_labels=None, cov_plugin=None
|
|
428
|
+
): # TODO remove cov_plugin
|
|
381
429
|
try:
|
|
382
|
-
from testmon.testmon_core import (
|
|
430
|
+
from testmon.testmon_core import ( # pylint: disable=import-outside-toplevel
|
|
383
431
|
Testmon as UberTestmon,
|
|
384
432
|
)
|
|
385
433
|
|
|
386
|
-
TestmonCollector.coverage_stack = UberTestmon.coverage_stack
|
|
434
|
+
TestmonCollector.coverage_stack: [Coverage] = UberTestmon.coverage_stack
|
|
387
435
|
except ImportError:
|
|
388
436
|
pass
|
|
389
437
|
if testmon_labels is None:
|
|
390
438
|
testmon_labels = {"singleprocess"}
|
|
391
439
|
self.rootdir = rootdir
|
|
392
440
|
self.testmon_labels = testmon_labels
|
|
393
|
-
self.cov = None
|
|
441
|
+
self.cov: Coverage = None
|
|
394
442
|
self.sub_cov_file = None
|
|
395
|
-
self.cov_plugin = cov_plugin
|
|
443
|
+
self.cov_plugin: CovPlugin = cov_plugin
|
|
396
444
|
self._test_name = None
|
|
397
445
|
self._next_test_name = None
|
|
398
446
|
self.batched_test_names = set()
|
|
@@ -445,6 +493,7 @@ class TestmonCollector:
|
|
|
445
493
|
params["include"] = list(
|
|
446
494
|
set(cov.config.run_include + params["include"])
|
|
447
495
|
)
|
|
496
|
+
# params["omit"] = cov.config.run_omit
|
|
448
497
|
if cov.config.branch:
|
|
449
498
|
raise TestmonException(
|
|
450
499
|
"testmon doesn't support simultaneous run with pytest-cov when "
|
|
@@ -515,7 +564,7 @@ class TestmonCollector:
|
|
|
515
564
|
return nodes_files_lines
|
|
516
565
|
|
|
517
566
|
def get_nodes_files_lines(self, dont_include):
|
|
518
|
-
cov_data = self.cov.get_data()
|
|
567
|
+
cov_data: CoverageData = self.cov.get_data()
|
|
519
568
|
files = cov_data.measured_files()
|
|
520
569
|
nodes_files_lines = {}
|
|
521
570
|
files_lines = {}
|
|
@@ -569,7 +618,7 @@ def eval_environment(environment, **kwargs):
|
|
|
569
618
|
|
|
570
619
|
try:
|
|
571
620
|
return str(eval(environment, eval_globals))
|
|
572
|
-
except Exception as error:
|
|
621
|
+
except Exception as error: # pylint: disable=broad-except
|
|
573
622
|
return repr(error)
|
|
574
623
|
|
|
575
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
|
{pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|