pytest-testmon 2.1.4__tar.gz → 2.3.0b0__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.4/pytest_testmon.egg-info → pytest_testmon-2.3.0b0}/PKG-INFO +5 -3
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pyproject.toml +23 -29
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0/pytest_testmon.egg-info}/PKG-INFO +5 -3
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/requires.txt +1 -1
- pytest_testmon-2.3.0b0/testmon/__init__.py +3 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/configure.py +4 -2
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/db.py +47 -37
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/process_code.py +16 -8
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/pytest_testmon.py +102 -40
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/testmon_core.py +159 -90
- pytest_testmon-2.1.4/testmon/__init__.py +0 -2
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/LICENSE +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/MANIFEST.in +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/README.md +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/SOURCES.txt +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/dependency_links.txt +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/entry_points.txt +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/top_level.txt +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/setup.cfg +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/setup.py +0 -0
- {pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/testmon/common.py +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-testmon
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0b0
|
|
4
4
|
Summary: selects tests affected by changed files and methods
|
|
5
5
|
Author-email: Tibor Arpas <tibor@testmon.org>
|
|
6
6
|
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://testmon.org
|
|
8
|
+
Project-URL: Source, https://github.com/tarpas/pytest-testmon
|
|
7
9
|
Keywords: testing,pytest,plugin
|
|
8
10
|
Classifier: Development Status :: 4 - Beta
|
|
9
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,10 +17,10 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
15
17
|
Classifier: Topic :: Utilities
|
|
16
18
|
Classifier: Programming Language :: Python
|
|
17
19
|
Classifier: Framework :: Pytest
|
|
18
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
20
22
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: pytest<
|
|
23
|
+
Requires-Dist: pytest<10,>=5
|
|
22
24
|
Requires-Dist: coverage<8,>=6
|
|
23
25
|
Dynamic: license-file
|
|
24
26
|
|
|
@@ -7,11 +7,11 @@ name = "pytest-testmon"
|
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Tibor Arpas", email = "tibor@testmon.org" },
|
|
9
9
|
]
|
|
10
|
-
dynamic =
|
|
10
|
+
dynamic = ["version"]
|
|
11
11
|
|
|
12
12
|
description = "selects tests affected by changed files and methods"
|
|
13
13
|
readme = "README.md"
|
|
14
|
-
requires-python = ">=3.
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
15
|
keywords = ["testing", "pytest", "plugin"]
|
|
16
16
|
license = { text = "MIT" }
|
|
17
17
|
classifiers = [
|
|
@@ -27,38 +27,32 @@ classifiers = [
|
|
|
27
27
|
"Framework :: Pytest",
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
|
-
"pytest>=5,<
|
|
30
|
+
"pytest>=5,<10",
|
|
31
31
|
"coverage>=6,<8",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://testmon.org"
|
|
36
|
+
Source = "https://github.com/tarpas/pytest-testmon"
|
|
37
|
+
|
|
38
|
+
[project.entry-points.pytest11]
|
|
39
|
+
pytest-testmon = "testmon.pytest_testmon"
|
|
40
|
+
|
|
35
41
|
[tool.setuptools]
|
|
36
|
-
packages = [
|
|
37
|
-
"testmon",
|
|
38
|
-
]
|
|
42
|
+
packages = ["testmon"]
|
|
39
43
|
|
|
40
44
|
[tool.setuptools.dynamic]
|
|
41
45
|
version = {attr = "testmon.TESTMON_VERSION"}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
| build
|
|
56
|
-
| dist
|
|
57
|
-
)/
|
|
58
|
-
| io.intellij/
|
|
59
|
-
)
|
|
60
|
-
'''
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[project.entry-points.pytest11]
|
|
64
|
-
pytest-testmon = "testmon.pytest_testmon"
|
|
47
|
+
# Development dependencies, managed by uv: `uv sync` then `uv run pytest` / `uv run tox`.
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest==8.3.5",
|
|
51
|
+
"pytest-cov",
|
|
52
|
+
"pytest-xdist",
|
|
53
|
+
"pytest-mock",
|
|
54
|
+
"ruff",
|
|
55
|
+
"tox",
|
|
56
|
+
"tox-gh-actions",
|
|
57
|
+
"pre-commit",
|
|
58
|
+
]
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-testmon
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0b0
|
|
4
4
|
Summary: selects tests affected by changed files and methods
|
|
5
5
|
Author-email: Tibor Arpas <tibor@testmon.org>
|
|
6
6
|
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://testmon.org
|
|
8
|
+
Project-URL: Source, https://github.com/tarpas/pytest-testmon
|
|
7
9
|
Keywords: testing,pytest,plugin
|
|
8
10
|
Classifier: Development Status :: 4 - Beta
|
|
9
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,10 +17,10 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
15
17
|
Classifier: Topic :: Utilities
|
|
16
18
|
Classifier: Programming Language :: Python
|
|
17
19
|
Classifier: Framework :: Pytest
|
|
18
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
20
22
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: pytest<
|
|
23
|
+
Requires-Dist: pytest<10,>=5
|
|
22
24
|
Requires-Dist: coverage<8,>=6
|
|
23
25
|
Dynamic: license-file
|
|
24
26
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
pytest<
|
|
1
|
+
pytest<10,>=5
|
|
2
2
|
coverage<8,>=6
|
|
@@ -75,10 +75,12 @@ def _get_noselect_reasons(options):
|
|
|
75
75
|
if options["markexpr"]:
|
|
76
76
|
return ["-m was used"]
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
# `--lf` is registered by the cacheprovider plugin, absent with `-p no:cacheprovider`
|
|
79
|
+
if options.get("lf"):
|
|
79
80
|
return ["--lf was used"]
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
file_or_dir = options.get("file_or_dir") or []
|
|
83
|
+
if any(re.match(r"(.*)\.py::(.*)", opt) for opt in file_or_dir):
|
|
82
84
|
return ["you selected tests manually"]
|
|
83
85
|
|
|
84
86
|
return []
|
|
@@ -5,12 +5,10 @@ import sqlite3
|
|
|
5
5
|
from collections import namedtuple
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
|
|
8
|
-
from testmon.process_code import blob_to_checksums, checksums_to_blob
|
|
9
|
-
|
|
10
8
|
from testmon.common import TestExecutions
|
|
9
|
+
from testmon.process_code import blob_to_checksums, checksums_to_blob
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
DATA_VERSION = 13
|
|
11
|
+
DATA_VERSION = 14
|
|
14
12
|
|
|
15
13
|
ChangedFileData = namedtuple(
|
|
16
14
|
"ChangedFileData", "filename name method_checksums id failed"
|
|
@@ -81,12 +79,14 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
81
79
|
def version_compatibility(self):
|
|
82
80
|
return DATA_VERSION
|
|
83
81
|
|
|
82
|
+
def close(self):
|
|
83
|
+
self.con.close()
|
|
84
|
+
|
|
84
85
|
def __enter__(self):
|
|
85
|
-
self.con = self.con.__enter__()
|
|
86
86
|
return self
|
|
87
87
|
|
|
88
|
-
def __exit__(self, *args
|
|
89
|
-
self.
|
|
88
|
+
def __exit__(self, *args):
|
|
89
|
+
self.close()
|
|
90
90
|
|
|
91
91
|
def _test_execution_fk_column(self) -> str:
|
|
92
92
|
return "environment_id"
|
|
@@ -100,9 +100,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
100
100
|
"UPDATE file_fp SET mtime=?, fsha=? WHERE id = ?", new_mtimes
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
def finish_execution(
|
|
104
|
-
self, exec_id, duration=None, select=True
|
|
105
|
-
): # pylint: disable=unused-argument
|
|
103
|
+
def finish_execution(self, exec_id, duration=None, select=True): # pylint: disable=unused-argument
|
|
106
104
|
self.update_saving_stats(exec_id, select)
|
|
107
105
|
self.fetch_or_create_file_fp.cache_clear()
|
|
108
106
|
with self.con as con:
|
|
@@ -193,9 +191,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
193
191
|
)
|
|
194
192
|
|
|
195
193
|
@lru_cache(1000)
|
|
196
|
-
def fetch_or_create_file_fp(
|
|
197
|
-
self, filename, fsha, method_checksums
|
|
198
|
-
): # pylint: disable=R0801
|
|
194
|
+
def fetch_or_create_file_fp(self, filename, fsha, method_checksums): # pylint: disable=R0801
|
|
199
195
|
cursor = self.con.cursor()
|
|
200
196
|
try:
|
|
201
197
|
cursor.execute(
|
|
@@ -216,9 +212,9 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
216
212
|
FROM
|
|
217
213
|
file_fp
|
|
218
214
|
WHERE
|
|
219
|
-
filename = ? AND method_checksums = ?
|
|
215
|
+
filename = ? AND fsha = ? AND method_checksums = ?
|
|
220
216
|
""",
|
|
221
|
-
(filename, method_checksums),
|
|
217
|
+
(filename, fsha, method_checksums),
|
|
222
218
|
).fetchone()
|
|
223
219
|
|
|
224
220
|
return fingerprint_id
|
|
@@ -359,7 +355,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
359
355
|
duration FLOAT,
|
|
360
356
|
failed BIT,
|
|
361
357
|
forced BIT,
|
|
362
|
-
FOREIGN KEY({self._test_execution_fk_column()}) REFERENCES {self._test_execution_fk_table()}(id));
|
|
358
|
+
FOREIGN KEY({self._test_execution_fk_column()}) REFERENCES {self._test_execution_fk_table()}(id) ON DELETE CASCADE);
|
|
363
359
|
CREATE INDEX test_execution_fk_name ON test_execution ({self._test_execution_fk_column()}, test_name);
|
|
364
360
|
"""
|
|
365
361
|
|
|
@@ -394,7 +390,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
394
390
|
CREATE TABLE test_execution_file_fp (
|
|
395
391
|
test_execution_id INTEGER,
|
|
396
392
|
fingerprint_id INTEGER,
|
|
397
|
-
FOREIGN KEY(test_execution_id) REFERENCES test_execution(id),
|
|
393
|
+
FOREIGN KEY(test_execution_id) REFERENCES test_execution(id) ON DELETE CASCADE,
|
|
398
394
|
FOREIGN KEY(fingerprint_id) REFERENCES file_fp(id)
|
|
399
395
|
);
|
|
400
396
|
CREATE INDEX test_execution_file_fp_both ON test_execution_file_fp (test_execution_id, fingerprint_id);
|
|
@@ -403,7 +399,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
403
399
|
suite_execution_id INTEGER,
|
|
404
400
|
filename TEXT,
|
|
405
401
|
fsha text,
|
|
406
|
-
FOREIGN KEY(suite_execution_id) REFERENCES suite_execution(id)
|
|
402
|
+
FOREIGN KEY(suite_execution_id) REFERENCES suite_execution(id) ON DELETE CASCADE
|
|
407
403
|
);
|
|
408
404
|
CREATE UNIQUE INDEX sefch_suite_id_filename_sha ON suite_execution_file_fsha(suite_execution_id, filename, fsha);
|
|
409
405
|
"""
|
|
@@ -559,23 +555,24 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
559
555
|
return {"affected": method_misses, "failing": failing_tests}
|
|
560
556
|
|
|
561
557
|
def delete_test_executions(self, test_names, exec_id):
|
|
562
|
-
self.con
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
558
|
+
with self.con as con:
|
|
559
|
+
con.executemany(
|
|
560
|
+
f"""
|
|
561
|
+
DELETE
|
|
562
|
+
FROM test_execution_file_fp
|
|
563
|
+
WHERE test_execution_id IN
|
|
564
|
+
(SELECT id FROM test_execution WHERE {self._test_execution_fk_column()} = ? AND test_name = ?)
|
|
565
|
+
""",
|
|
566
|
+
[(exec_id, test_name) for test_name in test_names],
|
|
567
|
+
)
|
|
568
|
+
con.executemany(
|
|
569
|
+
f"""
|
|
570
|
+
DELETE
|
|
571
|
+
FROM test_execution
|
|
572
|
+
WHERE {self._test_execution_fk_column()} = ?
|
|
573
|
+
AND test_name = ?""",
|
|
574
|
+
[(exec_id, test_name) for test_name in test_names],
|
|
575
|
+
)
|
|
579
576
|
|
|
580
577
|
def all_test_executions(self, exec_id):
|
|
581
578
|
return {
|
|
@@ -659,7 +656,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
659
656
|
ORDER BY id DESC
|
|
660
657
|
""",
|
|
661
658
|
(environment_name,),
|
|
662
|
-
).fetchone()
|
|
659
|
+
).fetchone() # TODO to pick best environment of many with the same name, we would need to fetch all.
|
|
663
660
|
|
|
664
661
|
if environment:
|
|
665
662
|
environment_id = environment["id"]
|
|
@@ -669,7 +666,7 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
669
666
|
)
|
|
670
667
|
else:
|
|
671
668
|
packages_changed = False
|
|
672
|
-
if not environment
|
|
669
|
+
if not environment:
|
|
673
670
|
try:
|
|
674
671
|
cursor.execute(
|
|
675
672
|
"""
|
|
@@ -694,6 +691,19 @@ class DB: # pylint: disable=too-many-public-methods
|
|
|
694
691
|
(environment_name,),
|
|
695
692
|
).fetchone()
|
|
696
693
|
environment_id = environment["id"]
|
|
694
|
+
elif packages_changed:
|
|
695
|
+
cursor.execute(
|
|
696
|
+
"""
|
|
697
|
+
INSERT INTO environment (environment_name, system_packages, python_version)
|
|
698
|
+
VALUES (?, ?, ?)
|
|
699
|
+
""",
|
|
700
|
+
(environment_name, system_packages, python_version),
|
|
701
|
+
)
|
|
702
|
+
new_environment_id = cursor.lastrowid
|
|
703
|
+
cursor.execute(
|
|
704
|
+
"DELETE FROM environment WHERE id = ?", (environment_id,)
|
|
705
|
+
)
|
|
706
|
+
environment_id = new_environment_id
|
|
697
707
|
|
|
698
708
|
return environment_id, packages_changed
|
|
699
709
|
|
|
@@ -86,14 +86,17 @@ class Block:
|
|
|
86
86
|
|
|
87
87
|
@lru_cache(300)
|
|
88
88
|
def bytes_to_string_and_fsha(byte_stream: bytes) -> Union[str, bytes]:
|
|
89
|
-
#
|
|
90
|
-
|
|
89
|
+
# The fsha must equal git's blob sha of the CRLF-normalized bytes, so that it
|
|
90
|
+
# matches the index entry returned by get_files_shas() and no read is needed
|
|
91
|
+
# for files git reports as unmodified. The header length is the byte length,
|
|
92
|
+
# and hashing happens before the \f replacement (git does not touch \f).
|
|
91
93
|
byte_stream = byte_stream.replace(b"\r\n", b"\n")
|
|
92
|
-
byte_string = byte_stream.decode(source_encoding(byte_stream), "replace")
|
|
93
|
-
git_header = b"blob %u\0" % len(byte_string)
|
|
94
94
|
hsh = hashlib.sha1()
|
|
95
|
-
hsh.update(
|
|
95
|
+
hsh.update(b"blob %u\0" % len(byte_stream))
|
|
96
96
|
hsh.update(byte_stream)
|
|
97
|
+
# Replace \f because of http://bugs.python.org/issue19035
|
|
98
|
+
byte_stream = byte_stream.replace(b"\f", b" ")
|
|
99
|
+
byte_string = byte_stream.decode(source_encoding(byte_stream), "replace")
|
|
97
100
|
if byte_string and byte_string[-1] != "\n":
|
|
98
101
|
byte_string += "\n"
|
|
99
102
|
return byte_string, hsh.hexdigest()
|
|
@@ -235,13 +238,18 @@ def read_source_sha(filename: str):
|
|
|
235
238
|
def noncached_get_files_shas(directory):
|
|
236
239
|
all_shas = {}
|
|
237
240
|
try:
|
|
241
|
+
# Run inside `directory` so the printed paths are relative to it (they
|
|
242
|
+
# are the keys testmon looks up); a pathspec argument would print paths
|
|
243
|
+
# relative to the current working directory instead.
|
|
238
244
|
result = run(
|
|
239
|
-
["git", "ls-files", "--stage", "-m"
|
|
245
|
+
["git", "ls-files", "--stage", "-m"],
|
|
246
|
+
cwd=directory or None,
|
|
240
247
|
capture_output=True,
|
|
241
|
-
|
|
248
|
+
text=True,
|
|
249
|
+
encoding="utf-8",
|
|
242
250
|
check=True,
|
|
243
251
|
)
|
|
244
|
-
except (FileNotFoundError, CalledProcessError):
|
|
252
|
+
except (FileNotFoundError, NotADirectoryError, CalledProcessError):
|
|
245
253
|
return all_shas
|
|
246
254
|
|
|
247
255
|
modified_files = set()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Main module of testmon pytest plugin.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import time
|
|
6
7
|
import xmlrpc.client
|
|
7
8
|
import os
|
|
@@ -105,13 +106,18 @@ def pytest_addoption(parser):
|
|
|
105
106
|
" code serving different endpoints or Django settings."
|
|
106
107
|
),
|
|
107
108
|
)
|
|
108
|
-
|
|
109
|
+
group.addoption(
|
|
110
|
+
"--testmon-singleprocess",
|
|
111
|
+
action="store_false",
|
|
112
|
+
dest="testmon_singleprocess",
|
|
113
|
+
help="Don't track subprocesses",
|
|
114
|
+
)
|
|
109
115
|
group.addoption(
|
|
110
116
|
"--tmnet",
|
|
111
117
|
action="store_true",
|
|
112
118
|
dest="tmnet",
|
|
113
119
|
help=(
|
|
114
|
-
"
|
|
120
|
+
"Use tmnet cloud for storage instead of .testmondata file used by --testmon (see https://www.testmon.net)."
|
|
115
121
|
),
|
|
116
122
|
)
|
|
117
123
|
|
|
@@ -132,6 +138,7 @@ def testmon_options(config):
|
|
|
132
138
|
"testmon",
|
|
133
139
|
"no-testmon",
|
|
134
140
|
"environment_expression",
|
|
141
|
+
"testmon_singleprocess",
|
|
135
142
|
]:
|
|
136
143
|
if config.getoption(label):
|
|
137
144
|
result.append(label.replace("testmon_", ""))
|
|
@@ -165,24 +172,52 @@ def init_testmon_data(config: Config):
|
|
|
165
172
|
)
|
|
166
173
|
else:
|
|
167
174
|
tmnet_api_key = os.getenv("TMNET_API_KEY")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
|
|
176
|
+
if not tmnet_api_key.strip():
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"TMNET_API_KEY is required when using --tmnet. "
|
|
179
|
+
"Please set it in pytest.ini, pyproject.toml, or as an environment variable. "
|
|
171
180
|
)
|
|
181
|
+
|
|
172
182
|
rpc_proxy = xmlrpc.client.ServerProxy(
|
|
173
183
|
url,
|
|
174
184
|
allow_none=True,
|
|
175
|
-
headers=[("x-api-key", tmnet_api_key)],
|
|
185
|
+
headers=[("x-api-key", tmnet_api_key.strip())],
|
|
176
186
|
)
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
188
|
+
# Check if we're a worker and have exec_id from controller
|
|
189
|
+
running_as = get_running_as(config)
|
|
190
|
+
exec_id = None
|
|
191
|
+
system_packages_change = None
|
|
192
|
+
files_of_interest = None
|
|
193
|
+
|
|
194
|
+
if running_as == "worker" and hasattr(config, "workerinput"):
|
|
195
|
+
# Use exec_id sent from controller
|
|
196
|
+
exec_id = config.workerinput.get("testmon_exec_id")
|
|
197
|
+
system_packages_change = config.workerinput.get(
|
|
198
|
+
"testmon_system_packages_change"
|
|
199
|
+
)
|
|
200
|
+
files_of_interest = config.workerinput.get("testmon_files_of_interest")
|
|
201
|
+
|
|
202
|
+
if running_as == "worker" and exec_id is not None:
|
|
203
|
+
# Initialize for xdist worker run
|
|
204
|
+
testmon_data: TestmonData = TestmonData.for_worker(
|
|
205
|
+
rootdir=str(config.rootpath),
|
|
206
|
+
exec_id=exec_id,
|
|
207
|
+
database=rpc_proxy,
|
|
208
|
+
system_packages_change=system_packages_change,
|
|
209
|
+
files_of_interest=files_of_interest,
|
|
210
|
+
environment=environment,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Initialize for local run (controller or single process)
|
|
214
|
+
testmon_data: TestmonData = TestmonData.for_local_run(
|
|
215
|
+
rootdir=str(config.rootpath),
|
|
216
|
+
database=rpc_proxy,
|
|
217
|
+
environment=environment,
|
|
218
|
+
system_packages=system_packages,
|
|
219
|
+
)
|
|
220
|
+
testmon_data.determine_stable()
|
|
186
221
|
config.testmon_data = testmon_data
|
|
187
222
|
|
|
188
223
|
|
|
@@ -206,7 +241,7 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
|
|
|
206
241
|
config.pluginmanager.register(
|
|
207
242
|
TestmonCollect(
|
|
208
243
|
TestmonCollector(
|
|
209
|
-
config.
|
|
244
|
+
str(config.rootpath),
|
|
210
245
|
testmon_labels=testmon_options(config),
|
|
211
246
|
cov_plugin=cov_plugin,
|
|
212
247
|
),
|
|
@@ -215,8 +250,8 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
|
|
|
215
250
|
),
|
|
216
251
|
"TestmonCollect",
|
|
217
252
|
)
|
|
218
|
-
|
|
219
|
-
|
|
253
|
+
if (should_select or should_collect) and config.pluginmanager.hasplugin("xdist"):
|
|
254
|
+
config.pluginmanager.register(TestmonXdistSync(sync=should_collect))
|
|
220
255
|
|
|
221
256
|
|
|
222
257
|
def pytest_configure(config):
|
|
@@ -238,6 +273,10 @@ def pytest_configure(config):
|
|
|
238
273
|
)
|
|
239
274
|
config.testmon_config: TmConf = tm_conf
|
|
240
275
|
if tm_conf.select or tm_conf.collect:
|
|
276
|
+
if config.pluginmanager.has_plugin(
|
|
277
|
+
"TestmonSelect"
|
|
278
|
+
) or config.pluginmanager.has_plugin("TestmonCollect"):
|
|
279
|
+
pytest.exit("You cannot use two instances of testmon plugin.")
|
|
241
280
|
try:
|
|
242
281
|
init_testmon_data(config)
|
|
243
282
|
register_plugins(config, tm_conf.select, tm_conf.collect, cov_plugin)
|
|
@@ -321,7 +360,9 @@ def pytest_unconfigure(config):
|
|
|
321
360
|
|
|
322
361
|
|
|
323
362
|
class TestmonCollect:
|
|
324
|
-
def __init__(
|
|
363
|
+
def __init__(
|
|
364
|
+
self, testmon, testmon_data: TestmonData, running_as="single", cov_plugin=None
|
|
365
|
+
):
|
|
325
366
|
self.testmon_data: TestmonData = testmon_data
|
|
326
367
|
self.testmon: TestmonCollector = testmon
|
|
327
368
|
self._running_as = running_as
|
|
@@ -332,9 +373,7 @@ class TestmonCollect:
|
|
|
332
373
|
self._sessionstarttime = time.time()
|
|
333
374
|
|
|
334
375
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
335
|
-
def pytest_pycollect_makeitem(
|
|
336
|
-
self, collector, name, obj
|
|
337
|
-
): # pylint: disable=unused-argument
|
|
376
|
+
def pytest_pycollect_makeitem(self, collector, name, obj): # pylint: disable=unused-argument
|
|
338
377
|
makeitem_result = yield
|
|
339
378
|
items = makeitem_result.get_result() or []
|
|
340
379
|
try:
|
|
@@ -345,9 +384,7 @@ class TestmonCollect:
|
|
|
345
384
|
pass
|
|
346
385
|
|
|
347
386
|
@pytest.hookimpl(tryfirst=True)
|
|
348
|
-
def pytest_collection_modifyitems(
|
|
349
|
-
self, session, config, items
|
|
350
|
-
): # pylint: disable=unused-argument
|
|
387
|
+
def pytest_collection_modifyitems(self, session, config, items): # pylint: disable=unused-argument
|
|
351
388
|
should_sync = not session.testsfailed and self._running_as in (
|
|
352
389
|
"single",
|
|
353
390
|
"controller",
|
|
@@ -356,9 +393,7 @@ class TestmonCollect:
|
|
|
356
393
|
config.testmon_data.sync_db_fs_tests(retain=set(self.raw_test_names))
|
|
357
394
|
|
|
358
395
|
@pytest.hookimpl(hookwrapper=True)
|
|
359
|
-
def pytest_runtest_protocol(
|
|
360
|
-
self, item, nextitem
|
|
361
|
-
): # pylint: disable=unused-argument
|
|
396
|
+
def pytest_runtest_protocol(self, item, nextitem): # pylint: disable=unused-argument
|
|
362
397
|
self.testmon.start_testmon(item.nodeid, nextitem.nodeid if nextitem else None)
|
|
363
398
|
result = yield
|
|
364
399
|
if result.excinfo and issubclass(result.excinfo[0], BaseException):
|
|
@@ -371,7 +406,9 @@ class TestmonCollect:
|
|
|
371
406
|
if call.when == "teardown":
|
|
372
407
|
report = result.get_result()
|
|
373
408
|
report.nodes_files_lines = self.testmon.get_batch_coverage_data()
|
|
374
|
-
result.force_result(
|
|
409
|
+
result.force_result(
|
|
410
|
+
report
|
|
411
|
+
) # under xdist, report is serialized on the worker and sent to the controller
|
|
375
412
|
|
|
376
413
|
@pytest.hookimpl
|
|
377
414
|
def pytest_runtest_logreport(self, report):
|
|
@@ -409,17 +446,44 @@ class TestmonCollect:
|
|
|
409
446
|
|
|
410
447
|
|
|
411
448
|
class TestmonXdistSync:
|
|
412
|
-
def __init__(self):
|
|
449
|
+
def __init__(self, sync=True):
|
|
413
450
|
self.await_nodes = 0
|
|
414
|
-
|
|
415
|
-
|
|
451
|
+
self.sync = sync
|
|
452
|
+
|
|
453
|
+
def pytest_configure_node(self, node):
|
|
454
|
+
"""
|
|
455
|
+
Send exec_id and related data from controller to worker during xdist initialization.
|
|
456
|
+
This avoids each worker having to independently determine the environment and check
|
|
457
|
+
for package changes.
|
|
458
|
+
|
|
459
|
+
Note: This hook is called on the controller side for each worker node.
|
|
460
|
+
The node.config here is the controller's config, not the worker's config.
|
|
461
|
+
"""
|
|
462
|
+
# Verify we're on the controller (not a worker)
|
|
463
|
+
# node.config in this hook is the controller's config
|
|
464
|
+
running_as = get_running_as(node.config)
|
|
465
|
+
if running_as != "controller":
|
|
466
|
+
return # Safety check: only run on controller
|
|
467
|
+
|
|
468
|
+
# Count the node here, before it starts: counting it when it reports ready would let a
|
|
469
|
+
# fast worker's collection trigger sync_db_fs_tests while a slow one is still determining
|
|
470
|
+
# stability, which then sees the placeholder rows as stable tests and collects nothing.
|
|
416
471
|
self.await_nodes += 1
|
|
417
472
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
473
|
+
# Only proceed if testmon_data has been initialized and workerinput exists
|
|
474
|
+
if hasattr(node.config, "testmon_data") and hasattr(node, "workerinput"):
|
|
475
|
+
testmon_data: TestmonData = node.config.testmon_data
|
|
476
|
+
node.workerinput["testmon_exec_id"] = testmon_data.exec_id
|
|
477
|
+
node.workerinput["testmon_system_packages_change"] = (
|
|
478
|
+
testmon_data.system_packages_change
|
|
479
|
+
)
|
|
480
|
+
node.workerinput["testmon_files_of_interest"] = (
|
|
481
|
+
testmon_data.files_of_interest
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def pytest_xdist_node_collection_finished(self, node, ids): # pylint: disable=invalid-name
|
|
421
485
|
self.await_nodes += -1
|
|
422
|
-
if self.await_nodes == 0:
|
|
486
|
+
if self.await_nodes == 0 and self.sync:
|
|
423
487
|
node.config.testmon_data.sync_db_fs_tests(retain=set(ids))
|
|
424
488
|
|
|
425
489
|
|
|
@@ -455,7 +519,7 @@ def format_time_saved(seconds):
|
|
|
455
519
|
|
|
456
520
|
|
|
457
521
|
class TestmonSelect:
|
|
458
|
-
def __init__(self, config, testmon_data):
|
|
522
|
+
def __init__(self, config, testmon_data: TestmonData):
|
|
459
523
|
self.testmon_data: TestmonData = testmon_data
|
|
460
524
|
self.config = config
|
|
461
525
|
|
|
@@ -472,15 +536,13 @@ class TestmonSelect:
|
|
|
472
536
|
self._interrupted = False
|
|
473
537
|
|
|
474
538
|
def pytest_ignore_collect(self, collection_path: Path, config):
|
|
475
|
-
strpath = cached_relpath(str(collection_path), config.
|
|
539
|
+
strpath = cached_relpath(str(collection_path), str(config.rootpath))
|
|
476
540
|
if strpath in self.deselected_files and self.config.testmon_config.select:
|
|
477
541
|
return True
|
|
478
542
|
return None
|
|
479
543
|
|
|
480
544
|
@pytest.hookimpl(trylast=True)
|
|
481
|
-
def pytest_collection_modifyitems(
|
|
482
|
-
self, session, config, items
|
|
483
|
-
): # pylint: disable=unused-argument
|
|
545
|
+
def pytest_collection_modifyitems(self, session, config, items): # pylint: disable=unused-argument
|
|
484
546
|
selected = []
|
|
485
547
|
deselected = []
|
|
486
548
|
for item in items:
|
|
@@ -6,7 +6,7 @@ import sysconfig
|
|
|
6
6
|
import textwrap
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
from collections import defaultdict
|
|
9
|
-
from xmlrpc.client import Fault, ProtocolError
|
|
9
|
+
from xmlrpc.client import Fault, ProtocolError, ServerProxy
|
|
10
10
|
from socket import gaierror
|
|
11
11
|
|
|
12
12
|
from typing import TypeVar
|
|
@@ -15,9 +15,9 @@ try:
|
|
|
15
15
|
from pytest_cov.plugin import CovPlugin
|
|
16
16
|
except ImportError:
|
|
17
17
|
pass
|
|
18
|
-
|
|
19
18
|
import pytest
|
|
20
19
|
from coverage import Coverage, CoverageData
|
|
20
|
+
from coverage.exceptions import CoverageException
|
|
21
21
|
|
|
22
22
|
from testmon import db
|
|
23
23
|
from testmon import TESTMON_VERSION as TM_CLIENT_VERSION
|
|
@@ -157,23 +157,14 @@ def collect_mhashes(source_tree, new_changed_file_data):
|
|
|
157
157
|
class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
158
158
|
__test__ = False
|
|
159
159
|
|
|
160
|
-
def __init__(
|
|
160
|
+
def __init__(
|
|
161
161
|
self,
|
|
162
162
|
rootdir,
|
|
163
163
|
database=None,
|
|
164
|
-
environment=None,
|
|
165
|
-
system_packages=None,
|
|
166
|
-
python_version=None,
|
|
167
164
|
readonly=False,
|
|
168
165
|
):
|
|
169
166
|
self.rootdir = rootdir
|
|
170
|
-
self.environment = environment if environment else "default"
|
|
171
167
|
self.source_tree = SourceTree(rootdir=self.rootdir)
|
|
172
|
-
if system_packages is None:
|
|
173
|
-
system_packages = get_system_packages()
|
|
174
|
-
system_packages = drop_patch_version(system_packages)
|
|
175
|
-
if not python_version:
|
|
176
|
-
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
177
168
|
|
|
178
169
|
if database:
|
|
179
170
|
self.db = database # pylint: disable=invalid-name
|
|
@@ -182,6 +173,48 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
182
173
|
os.path.join(self.rootdir, get_data_file_path()), readonly=readonly
|
|
183
174
|
) # pylint: disable=invalid-name
|
|
184
175
|
|
|
176
|
+
# Initialize instance variables (will be set by init methods)
|
|
177
|
+
self.environment = None
|
|
178
|
+
self.exec_id = None
|
|
179
|
+
self.system_packages_change = None
|
|
180
|
+
self.files_of_interest = None
|
|
181
|
+
self.all_files = {}
|
|
182
|
+
self.unstable_test_names = None
|
|
183
|
+
self.unstable_files = None
|
|
184
|
+
self.stable_test_names = None
|
|
185
|
+
self.stable_files = None
|
|
186
|
+
self.failing_tests = None
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def for_local_run(
|
|
190
|
+
cls,
|
|
191
|
+
rootdir,
|
|
192
|
+
database=None,
|
|
193
|
+
environment=None,
|
|
194
|
+
system_packages=None,
|
|
195
|
+
python_version=None,
|
|
196
|
+
):
|
|
197
|
+
instance = cls(rootdir, database=database, readonly=False)
|
|
198
|
+
instance._init_for_local_run(environment, system_packages, python_version)
|
|
199
|
+
return instance
|
|
200
|
+
|
|
201
|
+
def _init_for_local_run(
|
|
202
|
+
self,
|
|
203
|
+
environment=None,
|
|
204
|
+
system_packages=None,
|
|
205
|
+
python_version=None,
|
|
206
|
+
):
|
|
207
|
+
"""Internal method to initialize for local run."""
|
|
208
|
+
self.environment = environment if environment else "default"
|
|
209
|
+
|
|
210
|
+
if system_packages is None:
|
|
211
|
+
system_packages = get_system_packages()
|
|
212
|
+
system_packages = drop_patch_version(system_packages)
|
|
213
|
+
|
|
214
|
+
if not python_version:
|
|
215
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
216
|
+
|
|
217
|
+
# Initiate execution (controller or single process)
|
|
185
218
|
try:
|
|
186
219
|
result = self.db.initiate_execution(
|
|
187
220
|
self.environment,
|
|
@@ -201,32 +234,72 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
201
234
|
),
|
|
202
235
|
exc,
|
|
203
236
|
)
|
|
204
|
-
self.db = db.DB(
|
|
205
|
-
os.path.join(self.rootdir, get_data_file_path())
|
|
206
|
-
) # pylint: disable=invalid-name
|
|
237
|
+
self.db = db.DB(os.path.join(self.rootdir, get_data_file_path())) # pylint: disable=invalid-name
|
|
207
238
|
result = self.db.initiate_execution(
|
|
208
239
|
self.environment, system_packages, python_version, {}
|
|
209
240
|
)
|
|
210
|
-
self.exec_id = result["exec_id"]
|
|
211
241
|
|
|
242
|
+
self.exec_id = result["exec_id"]
|
|
212
243
|
self.system_packages_change = result["packages_changed"]
|
|
213
244
|
self.files_of_interest = result["filenames"]
|
|
214
245
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
246
|
+
@classmethod
|
|
247
|
+
def for_worker( # pylint: disable=too-many-arguments
|
|
248
|
+
cls,
|
|
249
|
+
rootdir,
|
|
250
|
+
exec_id,
|
|
251
|
+
database=None,
|
|
252
|
+
system_packages_change=None,
|
|
253
|
+
files_of_interest=None,
|
|
254
|
+
environment=None,
|
|
255
|
+
):
|
|
256
|
+
instance = cls(rootdir, database=database, readonly=True)
|
|
257
|
+
instance._init_for_worker(
|
|
258
|
+
exec_id, system_packages_change, files_of_interest, environment
|
|
259
|
+
)
|
|
260
|
+
return instance
|
|
261
|
+
|
|
262
|
+
def _init_for_worker(
|
|
263
|
+
self,
|
|
264
|
+
exec_id,
|
|
265
|
+
system_packages_change=None,
|
|
266
|
+
files_of_interest=None,
|
|
267
|
+
environment=None,
|
|
268
|
+
):
|
|
269
|
+
"""Internal method to initialize for worker run."""
|
|
270
|
+
self.exec_id = exec_id
|
|
271
|
+
self.system_packages_change = system_packages_change
|
|
272
|
+
self.files_of_interest = files_of_interest
|
|
273
|
+
if environment:
|
|
274
|
+
self.environment = environment
|
|
275
|
+
elif not hasattr(self, "environment") or self.environment is None:
|
|
276
|
+
self.environment = "default"
|
|
221
277
|
|
|
222
278
|
@property
|
|
223
279
|
def new_db(self):
|
|
224
|
-
|
|
280
|
+
# Only local DB instances have file_created attribute
|
|
281
|
+
if isinstance(self.db, db.DB):
|
|
282
|
+
return self.db.file_created
|
|
283
|
+
return False
|
|
225
284
|
|
|
226
285
|
def close_connection(self):
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
286
|
+
"""Release the storage: the sqlite connection, or the transport of a tmnet proxy.
|
|
287
|
+
|
|
288
|
+
Idempotent. Tests that inspect the data after a run reopen it (see tests/lib4tests.py).
|
|
289
|
+
"""
|
|
290
|
+
database, self.db = self.db, None
|
|
291
|
+
if database is None:
|
|
292
|
+
return
|
|
293
|
+
if isinstance(database, ServerProxy):
|
|
294
|
+
database("close")() # attribute access would be a remote call
|
|
295
|
+
else:
|
|
296
|
+
database.close()
|
|
297
|
+
|
|
298
|
+
def __enter__(self):
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __exit__(self, *args):
|
|
302
|
+
self.close_connection()
|
|
230
303
|
|
|
231
304
|
@property
|
|
232
305
|
def all_tests(self):
|
|
@@ -260,29 +333,27 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
260
333
|
def sync_db_fs_tests(self, retain):
|
|
261
334
|
collected = retain.union(set(self.stable_test_names))
|
|
262
335
|
add = list(collected - set(self.all_tests))
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
for test_name in add
|
|
276
|
-
if is_python_file(home_file(test_name))
|
|
336
|
+
test_execution_file_fps = {
|
|
337
|
+
test_name: {
|
|
338
|
+
"deps": (
|
|
339
|
+
{
|
|
340
|
+
"filename": home_file(test_name),
|
|
341
|
+
"method_checksums": methods_to_checksums(["0match"]),
|
|
342
|
+
"mtime": None,
|
|
343
|
+
"fsha": None,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
277
346
|
}
|
|
278
|
-
|
|
279
|
-
|
|
347
|
+
for test_name in add
|
|
348
|
+
if is_python_file(home_file(test_name))
|
|
349
|
+
}
|
|
350
|
+
if test_execution_file_fps:
|
|
351
|
+
self.save_test_execution_file_fps(test_execution_file_fps)
|
|
280
352
|
|
|
281
353
|
to_delete = list(set(self.all_tests) - collected)
|
|
282
|
-
|
|
283
|
-
database.delete_test_executions(to_delete, self.exec_id)
|
|
354
|
+
self.db.delete_test_executions(to_delete, self.exec_id)
|
|
284
355
|
|
|
285
|
-
def determine_stable(self
|
|
356
|
+
def determine_stable(self):
|
|
286
357
|
files_fshas = {}
|
|
287
358
|
for filename in self.files_of_interest:
|
|
288
359
|
module = self.source_tree.get_file(filename)
|
|
@@ -299,9 +370,6 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
299
370
|
tests = self.db.determine_tests(self.exec_id, files_mhashes)
|
|
300
371
|
affected_tests, self.failing_tests = tests["affected"], tests["failing"]
|
|
301
372
|
|
|
302
|
-
if assert_old:
|
|
303
|
-
self.assert_old_determin_stable(affected_tests)
|
|
304
|
-
|
|
305
373
|
self.all_files = set(self.db.filenames(self.exec_id))
|
|
306
374
|
self.unstable_test_names = set()
|
|
307
375
|
self.unstable_files = set()
|
|
@@ -313,47 +381,6 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
313
381
|
self.stable_test_names = set(self.all_tests) - self.unstable_test_names
|
|
314
382
|
self.stable_files = set(self.all_files) - self.unstable_files
|
|
315
383
|
|
|
316
|
-
def assert_old_determin_stable(self, new_fingerprint_misses):
|
|
317
|
-
filenames_fingerprints = self.db.filenames_fingerprints(self.exec_id)
|
|
318
|
-
|
|
319
|
-
_, fsha_misses = split_filter(
|
|
320
|
-
self.source_tree, check_fsha, filenames_fingerprints
|
|
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
|
|
325
|
-
|
|
326
|
-
changed_file_data = self.db.fetch_changed_file_data(
|
|
327
|
-
[fsha_miss["fingerprint_id"] for fsha_miss in (fsha_misses)],
|
|
328
|
-
self.exec_id,
|
|
329
|
-
)
|
|
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
|
-
|
|
339
|
-
_, fingerprint_misses = split_filter(
|
|
340
|
-
self.source_tree, check_fingerprint, changed_file_data
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
if {fingerprint_miss[1] for fingerprint_miss in fingerprint_misses} != set(
|
|
344
|
-
new_fingerprint_misses
|
|
345
|
-
):
|
|
346
|
-
print("ERROR: old and new fingerprint misses differ.. printing old algo")
|
|
347
|
-
print(
|
|
348
|
-
"\n".join(
|
|
349
|
-
sorted(
|
|
350
|
-
{fingerprint_miss[1] for fingerprint_miss in fingerprint_misses}
|
|
351
|
-
)
|
|
352
|
-
)
|
|
353
|
-
)
|
|
354
|
-
print("printing new algo")
|
|
355
|
-
print("\n".join(sorted(new_fingerprint_misses)))
|
|
356
|
-
|
|
357
384
|
@property
|
|
358
385
|
def avg_durations(self) -> dict:
|
|
359
386
|
stats = defaultdict(lambda: {"test_execution": 0, "sum_duration": 0})
|
|
@@ -420,6 +447,22 @@ def cached_relpath(path, basepath):
|
|
|
420
447
|
return os.path.relpath(path, basepath).replace(os.sep, "/")
|
|
421
448
|
|
|
422
449
|
|
|
450
|
+
# coverage 7.10+ defaults to the sys.monitoring core on CPython 3.14+. That core
|
|
451
|
+
# doesn't support dynamic contexts: sys.monitoring disables a line event once the
|
|
452
|
+
# line has been recorded, so a line executed by several tests of one batch is
|
|
453
|
+
# attributed to the first test only and the other tests lose the dependency.
|
|
454
|
+
# testmon relies on contexts_by_lineno, so it needs the C tracer.
|
|
455
|
+
COVERAGE_CORE = "ctrace"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def force_context_capable_core(cov: Coverage):
|
|
459
|
+
try:
|
|
460
|
+
cov.set_option("run:core", COVERAGE_CORE)
|
|
461
|
+
except CoverageException:
|
|
462
|
+
# coverage < 7.10 has no core option and never defaults to sys.monitoring.
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
|
|
423
466
|
class TestmonCollector:
|
|
424
467
|
coverage_stack: [Coverage] = []
|
|
425
468
|
|
|
@@ -499,9 +542,11 @@ class TestmonCollector:
|
|
|
499
542
|
"testmon doesn't support simultaneous run with pytest-cov when "
|
|
500
543
|
"branch coverage is on. Please disable branch coverage."
|
|
501
544
|
)
|
|
502
|
-
|
|
545
|
+
if subprocess:
|
|
546
|
+
self.setup_subprocess(params["include"])
|
|
503
547
|
self.cov = Coverage(data_file=self.sub_cov_file, config_file=False, **params)
|
|
504
548
|
self.cov._warn_no_data = False
|
|
549
|
+
force_context_capable_core(self.cov)
|
|
505
550
|
if TestmonCollector.coverage_stack:
|
|
506
551
|
TestmonCollector.coverage_stack[-1].stop()
|
|
507
552
|
|
|
@@ -600,11 +645,35 @@ class TestmonCollector:
|
|
|
600
645
|
TestmonCollector.coverage_stack.pop()
|
|
601
646
|
if self.sub_cov_file:
|
|
602
647
|
os.remove(self.sub_cov_file + "_rc")
|
|
648
|
+
os.environ.pop("COVERAGE_CORE", None)
|
|
603
649
|
os.environ.pop("COVERAGE_PROCESS_START", None)
|
|
604
650
|
self.cov = None
|
|
605
651
|
if TestmonCollector.coverage_stack:
|
|
606
652
|
TestmonCollector.coverage_stack[-1].start()
|
|
607
653
|
|
|
654
|
+
def setup_subprocess(self, includes):
|
|
655
|
+
# This can be redone to use functionality from pytest-cov
|
|
656
|
+
if not os.path.exists(".tmontmp"):
|
|
657
|
+
os.makedirs(".tmontmp")
|
|
658
|
+
self.sub_cov_file = os.path.abspath(
|
|
659
|
+
".tmontmp/.testmoncoverage" + str(random.randint(0, 1000000))
|
|
660
|
+
)
|
|
661
|
+
with open(self.sub_cov_file + "_rc", "w", encoding="utf-8") as subprocess_rc:
|
|
662
|
+
rc_content = textwrap.dedent(
|
|
663
|
+
"""\
|
|
664
|
+
[run]
|
|
665
|
+
data_file = {}
|
|
666
|
+
include = {}
|
|
667
|
+
parallel=True
|
|
668
|
+
"""
|
|
669
|
+
).format(
|
|
670
|
+
self.sub_cov_file,
|
|
671
|
+
"\n ".join(includes),
|
|
672
|
+
)
|
|
673
|
+
subprocess_rc.write(rc_content)
|
|
674
|
+
os.environ["COVERAGE_PROCESS_START"] = self.sub_cov_file + "_rc"
|
|
675
|
+
os.environ["COVERAGE_CORE"] = COVERAGE_CORE
|
|
676
|
+
|
|
608
677
|
|
|
609
678
|
def eval_environment(environment, **kwargs):
|
|
610
679
|
if not environment:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_testmon-2.1.4 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|