pytest-testmon 2.1.1__tar.gz → 2.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {pytest-testmon-2.1.1/pytest_testmon.egg-info → pytest_testmon-2.1.3}/PKG-INFO +1 -1
  2. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pyproject.toml +1 -0
  3. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3/pytest_testmon.egg-info}/PKG-INFO +1 -1
  4. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/__init__.py +1 -1
  5. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/common.py +42 -5
  6. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/configure.py +1 -1
  7. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/db.py +43 -32
  8. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/process_code.py +24 -10
  9. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/pytest_testmon.py +37 -22
  10. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/testmon_core.py +68 -23
  11. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/testmon/tox_testmon.py +1 -3
  12. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/LICENSE +0 -0
  13. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/MANIFEST.in +0 -0
  14. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/README.md +0 -0
  15. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/SOURCES.txt +0 -0
  16. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/dependency_links.txt +0 -0
  17. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/entry_points.txt +0 -0
  18. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/requires.txt +0 -0
  19. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/pytest_testmon.egg-info/top_level.txt +0 -0
  20. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/setup.cfg +0 -0
  21. {pytest-testmon-2.1.1 → pytest_testmon-2.1.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytest-testmon
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: selects tests affected by changed files and methods
5
5
  Author-email: Tibor Arpas <tibor@testmon.org>
6
6
  License: MIT
@@ -31,6 +31,7 @@ dependencies = [
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
1
  Metadata-Version: 2.1
2
2
  Name: pytest-testmon
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: selects tests affected by changed files and methods
5
5
  Author-email: Tibor Arpas <tibor@testmon.org>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """PYTEST_DONT_REWRITE"""
2
- TESTMON_VERSION = "2.1.1"
2
+ TESTMON_VERSION = "2.1.3"
@@ -3,6 +3,7 @@ import os
3
3
  import re
4
4
 
5
5
  try:
6
+ # Python >= 3.8
6
7
  import importlib.metadata
7
8
 
8
9
  def get_system_packages_raw():
@@ -12,14 +13,46 @@ try:
12
13
  )
13
14
 
14
15
  except ImportError:
16
+ # Python < 3.7
15
17
  import pkg_resources
16
18
 
17
19
  def get_system_packages_raw():
18
- return ((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
@@ -30,6 +63,7 @@ def get_logger(name):
30
63
  handler = logging.StreamHandler()
31
64
  handler.setFormatter(formatter)
32
65
 
66
+ # Configure the logger
33
67
  tm_logger = logging.getLogger(name)
34
68
  tm_logger.setLevel(logging.INFO)
35
69
  tm_logger.addHandler(handler)
@@ -55,16 +89,19 @@ def get_system_packages(ignore=None):
55
89
 
56
90
  def drop_patch_version(system_packages):
57
91
  return re.sub(
58
- r"\b([\w_-]+\s\d+\.\d+)\.\w+\b",
92
+ r"\b([\w_-]+\s\d+\.\d+)\.\w+\b", # extract (Package M.N).P / drop .patch
59
93
  r"\1",
60
94
  system_packages,
61
95
  )
62
96
 
63
97
 
64
- def git_path(start_path=None):
98
+ #
99
+ # .git utilities
100
+ #
101
+ def git_path(start_path=None): # parent dirs only
65
102
  start_path = Path(start_path or os.getcwd()).resolve()
66
103
  current_path = start_path
67
- while current_path != current_path.parent:
104
+ while current_path != current_path.parent: # '/'.parent == '/'
68
105
  path = current_path / ".git"
69
106
  if path.exists() and path.is_dir():
70
107
  return str(path)
@@ -81,7 +118,7 @@ def git_current_branch(path=None):
81
118
  with open(git_head_file, "r", encoding="utf8") as head_file:
82
119
  head_content = head_file.read().strip()
83
120
  if head_content.startswith("ref:"):
84
- return head_content.split("/")[-1]
121
+ return head_content.split("/")[-1] # e.g. ref: refs/heads/master
85
122
  except FileNotFoundError:
86
123
  pass
87
124
  return None
@@ -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
 
@@ -34,7 +36,9 @@ def connection_options(connection):
34
36
  return connection
35
37
 
36
38
 
37
- def check_fingerprint_db(files_methods_checksums, file_name, fingerprint):
39
+ def check_fingerprint_db(
40
+ files_methods_checksums, file_name, fingerprint: ChangedFileData
41
+ ): # filename name method_checksums id failed
38
42
  if file_name in files_methods_checksums and files_methods_checksums[file_name]:
39
43
  if set(fingerprint) - set(files_methods_checksums[file_name]):
40
44
  return False
@@ -55,7 +59,7 @@ def check_data_version(connection, datafile, data_version):
55
59
  return connection, True
56
60
 
57
61
 
58
- class DB:
62
+ class DB: # pylint: disable=too-many-public-methods
59
63
  def __init__(self, datafile, readonly=False):
60
64
  self._readonly = readonly
61
65
  file_exists = os.path.exists(datafile)
@@ -84,10 +88,10 @@ class DB:
84
88
  def __exit__(self, *args, **kwargs):
85
89
  self.con.__exit__(*args, **kwargs)
86
90
 
87
- def _test_execution_fk_column(self):
91
+ def _test_execution_fk_column(self) -> str:
88
92
  return "environment_id"
89
93
 
90
- def _test_execution_fk_table(self):
94
+ def _test_execution_fk_table(self) -> str:
91
95
  return "environment"
92
96
 
93
97
  def update_mtimes(self, new_mtimes):
@@ -96,7 +100,9 @@ class DB:
96
100
  "UPDATE file_fp SET mtime=?, fsha=? WHERE id = ?", new_mtimes
97
101
  )
98
102
 
99
- def finish_execution(self, exec_id, duration=None, select=True):
103
+ def finish_execution(
104
+ self, exec_id, duration=None, select=True
105
+ ): # pylint: disable=unused-argument
100
106
  self.update_saving_stats(exec_id, select)
101
107
  self.fetch_or_create_file_fp.cache_clear()
102
108
  with self.con as con:
@@ -187,7 +193,9 @@ class DB:
187
193
  )
188
194
 
189
195
  @lru_cache(1000)
190
- def fetch_or_create_file_fp(self, filename, fsha, method_checksums):
196
+ def fetch_or_create_file_fp(
197
+ self, filename, fsha, method_checksums
198
+ ): # pylint: disable=R0801
191
199
  cursor = self.con.cursor()
192
200
  try:
193
201
  cursor.execute(
@@ -200,7 +208,7 @@ class DB:
200
208
  )
201
209
 
202
210
  fingerprint_id = cursor.lastrowid
203
- except sqlite3.IntegrityError:
211
+ except sqlite3.IntegrityError: # rather fetching existing fingerprint
204
212
  fingerprint_id, *_ = cursor.execute(
205
213
  """
206
214
  SELECT
@@ -215,15 +223,15 @@ class DB:
215
223
 
216
224
  return fingerprint_id
217
225
 
218
- def _insert_test_execution(
226
+ def _insert_test_execution( # pylint: disable=too-many-arguments
219
227
  self,
220
228
  con,
221
- exec_id,
222
- test_name,
223
- duration,
224
- failed,
225
- forced,
226
- ):
229
+ exec_id: int,
230
+ test_name: "str",
231
+ duration: "float",
232
+ failed: "bool",
233
+ forced: bool,
234
+ ) -> int:
227
235
  cursor = con.cursor()
228
236
  cursor.execute(
229
237
  f"""
@@ -241,7 +249,7 @@ class DB:
241
249
  )
242
250
  return cursor.lastrowid
243
251
 
244
- def insert_test_file_fps(self, tests_deps_n_outcomes, exec_id=None):
252
+ def insert_test_file_fps(self, tests_deps_n_outcomes: TestExecutions, exec_id=None):
245
253
  assert exec_id
246
254
  with self.con as con:
247
255
  cursor = con.cursor()
@@ -306,7 +314,7 @@ class DB:
306
314
  )
