pytest-testmon 2.2.0__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.2.0/pytest_testmon.egg-info → pytest_testmon-2.3.0b0}/PKG-INFO +4 -2
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pyproject.toml +22 -28
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0/pytest_testmon.egg-info}/PKG-INFO +4 -2
- pytest_testmon-2.3.0b0/testmon/__init__.py +3 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/testmon/configure.py +2 -1
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/testmon/db.py +28 -31
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/testmon/process_code.py +14 -7
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/testmon/pytest_testmon.py +37 -33
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/testmon/testmon_core.py +79 -27
- pytest_testmon-2.2.0/testmon/__init__.py +0 -2
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/LICENSE +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/MANIFEST.in +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/README.md +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/SOURCES.txt +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/dependency_links.txt +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/entry_points.txt +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/requires.txt +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/pytest_testmon.egg-info/top_level.txt +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/setup.cfg +0 -0
- {pytest_testmon-2.2.0 → pytest_testmon-2.3.0b0}/setup.py +0 -0
- {pytest_testmon-2.2.0 → 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,7 +17,7 @@ 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
23
|
Requires-Dist: pytest<10,>=5
|
|
@@ -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 = [
|
|
@@ -31,34 +31,28 @@ dependencies = [
|
|
|
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,7 +17,7 @@ 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
23
|
Requires-Dist: pytest<10,>=5
|
|
@@ -75,7 +75,8 @@ 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 []
|
|
@@ -5,10 +5,8 @@ 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
|
|
11
|
-
|
|
9
|
+
from testmon.process_code import blob_to_checksums, checksums_to_blob
|
|
12
10
|
|
|
13
11
|
DATA_VERSION = 14
|
|
14
12
|
|
|
@@ -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
|
|
@@ -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 {
|
|
@@ -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,14 +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,
|
|
242
249
|
encoding="utf-8",
|
|
243
250
|
check=True,
|
|
244
251
|
)
|
|
245
|
-
except (FileNotFoundError, CalledProcessError):
|
|
252
|
+
except (FileNotFoundError, NotADirectoryError, CalledProcessError):
|
|
246
253
|
return all_shas
|
|
247
254
|
|
|
248
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,7 +106,12 @@ 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",
|
|
@@ -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_", ""))
|
|
@@ -195,7 +202,7 @@ def init_testmon_data(config: Config):
|
|
|
195
202
|
if running_as == "worker" and exec_id is not None:
|
|
196
203
|
# Initialize for xdist worker run
|
|
197
204
|
testmon_data: TestmonData = TestmonData.for_worker(
|
|
198
|
-
rootdir=config.
|
|
205
|
+
rootdir=str(config.rootpath),
|
|
199
206
|
exec_id=exec_id,
|
|
200
207
|
database=rpc_proxy,
|
|
201
208
|
system_packages_change=system_packages_change,
|
|
@@ -205,7 +212,7 @@ def init_testmon_data(config: Config):
|
|
|
205
212
|
else:
|
|
206
213
|
# Initialize for local run (controller or single process)
|
|
207
214
|
testmon_data: TestmonData = TestmonData.for_local_run(
|
|
208
|
-
rootdir=config.
|
|
215
|
+
rootdir=str(config.rootpath),
|
|
209
216
|
database=rpc_proxy,
|
|
210
217
|
environment=environment,
|
|
211
218
|
system_packages=system_packages,
|
|
@@ -234,7 +241,7 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
|
|
|
234
241
|
config.pluginmanager.register(
|
|
235
242
|
TestmonCollect(
|
|
236
243
|
TestmonCollector(
|
|
237
|
-
config.
|
|
244
|
+
str(config.rootpath),
|
|
238
245
|
testmon_labels=testmon_options(config),
|
|
239
246
|
cov_plugin=cov_plugin,
|
|
240
247
|
),
|
|
@@ -243,8 +250,8 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
|
|
|
243
250
|
),
|
|
244
251
|
"TestmonCollect",
|
|
245
252
|
)
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
if (should_select or should_collect) and config.pluginmanager.hasplugin("xdist"):
|
|
254
|
+
config.pluginmanager.register(TestmonXdistSync(sync=should_collect))
|
|
248
255
|
|
|
249
256
|
|
|
250
257
|
def pytest_configure(config):
|
|
@@ -266,6 +273,10 @@ def pytest_configure(config):
|
|
|
266
273
|
)
|
|
267
274
|
config.testmon_config: TmConf = tm_conf
|
|
268
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.")
|
|
269
280
|
try:
|
|
270
281
|
init_testmon_data(config)
|
|
271
282
|
register_plugins(config, tm_conf.select, tm_conf.collect, cov_plugin)
|
|
@@ -362,9 +373,7 @@ class TestmonCollect:
|
|
|
362
373
|
self._sessionstarttime = time.time()
|
|
363
374
|
|
|
364
375
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
365
|
-
def pytest_pycollect_makeitem(
|
|
366
|
-
self, collector, name, obj
|
|
367
|
-
): # pylint: disable=unused-argument
|
|
376
|
+
def pytest_pycollect_makeitem(self, collector, name, obj): # pylint: disable=unused-argument
|
|
368
377
|
makeitem_result = yield
|
|
369
378
|
items = makeitem_result.get_result() or []
|
|
370
379
|
try:
|
|
@@ -375,9 +384,7 @@ class TestmonCollect:
|
|
|
375
384
|
pass
|
|
376
385
|
|
|
377
386
|
@pytest.hookimpl(tryfirst=True)
|
|
378
|
-
def pytest_collection_modifyitems(
|
|
379
|
-
self, session, config, items
|
|
380
|
-
): # pylint: disable=unused-argument
|
|
387
|
+
def pytest_collection_modifyitems(self, session, config, items): # pylint: disable=unused-argument
|
|
381
388
|
should_sync = not session.testsfailed and self._running_as in (
|
|
382
389
|
"single",
|
|
383
390
|
"controller",
|
|
@@ -386,9 +393,7 @@ class TestmonCollect:
|
|
|
386
393
|
config.testmon_data.sync_db_fs_tests(retain=set(self.raw_test_names))
|
|
387
394
|
|
|
388
395
|
@pytest.hookimpl(hookwrapper=True)
|
|
389
|
-
def pytest_runtest_protocol(
|
|
390
|
-
self, item, nextitem
|
|
391
|
-
): # pylint: disable=unused-argument
|
|
396
|
+
def pytest_runtest_protocol(self, item, nextitem): # pylint: disable=unused-argument
|
|
392
397
|
self.testmon.start_testmon(item.nodeid, nextitem.nodeid if nextitem else None)
|
|
393
398
|
result = yield
|
|
394
399
|
if result.excinfo and issubclass(result.excinfo[0], BaseException):
|
|
@@ -441,8 +446,9 @@ class TestmonCollect:
|
|
|
441
446
|
|
|
442
447
|
|
|
443
448
|
class TestmonXdistSync:
|
|
444
|
-
def __init__(self):
|
|
449
|
+
def __init__(self, sync=True):
|
|
445
450
|
self.await_nodes = 0
|
|
451
|
+
self.sync = sync
|
|
446
452
|
|
|
447
453
|
def pytest_configure_node(self, node):
|
|
448
454
|
"""
|
|
@@ -459,25 +465,25 @@ class TestmonXdistSync:
|
|
|
459
465
|
if running_as != "controller":
|
|
460
466
|
return # Safety check: only run on controller
|
|
461
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.
|
|
471
|
+
self.await_nodes += 1
|
|
472
|
+
|
|
462
473
|
# Only proceed if testmon_data has been initialized and workerinput exists
|
|
463
474
|
if hasattr(node.config, "testmon_data") and hasattr(node, "workerinput"):
|
|
464
475
|
testmon_data: TestmonData = node.config.testmon_data
|
|
465
476
|
node.workerinput["testmon_exec_id"] = testmon_data.exec_id
|
|
466
|
-
node.workerinput[
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
node.workerinput[
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def pytest_testnodeready(self, node): # pylint: disable=unused-argument
|
|
474
|
-
self.await_nodes += 1
|
|
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
|
+
)
|
|
475
483
|
|
|
476
|
-
def pytest_xdist_node_collection_finished(
|
|
477
|
-
self, node, ids
|
|
478
|
-
): # pylint: disable=invalid-name
|
|
484
|
+
def pytest_xdist_node_collection_finished(self, node, ids): # pylint: disable=invalid-name
|
|
479
485
|
self.await_nodes += -1
|
|
480
|
-
if self.await_nodes == 0:
|
|
486
|
+
if self.await_nodes == 0 and self.sync:
|
|
481
487
|
node.config.testmon_data.sync_db_fs_tests(retain=set(ids))
|
|
482
488
|
|
|
483
489
|
|
|
@@ -530,15 +536,13 @@ class TestmonSelect:
|
|
|
530
536
|
self._interrupted = False
|
|
531
537
|
|
|
532
538
|
def pytest_ignore_collect(self, collection_path: Path, config):
|
|
533
|
-
strpath = cached_relpath(str(collection_path), config.
|
|
539
|
+
strpath = cached_relpath(str(collection_path), str(config.rootpath))
|
|
534
540
|
if strpath in self.deselected_files and self.config.testmon_config.select:
|
|
535
541
|
return True
|
|
536
542
|
return None
|
|
537
543
|
|
|
538
544
|
@pytest.hookimpl(trylast=True)
|
|
539
|
-
def pytest_collection_modifyitems(
|
|
540
|
-
self, session, config, items
|
|
541
|
-
): # pylint: disable=unused-argument
|
|
545
|
+
def pytest_collection_modifyitems(self, session, config, items): # pylint: disable=unused-argument
|
|
542
546
|
selected = []
|
|
543
547
|
deselected = []
|
|
544
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
|
|
@@ -234,9 +234,7 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
234
234
|
),
|
|
235
235
|
exc,
|
|
236
236
|
)
|
|
237
|
-
self.db = db.DB(
|
|
238
|
-
os.path.join(self.rootdir, get_data_file_path())
|
|
239
|
-
) # pylint: disable=invalid-name
|
|
237
|
+
self.db = db.DB(os.path.join(self.rootdir, get_data_file_path())) # pylint: disable=invalid-name
|
|
240
238
|
result = self.db.initiate_execution(
|
|
241
239
|
self.environment, system_packages, python_version, {}
|
|
242
240
|
)
|
|
@@ -285,9 +283,23 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
285
283
|
return False
|
|
286
284
|
|
|
287
285
|
def close_connection(self):
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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()
|
|
291
303
|
|
|
292
304
|
@property
|
|
293
305
|
def all_tests(self):
|
|
@@ -321,27 +333,25 @@ class TestmonData: # pylint: disable=too-many-instance-attributes
|
|
|
321
333
|
def sync_db_fs_tests(self, retain):
|
|
322
334
|
collected = retain.union(set(self.stable_test_names))
|
|
323
335
|
add = list(collected - set(self.all_tests))
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
for test_name in add
|
|
337
|
-
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
|
+
)
|
|
338
346
|
}
|
|
339
|
-
|
|
340
|
-
|
|
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)
|
|
341
352
|
|
|
342
353
|
to_delete = list(set(self.all_tests) - collected)
|
|
343
|
-
|
|
344
|
-
database.delete_test_executions(to_delete, self.exec_id)
|
|
354
|
+
self.db.delete_test_executions(to_delete, self.exec_id)
|
|
345
355
|
|
|
346
356
|
def determine_stable(self):
|
|
347
357
|
files_fshas = {}
|
|
@@ -437,6 +447,22 @@ def cached_relpath(path, basepath):
|
|
|
437
447
|
return os.path.relpath(path, basepath).replace(os.sep, "/")
|
|
438
448
|
|
|
439
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
|
+
|
|
440
466
|
class TestmonCollector:
|
|
441
467
|
coverage_stack: [Coverage] = []
|
|
442
468
|
|
|
@@ -516,9 +542,11 @@ class TestmonCollector:
|
|
|
516
542
|
"testmon doesn't support simultaneous run with pytest-cov when "
|
|
517
543
|
"branch coverage is on. Please disable branch coverage."
|
|
518
544
|
)
|
|
519
|
-
|
|
545
|
+
if subprocess:
|
|
546
|
+
self.setup_subprocess(params["include"])
|
|
520
547
|
self.cov = Coverage(data_file=self.sub_cov_file, config_file=False, **params)
|
|
521
548
|
self.cov._warn_no_data = False
|
|
549
|
+
force_context_capable_core(self.cov)
|
|
522
550
|
if TestmonCollector.coverage_stack:
|
|
523
551
|
TestmonCollector.coverage_stack[-1].stop()
|
|
524
552
|
|
|
@@ -617,11 +645,35 @@ class TestmonCollector:
|
|
|
617
645
|
TestmonCollector.coverage_stack.pop()
|
|
618
646
|
if self.sub_cov_file:
|
|
619
647
|
os.remove(self.sub_cov_file + "_rc")
|
|
648
|
+
os.environ.pop("COVERAGE_CORE", None)
|
|
620
649
|
os.environ.pop("COVERAGE_PROCESS_START", None)
|
|
621
650
|
self.cov = None
|
|
622
651
|
if TestmonCollector.coverage_stack:
|
|
623
652
|
TestmonCollector.coverage_stack[-1].start()
|
|
624
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
|
+
|
|
625
677
|
|
|
626
678
|
def eval_environment(environment, **kwargs):
|
|
627
679
|
if not environment:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_testmon-2.2.0 → 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
|
|
File without changes
|