pintest-cli 0.2.6__tar.gz → 0.2.8__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 (28) hide show
  1. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/PKG-INFO +1 -1
  2. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/build_mapping_iterative.py +15 -2
  3. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/cli.py +22 -5
  4. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/cloud_mapping_db.py +29 -2
  5. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/pre_commit_hook.py +15 -4
  6. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/push_cache.py +15 -9
  7. pintest_cli-0.2.8/pintest/pytest_plugin.py +26 -0
  8. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/update_mapping.py +11 -2
  9. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/PKG-INFO +1 -1
  10. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/SOURCES.txt +1 -0
  11. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/setup.py +1 -1
  12. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/README.md +0 -0
  13. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/__init__.py +0 -0
  14. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/config.py +0 -0
  15. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/coverage_mapper.py +0 -0
  16. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/git_diff_parser.py +0 -0
  17. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/post_commit_hook.py +0 -0
  18. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/range_set.py +0 -0
  19. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/test_mapping_db_v2.py +0 -0
  20. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/dependency_links.txt +0 -0
  21. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/entry_points.txt +0 -0
  22. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/requires.txt +0 -0
  23. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/top_level.txt +0 -0
  24. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/setup.cfg +0 -0
  25. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/__init__.py +0 -0
  26. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_git_diff_parser.py +0 -0
  27. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_new_feature.py +0 -0
  28. {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_range_set.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pintest-cli
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -52,6 +52,7 @@ def run_test_chunk_with_mapping(
52
52
  # Run chunk of tests with coverage
53
53
  cmd = [
54
54
  sys.executable, "-m", "pytest",
55
+ "-p", "pintest.pytest_plugin",
55
56
  "--cov",
56
57
  "--cov-context=test",
57
58
  "--cov-append",
@@ -298,7 +299,7 @@ def main():
298
299
  parser.add_argument(
299
300
  "--mapping-db",
300
301
  type=Path,
301
- help="Path to mapping database (default: <repo>/.test_mapping.db)"
302
+ help="Path to mapping database (default: <repo>/.pintest/test_mapping.db)"
302
303
  )
303
304
  parser.add_argument(
304
305
  "--test-dir",
@@ -315,7 +316,19 @@ def main():
315
316
  args = parser.parse_args()
316
317
 
317
318
  repo_root = args.repo_root.resolve()
318
- mapping_db = args.mapping_db or (repo_root / ".test_mapping.db")
319
+ if args.mapping_db:
320
+ mapping_db = args.mapping_db
321
+ else:
322
+ mapping_db = repo_root / ".pintest" / "test_mapping.db"
323
+ legacy_db = repo_root / ".test_mapping.db"
324
+ if not mapping_db.exists() and legacy_db.exists():
325
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
326
+ try:
327
+ legacy_db.rename(mapping_db)
328
+ except Exception:
329
+ mapping_db = legacy_db
330
+ else:
331
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
319
332
 
320
333
  return build_mapping_iteratively(
321
334
  repo_root,
@@ -138,7 +138,7 @@ class PintestRunner:
138
138
  return 0
139
139
 
140
140
  # Build pytest command
141
- cmd = [sys.executable, "-m", "pytest"]
141
+ cmd = [sys.executable, "-m", "pytest", "-p", "pintest.pytest_plugin"]
142
142
 
143
143
  # Automatically generate coverage for local updates (suppress terminal report)
144
144
  cmd.extend(["--cov", "--cov-context=test", "--cov-append", "--cov-report="])
@@ -188,7 +188,14 @@ def cmd_run(args):
188
188
  # 2. Try Local V2 Mapping DB
189
189
  mapping_db = args.mapping_db if hasattr(args, 'mapping_db') else None
190
190
  if not mapping_db:
191
- mapping_db = repo_root / ".test_mapping.db"
191
+ mapping_db = repo_root / ".pintest" / "test_mapping.db"
192
+ legacy_db = repo_root / ".test_mapping.db"
193
+ if not mapping_db.exists() and legacy_db.exists():
194
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
195
+ try:
196
+ legacy_db.rename(mapping_db)
197
+ except Exception:
198
+ mapping_db = legacy_db
192
199
 
193
200
  if not mapping_db.exists():
194
201
  print("🏗️ No local mapping DB found. Initializing build...", file=sys.stderr)
@@ -303,7 +310,17 @@ def cmd_build_mapping(args):
303
310
  from .build_mapping_iterative import build_mapping_iteratively
304
311
 
305
312
  repo_root = args.repo_root.resolve()
306
- mapping_db = args.mapping_db or (repo_root / ".test_mapping.db")
313
+ if args.mapping_db:
314
+ mapping_db = args.mapping_db
315
+ else:
316
+ mapping_db = repo_root / ".pintest" / "test_mapping.db"
317
+ legacy_db = repo_root / ".test_mapping.db"
318
+ if not mapping_db.exists() and legacy_db.exists():
319
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
320
+ try:
321
+ legacy_db.rename(mapping_db)
322
+ except Exception:
323
+ mapping_db = legacy_db
307
324
 
308
325
  # Load config to get default test_dir if it was saved during track
309
326
  from .config import Config
@@ -618,7 +635,7 @@ Examples:
618
635
  update_parser.add_argument(
619
636
  "--mapping-db",
620
637
  type=Path,
621
- help="Path to mapping database (default: <repo>/.test_mapping.db)"
638
+ help="Path to mapping database (default: <repo>/.pintest/test_mapping.db)"
622
639
  )
623
640
  update_parser.add_argument(
624
641
  "-v", "--verbose",
@@ -648,7 +665,7 @@ Examples:
648
665
  build_parser.add_argument(
649
666
  "--mapping-db",
650
667
  type=Path,
651
- help="Path to mapping database (default: <repo>/.test_mapping.db)"
668
+ help="Path to mapping database (default: <repo>/.pintest/test_mapping.db)"
652
669
  )
653
670
  build_parser.add_argument(
654
671
  "--test-dir",
@@ -138,19 +138,46 @@ class CloudMappingDB:
138
138
  test_file_ranges[key] = RangeSet()
139
139
  test_file_ranges[key].add_range(line_num, line_num)
140
140
 
141
- cache_db_path = coverage_file.parent / ".pintest_push_cache.db"
141
+ pintest_dir = coverage_file.parent / ".pintest"
142
+ pintest_dir.mkdir(parents=True, exist_ok=True)
143
+
144
+ cache_db_path = pintest_dir / "push_cache.db"
145
+ legacy_cache = coverage_file.parent / ".pintest_push_cache.db"
146
+ if not cache_db_path.exists() and legacy_cache.exists():
147
+ try:
148
+ legacy_cache.rename(cache_db_path)
149
+ except Exception:
150
+ cache_db_path = legacy_cache
151
+
142
152
  push_cache = PushCache(cache_db_path)
143
153
  cached_state = push_cache.get_cached_state(self._branch)
144
154
 
155
+ import json
156
+ durations_file = pintest_dir / "durations.json"
157
+ legacy_durations = coverage_file.parent / ".pintest_durations.json"
158
+ if not durations_file.exists() and legacy_durations.exists():
159
+ try:
160
+ legacy_durations.rename(durations_file)
161
+ except Exception:
162
+ durations_file = legacy_durations
163
+
164
+ try:
165
+ test_durations = json.loads(durations_file.read_text()) if durations_file.exists() else {}
166
+ except Exception:
167
+ test_durations = {}
168
+
145
169
  mappings = []
146
170
  for (test_name, file_path), rs in test_file_ranges.items():
147
171
  compact_ranges = rs.to_compact_string()
148
172
  key = (test_name, file_path)
149
- if key not in cached_state or cached_state[key] != compact_ranges:
173
+ duration_ms = test_durations.get(test_name, 0)
174
+ cached_ranges, cached_duration = cached_state.get(key, ("", 0))
175
+ if cached_ranges != compact_ranges or (duration_ms > 0 and duration_ms != cached_duration):
150
176
  mappings.append({
151
177
  "test_name": test_name,
152
178
  "file_path": file_path,
153
179
  "ranges": compact_ranges,
180
+ "duration_ms": duration_ms,
154
181
  })
155
182
 
156
183
  if not mappings:
@@ -71,7 +71,7 @@ def ensure_git_lfs(repo_root: Path, verbose: bool = False) -> bool:
71
71
  """
72
72
  Ensure Git LFS is initialized in the repository.
73
73
 
74
- This is needed if the repo uses Git LFS for storing large files like .test_mapping.db.
74
+ This is needed if the repo uses Git LFS for storing large files like .pintest/test_mapping.db.
75
75
  Safe to run multiple times.
76
76
 
77
77
  Args:
@@ -426,6 +426,7 @@ def run_test_chunk_with_mapping_update(
426
426
  # Run tests with coverage - output flows directly to terminal
427
427
  cmd = [
428
428
  sys.executable, "-m", "pytest",
429
+ "-p", "pintest.pytest_plugin",
429
430
  "--cov",
430
431
  "--cov-context=test",
431
432
  "--cov-append",
@@ -558,6 +559,7 @@ def run_single_test_with_mapping_update(
558
559
  # Run single test with coverage
559
560
  cmd = [
560
561
  "python", "-m", "pytest",
562
+ "-p", "pintest.pytest_plugin",
561
563
  "--cov",
562
564
  "--cov-context=test",
563
565
  "--cov-append",
@@ -1023,6 +1025,7 @@ def run_tests_with_coverage(
1023
1025
  # Build pytest command
1024
1026
  cmd = [
1025
1027
  sys.executable, "-m", "pytest",
1028
+ "-p", "pintest.pytest_plugin",
1026
1029
  "--cov=src",
1027
1030
  "--cov-context=test",
1028
1031
  "--cov-append", # Append to existing coverage
@@ -1076,7 +1079,7 @@ def main():
1076
1079
  "--mapping-db",
1077
1080
  type=Path,
1078
1081
  default=None,
1079
- help="Path to test mapping database (default: <repo>/.test_mapping.db)"
1082
+ help="Path to test mapping database (default: <repo>/.pintest/test_mapping.db)"
1080
1083
  )
1081
1084
  parser.add_argument(
1082
1085
  "--test-dir",
@@ -1115,13 +1118,21 @@ def main():
1115
1118
  repo_root = args.repo_root.resolve()
1116
1119
 
1117
1120
  # Set up log file for tracking all output
1118
- log_file = repo_root / ".pre-commit.log"
1121
+ pintest_dir = repo_root / ".pintest"
1122
+ pintest_dir.mkdir(parents=True, exist_ok=True)
1123
+ log_file = pintest_dir / "pre-commit.log"
1119
1124
 
1120
1125
  # Use TeeOutput to write to both stdout and log file
1121
1126
  with TeeOutput(log_file):
1122
1127
  # Default mapping database location
1123
1128
  if args.mapping_db is None:
1124
- mapping_db = repo_root / ".test_mapping.db"
1129
+ mapping_db = pintest_dir / "test_mapping.db"
1130
+ legacy_db = repo_root / ".test_mapping.db"
1131
+ if not mapping_db.exists() and legacy_db.exists():
1132
+ try:
1133
+ legacy_db.rename(mapping_db)
1134
+ except Exception:
1135
+ mapping_db = legacy_db
1125
1136
  else:
1126
1137
  mapping_db = args.mapping_db
1127
1138
 
@@ -23,39 +23,45 @@ class PushCache:
23
23
  test_name TEXT,
24
24
  file_path TEXT,
25
25
  ranges TEXT,
26
+ duration_ms INTEGER DEFAULT 0,
26
27
  last_pushed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
27
28
  PRIMARY KEY (branch, test_name, file_path)
28
29
  )
29
30
  """)
31
+ try:
32
+ conn.execute("ALTER TABLE push_cache ADD COLUMN duration_ms INTEGER DEFAULT 0")
33
+ except sqlite3.OperationalError:
34
+ pass
30
35
  conn.execute("CREATE INDEX IF NOT EXISTS idx_push_cache_lookup ON push_cache(branch)")
31
36
 
32
- def get_cached_state(self, branch: str) -> Dict[Tuple[str, str], str]:
37
+ def get_cached_state(self, branch: str) -> Dict[Tuple[str, str], Tuple[str, int]]:
33
38
  """
34
39
  Fetch the last successfully pushed state for the active branch.
35
40
 
36
41
  Returns:
37
- {(test_name, file_path): ranges_string}
42
+ {(test_name, file_path): (ranges_string, duration_ms)}
38
43
  """
39
44
  with sqlite3.connect(self.db_path) as conn:
40
45
  cursor = conn.execute(
41
- "SELECT test_name, file_path, ranges FROM push_cache WHERE branch = ?",
46
+ "SELECT test_name, file_path, ranges, duration_ms FROM push_cache WHERE branch = ?",
42
47
  (branch,)
43
48
  )
44
- return {(row[0], row[1]): row[2] for row in cursor}
49
+ return {(row[0], row[1]): (row[2], row[3] if row[3] is not None else 0) for row in cursor}
45
50
 
46
- def batch_upsert(self, branch: str, mappings: List[Dict[str, str]]):
51
+ def batch_upsert(self, branch: str, mappings: List[Dict]):
47
52
  """
48
53
  Atomically update the cache with successfully pushed mappings.
49
54
 
50
55
  Args:
51
56
  branch: Active git branch
52
- mappings: List of dicts [{"test_name": ..., "file_path": ..., "ranges": ...}]
57
+ mappings: List of dicts [{"test_name": ..., "file_path": ..., "ranges": ..., "duration_ms": ...}]
53
58
  """
54
59
  with sqlite3.connect(self.db_path) as conn:
55
60
  conn.executemany("""
56
- INSERT INTO push_cache (branch, test_name, file_path, ranges, last_pushed_at)
57
- VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
61
+ INSERT INTO push_cache (branch, test_name, file_path, ranges, duration_ms, last_pushed_at)
62
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
58
63
  ON CONFLICT(branch, test_name, file_path) DO UPDATE SET
59
64
  ranges = EXCLUDED.ranges,
65
+ duration_ms = CASE WHEN EXCLUDED.duration_ms > 0 THEN EXCLUDED.duration_ms ELSE push_cache.duration_ms END,
60
66
  last_pushed_at = CURRENT_TIMESTAMP
61
- """, [(branch, m["test_name"], m["file_path"], m["ranges"]) for m in mappings])
67
+ """, [(branch, m["test_name"], m["file_path"], m["ranges"], m.get("duration_ms", 0)) for m in mappings])
@@ -0,0 +1,26 @@
1
+ """Pytest plugin to measure individual test execution duration for Pintest."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ # Stores test_node_id -> duration_ms
6
+ test_durations = {}
7
+
8
+
9
+ def pytest_runtest_makereport(item, call):
10
+ """Record test execution time during the 'call' phase."""
11
+ if call.when == "call":
12
+ # call.duration is a float representing exact seconds
13
+ test_durations[item.nodeid] = int(call.duration * 1000)
14
+
15
+
16
+ def pytest_sessionfinish(session, exitstatus):
17
+ """Dump durations to a temporary file for the CLI push phase."""
18
+ try:
19
+ pintest_dir = Path(session.config.rootdir) / ".pintest"
20
+ pintest_dir.mkdir(parents=True, exist_ok=True)
21
+ out_file = pintest_dir / "durations.json"
22
+ with open(out_file, "w") as f:
23
+ json.dump(test_durations, f)
24
+ except Exception as e:
25
+ # Silently pass if we cannot write to rootdir
26
+ pass
@@ -42,7 +42,16 @@ def update_mapping(
42
42
  coverage_file = repo_root / "coverage" / ".coverage"
43
43
 
44
44
  if mapping_db is None:
45
- mapping_db = repo_root / ".test_mapping.db"
45
+ mapping_db = repo_root / ".pintest" / "test_mapping.db"
46
+ legacy_db = repo_root / ".test_mapping.db"
47
+ if not mapping_db.exists() and legacy_db.exists():
48
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
49
+ try:
50
+ legacy_db.rename(mapping_db)
51
+ except Exception:
52
+ mapping_db = legacy_db
53
+ else:
54
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
46
55
 
47
56
  if verbose:
48
57
  pass
@@ -101,7 +110,7 @@ def main():
101
110
  parser.add_argument(
102
111
  "--mapping-db",
103
112
  type=Path,
104
- help="Path to mapping database (default: <repo>/.test_mapping.db)"
113
+ help="Path to mapping database (default: <repo>/.pintest/test_mapping.db)"
105
114
  )
106
115
  parser.add_argument(
107
116
  "-v", "--verbose",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pintest-cli
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -10,6 +10,7 @@ pintest/git_diff_parser.py
10
10
  pintest/post_commit_hook.py
11
11
  pintest/pre_commit_hook.py
12
12
  pintest/push_cache.py
13
+ pintest/pytest_plugin.py
13
14
  pintest/range_set.py
14
15
  pintest/test_mapping_db_v2.py
15
16
  pintest/update_mapping.py
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="pintest-cli",
8
- version="0.2.6",
8
+ version="0.2.8",
9
9
  description="Run only the tests affected by your code changes.",
10
10
  long_description=long_description,
11
11
  long_description_content_type="text/markdown",
File without changes
File without changes