307
315
  result = cursor.fetchone()
308
316
  if result:
309
- return json.loads(result[0])
317
+ return json.loads(result[0]) # zlib.decompress(result[0]).decode('utf-8)'))
310
318
  return default
311
319
 
312
320
  def increment_attributes(self, attributes_to_increment, exec_id=None):
@@ -329,10 +337,10 @@ class DB:
329
337
  ],
330
338
  )
331
339
 
332
- def _create_metadata_statement(self):
340
+ def _create_metadata_statement(self) -> str:
333
341
  return """CREATE TABLE metadata (dataid TEXT PRIMARY KEY, data TEXT);"""
334
342
 
335
- def _create_environment_statement(self):
343
+ def _create_environment_statement(self) -> str:
336
344
  return """
337
345
  CREATE TABLE environment (
338
346
  id INTEGER PRIMARY KEY ASC,
@@ -342,7 +350,7 @@ class DB:
342
350
  UNIQUE (environment_name, system_packages, python_version)
343
351
  );"""
344
352
 
345
- def _create_test_execution_statement(self):
353
+ def _create_test_execution_statement(self) -> str: # pylint: disable=invalid-name
346
354
  return f"""
347
355
  CREATE TABLE test_execution (
348
356
  id INTEGER PRIMARY KEY ASC,
@@ -355,10 +363,10 @@ class DB:
355
363
  CREATE INDEX test_execution_fk_name ON test_execution ({self._test_execution_fk_column()}, test_name);
356
364
  """
