pytest-testmon 2.1.1.dev0__tar.gz → 2.1.4__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.
Files changed (22) hide show
  1. {pytest-testmon-2.1.1.dev0/pytest_testmon.egg-info → pytest_testmon-2.1.4}/PKG-INFO +4 -3
  2. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pyproject.toml +2 -1
  3. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4/pytest_testmon.egg-info}/PKG-INFO +4 -3
  4. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pytest_testmon.egg-info/SOURCES.txt +1 -2
  5. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pytest_testmon.egg-info/requires.txt +1 -1
  6. pytest_testmon-2.1.4/testmon/__init__.py +2 -0
  7. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/common.py +53 -8
  8. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/configure.py +1 -1
  9. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/db.py +76 -62
  10. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/process_code.py +31 -11
  11. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/pytest_testmon.py +38 -23
  12. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/testmon/testmon_core.py +72 -23
  13. pytest-testmon-2.1.1.dev0/testmon/__init__.py +0 -2
  14. pytest-testmon-2.1.1.dev0/testmon/tox_testmon.py +0 -44
  15. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/LICENSE +0 -0
  16. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/MANIFEST.in +0 -0
  17. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/README.md +0 -0
  18. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pytest_testmon.egg-info/dependency_links.txt +0 -0
  19. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pytest_testmon.egg-info/entry_points.txt +0 -0
  20. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/pytest_testmon.egg-info/top_level.txt +0 -0
  21. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/setup.cfg +0 -0
  22. {pytest-testmon-2.1.1.dev0 → pytest_testmon-2.1.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pytest-testmon
3
- Version: 2.1.1.dev0
3
+ Version: 2.1.4
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,8 +18,9 @@ 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<8,>=5
21
+ Requires-Dist: pytest<9,>=5
22
22
  Requires-Dist: coverage<8,>=6
23
+ Dynamic: license-file
23
24
 
24
25
  <img src=https://user-images.githubusercontent.com/135344/219700265-0a9b152f-7285-4607-bbce-0c9aeddd520b.svg width=300>
25
26
 
@@ -27,10 +27,11 @@ classifiers = [
27
27
  "Framework :: Pytest",
28
28
  ]
29
29
  dependencies = [
30
- "pytest>=5,<8",
30
+ "pytest>=5,<9",
31
31
  "coverage>=6,<8",
32
32
  ]
33
33
 
34
+ # specify testmon and tmserver as packages of this project
34
35
  [tool.setuptools]
35
36
  packages = [
36
37
  "testmon",
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pytest-testmon
3
- Version: 2.1.1.dev0
3
+ Version: 2.1.4
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,8 +18,9 @@ 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<8,>=5
21
+ Requires-Dist: pytest<9,>=5
22
22
  Requires-Dist: coverage<8,>=6
23
+ Dynamic: license-file
23
24
 
24
25
  <img src=https://user-images.githubusercontent.com/135344/219700265-0a9b152f-7285-4607-bbce-0c9aeddd520b.svg width=300>
25
26
 
@@ -15,5 +15,4 @@ testmon/configure.py
15
15
  testmon/db.py
16
16
  testmon/process_code.py
17
17
  testmon/pytest_testmon.py
18
- testmon/testmon_core.py
19
- testmon/tox_testmon.py
18
+ testmon/testmon_core.py
@@ -1,2 +1,2 @@
1
- pytest<8,>=5
1
+ pytest<9,>=5
2
2
  coverage<8,>=6
@@ -0,0 +1,2 @@
1
+ """PYTEST_DONT_REWRITE"""
2
+ TESTMON_VERSION = "2.1.4"
@@ -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 ((pkg.project_name, pkg.version) for pkg in pkg_resources.working_set)
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.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
30
- return logging.getLogger(name)
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 ignore or not package in ignore
84
+ if package not in ignore and package != "UNKNOWN" and version != "0.0.0"
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
- def git_path(start_path=None):
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
@@ -2,7 +2,7 @@ import sys
2
2
  import re
3
3
 
4
4
  try:
5
- from coverage.tracer import CTracer as Tracer
5
+ from coverage.tracer import CTracer as Tracer # pylint: disable=no-name-in-module
6
6
  except ImportError:
7
7
  from coverage.pytracer import PyTracer as Tracer
8
8
 
@@ -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
- connection = sqlite3.connect(
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(files_methods_checksums, record):
36
- file = record[0]
37
- fingerprint = record[2]
38
-
39
- if file in files_methods_checksums and files_methods_checksums[file]:
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
- class DB:
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
- new_db = not os.path.exists(datafile)
65
+ file_exists = os.path.exists(datafile)
50
66
 
51
67
  connection = connect(datafile, readonly)
52
- self.con = connection
53
- old_format = self._check_data_version(datafile)
68
+ connection, old_format = check_data_version(
69
+ connection, datafile, self.version_compatibility()
70
+ )
71
+ self.con = connection_options(connection)
54
72
 
55
- if new_db or old_format:
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(self, exec_id, duration=None, select=True):
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(self, filename, fsha, method_checksums):
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(self, files_fshas, exec_id):
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
- new_method_misses = []
539
+ method_misses = []
529
540
  for result in results:
530
- if not check_fingerprint_db(files_mhashes, result):
531
- new_method_misses.append(result[1])
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": new_method_misses, "failing": failing_tests}
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
  """
@@ -643,6 +656,7 @@ class DB:
643
656
  id, environment_name, system_packages, python_version
644
657
  FROM environment
645
658
  WHERE environment_name = ?
659
+ ORDER BY id DESC
646
660
  """,
647
661
  (environment_name,),
648
662
  ).fetchone()
@@ -683,13 +697,13 @@ class DB:
683
697
 
684
698
  return environment_id, packages_changed
685
699
 
686
- def initiate_execution(
700
+ def initiate_execution( # pylint: disable= R0913 W0613
687
701
  self,
688
- environment_name,
689
- system_packages,
690
- python_version,
691
- execution_metadata,
692
- ):
702
+ environment_name: str,
703
+ system_packages: str,
704
+ python_version: str,
705
+ execution_metadata: dict, # pylint: disable=unused-argument
706
+ ) -> [int, list]: # exec_id # changed_file_data # future_string2
693
707
  exec_id, packages_changed = self.fetch_or_create_environment(
694
708
  environment_name, system_packages, python_version
695
709
  )
@@ -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 = []
@@ -156,10 +168,16 @@ class Module:
156
168
  self.dump_and_block(item, _next_lineno(node, i, end))
157
169
  )
158
170
  if into_block and node:
171
+ # Use last child's end_lineno only if end is not available (None)
172
+ if end is None:
173
+ last_child = node[-1]
174
+ block_end = getattr(last_child, "end_lineno", None)
175
+ else:
176
+ block_end = end
159
177
  self._blocks.append(
160
178
  Block(
161
179
  node[0].lineno,
162
- end,
180
+ block_end,
163
181
  code=str(self.counter) + ":" + ", ".join(representations),
164
182
  name=name,
165
183
  )
@@ -183,6 +201,8 @@ class Module:
183
201
  tree = ast.parse(self.source_code, filename="<unknown>")
184
202
  self.dump_and_block(tree, len(lines), name="<module>")
185
203
  except SyntaxError:
204
+ # We can continue without blocks because no tests depending on this file will ever get executed,
205
+ # so no node depending on this checksum and mtime will ever be written to db.
186
206
  pass
187
207
  else:
188
208
  self._blocks = [Block(1, len(lines), self.source_code)]
@@ -199,8 +219,8 @@ class Module:
199
219
  return methods_to_checksums([block.checksum for block in self.blocks])
200
220
 
201
221
 
202
- def read_source_sha(filename):
203
- # source_bytes: Optional[bytes]
222
+ def read_source_sha(filename: str):
223
+ source_bytes: Optional[bytes]
204
224
 
205
225
  try:
206
226
  with open(filename, "rb") as file:
@@ -242,7 +262,7 @@ def get_files_shas(directory):
242
262
  return noncached_get_files_shas(directory)
243
263
 
244
264
 
245
- def get_source_sha(directory, filename):
265
+ def get_source_sha(directory: "str", filename: "str"):
246
266
  try:
247
267
  sha = get_files_shas(directory)[filename]
248
268
  return (None, sha)
@@ -256,7 +276,7 @@ def match_fingerprint_source(source_code, fingerprint, ext="py"):
256
276
  return match_fingerprint(module, fingerprint)
257
277
 
258
278
 
259
- def match_fingerprint(module, fingerprint):
279
+ def match_fingerprint(module: Module, fingerprint):
260
280
  if set(fingerprint) - set(module.checksums):
261
281
  return False
262
282
  return True
@@ -267,8 +287,8 @@ def create_fingerprint_source(source_code, lines, ext="py"):
267
287
  return create_fingerprint(module, lines)
268
288
 
269
289
 
270
- def create_fingerprint(module, covered_lines):
271
- blocks = module.blocks
290
+ def create_fingerprint(module, covered_lines) -> [int]:
291
+ blocks: [Block] = module.blocks
272
292
  method_reprs = []
273
293
  line_index = 0
274
294
  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
- "🙏🙏 go to https://testmon.org/survey to leave feedback ✅❌"
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 changed_files_msg == "0" and len(stable_files) == 0 and not packages_change:
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(self, collector, name, obj):
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(self, session, config, items):
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(self, item, nextitem):
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(self, node, ids):
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, path, config):
462
- strpath = cached_relpath(path.strpath, config.rootdir.strpath)
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(self, session, config, items):
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(disk, record):
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(os.path.join(self.rootdir, get_data_file_path()))
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__(self, rootdir, testmon_labels=None, cov_plugin=None):
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,2 +0,0 @@
1
- """PYTEST_DONT_REWRITE"""
2
- TESTMON_VERSION = "2.1.1.dev0"
@@ -1,44 +0,0 @@
1
- import os
2
-
3
- from pathlib import Path
4
- import pluggy
5
-
6
- from tox.config import DepConfig
7
-
8
- hookimpl = pluggy.HookimplMarker("tox")
9
-
10
-
11
- def _uses_testmon(envconfig):
12
-
13
- for command in envconfig.commands:
14
- if "--testmon" in command:
15
- return True
16
- return False
17
-
18
-
19
- def touch_stampfile(venv):
20
- Path(venv.path.join(".testmon_installed")).touch()
21
-
22
-
23
- def installed_testmon(venv):
24
- return os.path.exists(venv.path.join(".testmon_installed"))
25
-
26
-
27
- @hookimpl
28
- def tox_runenvreport(venv, action):
29
- if "TESTMON_DATAFILE" in venv.envconfig.setenv:
30
- datafile = venv.envconfig.setenv["TESTMON_DATAFILE"]
31
- action.setactivity("testmon", f"keeping TESTMON_DATAFILE={datafile}")
32
- else:
33
- datafile = str(venv.path.join(".testmondata"))
34
- action.setactivity("testmon", f"setting TESTMON_DATAFILE={datafile}")
35
- venv.envconfig.setenv["TESTMON_DATAFILE"] = datafile
36
-
37
- if _uses_testmon(venv.envconfig) and "pytest-testmon" not in (
38
- x.name for x in venv.envconfig.deps
39
- ):
40
- if not installed_testmon(venv):
41
- action.setactivity("testmon", "installing pytest-testmon")
42
- venv._install([DepConfig("pytest-testmon")], action=action)
43
-
44
- touch_stampfile(venv)