357
365
 
358
- def _create_temp_tables_statement(self):
366
+ def _create_temp_tables_statement(self) -> str:
359
367
  return ""
360
368
 
361
- def _local_temp_tables_statement(self):
369
+ def _local_temp_tables_statement(self) -> str:
362
370
  return """
363
371
  CREATE TEMPORARY TABLE changed_files_fshas (exec_id INTEGER, filename TEXT, fsha TEXT);
364
372
  CREATE INDEX changed_files_fshas_mcall ON changed_files_fshas (exec_id, filename, fsha);
@@ -367,7 +375,7 @@ class DB:
367
375
  CREATE INDEX changed_files_mhashes_eid ON changed_files_mhashes (exec_id);
368
376
  """
369
377
 
370
- def _create_file_fp_statement(self):
378
+ def _create_file_fp_statement(self) -> str:
371
379
  return """
372
380
  CREATE TABLE file_fp
373
381
  (
@@ -379,9 +387,9 @@ class DB:
379
387
  UNIQUE (filename, fsha, method_checksums)
380
388
  );"""
381
389
 
382
- def _create_test_execution_ffp_statement(
390
+ def _create_test_execution_ffp_statement( # pylint: disable=invalid-name
383
391
  self,
384
- ):
392
+ ) -> str:
385
393
  return """
386
394
  CREATE TABLE test_execution_file_fp (
387
395
  test_execution_id INTEGER,
@@ -414,7 +422,7 @@ class DB:
414
422
 
415
423
  connection.execute(f"PRAGMA user_version = {self.version_compatibility()}")
416
424
 
417
- def fetch_changed_file_data(self, changed_fingerprints, exec_id):
425
+ def fetch_changed_file_data(self, changed_fingerprints, exec_id) -> []:
418
426
  in_clause_questionsmarks = ", ".join("?" * len(changed_fingerprints))
419
427
  result = []
420
428
  for row in self.con.execute(
@@ -451,7 +459,9 @@ class DB:
451
459
 
452
460
  return result
453
461
 
454
- def fetch_unknown_files(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
455
465
  with self.con as con:
456
466
  con.execute("DELETE FROM changed_files_fshas WHERE exec_id = ?", (exec_id,))
457
467
  con.executemany(
@@ -598,6 +608,7 @@ class DB:
598
608
 
599
609
  return [row[0] for row in cursor]
600
610
 
611
+ # TODO unify with filenames? Restrict not to go into ancient history, but not miss combinations?
601
612
  def all_filenames(self):
602
613
  cursor = self.con.execute(
603
614
  """
@@ -685,13 +696,13 @@ class DB:
685
696
 
686
697
  return environment_id, packages_changed
687
698
 
688
- def initiate_execution(
699
+ def initiate_execution( # pylint: disable= R0913 W0613
689
700
  self,
690
- environment_name,
691
- system_packages,
692
- python_version,
693
- execution_metadata,
694
- ):
701
+ environment_name: str,
702
+ system_packages: str,
703
+ python_version: str,
704
+ execution_metadata: dict, # pylint: disable=unused-argument
705
+ ) -> [int, list]: # exec_id # changed_file_data # future_string2
695
706
  exec_id, packages_changed = self.fetch_or_create_environment(
696
707
  environment_name, system_packages, python_version
697
708
  )
@@ -31,7 +31,7 @@ def debug_blob_to_code(blob):
31
31
  return blob.split(";\n")
32
32
 
33
33
 
34
- def methods_to_checksums(blocks):
34
+ def methods_to_checksums(blocks) -> [int]:
35
35
  checksums = []
36
36
  for block in blocks:
37
37
  checksums.append(to_signed(zlib.crc32(block.encode("UTF-8"))))
@@ -39,7 +39,7 @@ def methods_to_checksums(blocks):
39
39
  return checksums
40
40
 
41
41
 
42
- def checksums_to_blob(checksums):
42
+ def checksums_to_blob(checksums: [int]) -> sqlite3.Binary:
43
43
  blob = array(CHECKUMS_ARRAY_TYPE, checksums)
44
44
  data = blob.tobytes()
45
45
  return sqlite3.Binary(data)
@@ -59,6 +59,7 @@ INVERTED_GAP_MARKS_CHECKSUMS = {
59
59
 
60
60
  class Block:
61
61
  def __init__(self, start, end, code=0, name=""):
62
+ # assert start <= end
62
63
  self.start = start
63
64
  self.end = end
64
65
  self.name = name
@@ -84,7 +85,8 @@ class Block:
84
85
 
85
86
 
86
87
  @lru_cache(300)
87
- def bytes_to_string_and_fsha(byte_stream):
88
+ def bytes_to_string_and_fsha(byte_stream: bytes) -> Union[str, bytes]:
89
+ # Replace \f because of http://bugs.python.org/issue19035
88
90
  byte_stream = byte_stream.replace(b"\f", b" ")
89
91
  byte_stream = byte_stream.replace(b"\r\n", b"\n")
90
92
  byte_string = byte_stream.decode(source_encoding(byte_stream), "replace")
@@ -107,7 +109,7 @@ def _next_lineno(nodes, i, end):
107
109
 
108
110
 
109
111
  class Module:
110
- def __init__(
112
+ def __init__( # pylint: disable=too-many-arguments
111
113
  self,
112
114
  source_code=None,
113
115
  mtime=None,
@@ -130,6 +132,16 @@ class Module:
130
132
  self.ext = ext
131
133
 
132
134
  def dump_and_block(self, node, end, name="unknown", into_block=False):
135
+ """Frame of this method is taken from ast.dump
136
+ Objective is to return a representation of python source code where
137
+ all of the bodies of functions are replaced with 'transformed_into_block'
138
+ string. The rest of the syntax tree is represented in the same way as
139
+ in ast.dump(tree, annotate_fields=False). Of course the bodies of functions
140
+ are not completely thrown away, they are transformed into Block() objects
141
+ and appended to self.blocks. More can be probably understood from
142
+ (at the time rather messy) test_process_code.py examples.
143
+ """
144
+
133
145
  if isinstance(node, ast.AST):
134
146
  class_name = node.__class__.__name__
135
147
  fields = []
@@ -183,6 +195,8 @@ class Module:
183
195
  tree = ast.parse(self.source_code, filename="<unknown>")
184
196
  self.dump_and_block(tree, len(lines), name="<module>")
185
197
  except SyntaxError:
198
+ # We can continue without blocks because no tests depending on this file will ever get executed,
199
+ # so no node depending on this checksum and mtime will ever be written to db.
186
200
  pass
187
201
  else:
188
202
  self._blocks = [Block(1, len(lines), self.source_code)]
@@ -199,8 +213,8 @@ class Module:
199
213
  return methods_to_checksums([block.checksum for block in self.blocks])
200
214
 
201
215
 
202
- def read_source_sha(filename):
203
- # source_bytes: Optional[bytes]
216
+ def read_source_sha(filename: str):
217
+ source_bytes: Optional[bytes]
204
218
 
205
219
  try:
206
220
  with open(filename, "rb") as file:
@@ -242,7 +256,7 @@ def get_files_shas(directory):
242
256
  return noncached_get_files_shas(directory)
243
257
 
244
258
 
245
- def get_source_sha(directory, filename):
259
+ def get_source_sha(directory: "str", filename: "str"):
246
260
  try:
247
261
  sha = get_files_shas(directory)[filename]
248
262
  return (None, sha)
@@ -256,7 +270,7 @@ def match_fingerprint_source(source_code, fingerprint, ext="py"):
256
270
  return match_fingerprint(module, fingerprint)
257
271
 
258
272
 
259
- def match_fingerprint(module, fingerprint):
273
+ def match_fingerprint(module: Module, fingerprint):
260
274
  if set(fingerprint) - set(module.checksums):
261
275
  return False
262
276
  return True
@@ -267,8 +281,8 @@ def create_fingerprint_source(source_code, lines, ext="py"):
267
281
  return create_fingerprint(module, lines)
268
282
 
269
283
 
270
- def create_fingerprint(module, covered_lines):
271
- blocks = module.blocks
284
+ def create_fingerprint(module, covered_lines) -> [int]:
285
+ blocks: [Block] = module.blocks
272
286
  method_reprs = []
273
287
  line_index = 0
274
288
  sorted_lines = sorted(covered_lines)
@@ -1,3 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Main module of testmon pytest plugin.
4
+ """
1
5
  import time
2
6
  import xmlrpc.client
3
7
  import os
@@ -5,6 +9,7 @@ import os
5
9
  from collections import defaultdict
6
10
  from datetime import date, timedelta
7
11
 
12
+ from pathlib import Path
8
13
  import pytest
9
14
 
10
15
  from _pytest.config import ExitCode, Config
@@ -133,7 +138,7 @@ def testmon_options(config):
133
138
  return result
134
139
 
135
140
 
136
- def init_testmon_data(config):
141
+ def init_testmon_data(config: Config):
137
142
  environment = config.getoption("environment_expression") or eval_environment(
138
143
  config.getini("environment_expression")
139
144
  )
@@ -217,7 +222,7 @@ def register_plugins(config, should_select, should_collect, cov_plugin):
217
222
  def pytest_configure(config):
218
223
  coverage_stack = None
219
224
  try:
220
- from tmnet.testmon_core import (
225
+ from tmnet.testmon_core import ( # pylint: disable=import-outside-toplevel
221
226
  Testmon as UberTestmon,
222
227
  )
223
228
 
@@ -231,7 +236,7 @@ def pytest_configure(config):
231
236
  tm_conf = configure.header_collect_select(
232
237
  config, coverage_stack, cov_plugin=cov_plugin
233
238
  )
234
- config.testmon_config = tm_conf
239
+ config.testmon_config: TmConf = tm_conf
235
240
  if tm_conf.select or tm_conf.collect:
236
241
  try:
237
242
  init_testmon_data(config)
@@ -277,7 +282,7 @@ def pytest_report_header(config):
277
282
  if show_survey_notification:
278
283
  tm_conf.message += (
279
284
  "\nWe'd like to hear from testmon users! "
280
- "🙏🙏 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
 
@@ -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
  )
@@ -200,15 +225,17 @@ class TestmonData:
200
225
 
201
226
  def close_connection(self):
202
227
  pass
228
+ # TODO this is why there are "too many open files" on MacOS.
229
+ # we need the leave the connection open for tests
203
230
 
204
231
  @property
205
232
  def all_tests(self):
206
233
  return self.db.all_test_executions(self.exec_id)
207
234
 
208
- def get_tests_fingerprints(self, nodes_files_lines, reports):
235
+ def get_tests_fingerprints(self, nodes_files_lines, reports) -> TestExecutions:
209
236
  test_executions_fingerprints = {}
210
237
  for context in nodes_files_lines:
211
- deps_n_outcomes = {"deps": []}
238
+ deps_n_outcomes: DepsNOutcomes = {"deps": []}
212
239
 
213
240
  for filename, covered in nodes_files_lines[context].items():
214
241
  if os.path.exists(os.path.join(self.rootdir, filename)):
@@ -262,8 +289,11 @@ class TestmonData:
262
289
  if module:
263
290
  files_fshas[filename] = module.fs_fsha
264
291
 
292
+ # Compare the fshas from disk to the fshas in the database and get files
293
+ # where the fsha is not in database.
265
294
  new_changed_file_data = self.db.fetch_unknown_files(files_fshas, self.exec_id)
266
295
 
296
+ # Get the mhashes for the files from above
267
297
  files_mhashes = collect_mhashes(self.source_tree, new_changed_file_data)
268
298
 
269
299
  tests = self.db.determine_tests(self.exec_id, files_mhashes)
@@ -288,13 +318,24 @@ class TestmonData:
288
318
 
289
319
  _, fsha_misses = split_filter(
290
320
  self.source_tree, check_fsha, filenames_fingerprints
291
- )
321
+ ) # check 2. fsha vs filesystem
322
+
323
+ # with the list of fingerprint_ids go to the database
324
+ # and fetch all the data needed for next step
292
325
 
293
326
  changed_file_data = self.db.fetch_changed_file_data(
294
327
  [fsha_miss["fingerprint_id"] for fsha_miss in (fsha_misses)],
295
328
  self.exec_id,
296
329
  )
297
330
 
331
+ # changed_file_data:
332
+ # [(filename, test_name, method_fshas, fingerprint_id, failed )]
333
+ # All the test_names in this list have a dependency on one
334
+ # or more changed files. And we also have the fingerprints
335
+ # of data content which they depend on. So it’s possible to
336
+ # filter out the node_ids where the content of the changed file
337
+ # still matches the fingerprint
338
+
298
339
  _, fingerprint_misses = split_filter(
299
340
  self.source_tree, check_fingerprint, changed_file_data
300
341
  )
@@ -314,13 +355,13 @@ class TestmonData:
314
355
  print("\n".join(sorted(new_fingerprint_misses)))
315
356
 
316
357
  @property
317
- def avg_durations(self):
358
+ def avg_durations(self) -> dict:
318
359
  stats = defaultdict(lambda: {"test_execution": 0, "sum_duration": 0})
319
360
 
320
361
  for (
321
362
  test_execution_id,
322
363
  report,
323
- ) in self.all_tests.items():
364
+ ) in self.all_tests.items(): # pylint: disable=no-member
324
365
  if report:
325
366
  class_name = get_test_execution_class_name(test_execution_id)
326
367
  module_name = get_test_execution_module_name(test_execution_id)
@@ -351,6 +392,7 @@ class TestmonData:
351
392
 
352
393
 
353
394
  def get_new_mtimes(filesystem, hits):
395
+ """hits: [(filename, _, _, fingerprint_id)]"""
354
396
  try:
355
397
  for hit in hits:
356
398
  module = filesystem.get_file(hit[0])
@@ -379,24 +421,26 @@ def cached_relpath(path, basepath):
379
421
 
380
422
 
381
423
  class TestmonCollector:
382
- coverage_stack = []
424
+ coverage_stack: [Coverage] = []
383
425
 
384
- def __init__(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
385
429
  try:
386
- from testmon.testmon_core import (
430
+ from testmon.testmon_core import ( # pylint: disable=import-outside-toplevel
387
431
  Testmon as UberTestmon,
388
432
  )
389
433
 
390
- TestmonCollector.coverage_stack = UberTestmon.coverage_stack
434
+ TestmonCollector.coverage_stack: [Coverage] = UberTestmon.coverage_stack
391
435
  except ImportError:
392
436
  pass
393
437
  if testmon_labels is None:
394
438
  testmon_labels = {"singleprocess"}
395
439
  self.rootdir = rootdir
396
440
  self.testmon_labels = testmon_labels
397
- self.cov = None
441
+ self.cov: Coverage = None
398
442
  self.sub_cov_file = None
399
- self.cov_plugin = cov_plugin
443
+ self.cov_plugin: CovPlugin = cov_plugin
400
444
  self._test_name = None
401
445
  self._next_test_name = None
402
446
  self.batched_test_names = set()
@@ -449,6 +493,7 @@ class TestmonCollector:
449
493
  params["include"] = list(
450
494
  set(cov.config.run_include + params["include"])
451
495
  )
496
+ # params["omit"] = cov.config.run_omit
452
497
  if cov.config.branch:
453
498
  raise TestmonException(
454
499
  "testmon doesn't support simultaneous run with pytest-cov when "
@@ -519,7 +564,7 @@ class TestmonCollector:
519
564
  return nodes_files_lines
520
565
 
521
566
  def get_nodes_files_lines(self, dont_include):
522
- cov_data = self.cov.get_data()
567
+ cov_data: CoverageData = self.cov.get_data()
523
568
  files = cov_data.measured_files()
524
569
  nodes_files_lines = {}
525
570
  files_lines = {}
@@ -573,7 +618,7 @@ def eval_environment(environment, **kwargs):
573
618
 
574
619
  try:
575
620
  return str(eval(environment, eval_globals))
576
- except Exception as error:
621
+ except Exception as error: # pylint: disable=broad-except
577
622
  return repr(error)
578
623
 
579
624
 
@@ -1,5 +1,3 @@
1
- import os
2
-
3
1
  from pathlib import Path
4
2
  import pluggy
5
3
 
@@ -21,7 +19,7 @@ def touch_stampfile(venv):
21
19
 
22
20
 
23
21
  def installed_testmon(venv):
24
- return os.path.exists(venv.path.join(".testmon_installed"))
22
+ return Path(venv.path.join(".testmon_installed")).exists()
25
23
 
26
24
 
27
25
  @hookimpl
File without changes
File without changes
File without changes
File without changes