pintest-cli 0.2.9__tar.gz → 0.3.1__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 (30) hide show
  1. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/PKG-INFO +1 -1
  2. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/cli.py +45 -25
  3. pintest_cli-0.3.1/pintest/pytest_plugin.py +166 -0
  4. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest_cli.egg-info/PKG-INFO +1 -1
  5. pintest_cli-0.3.1/pintest_cli.egg-info/entry_points.txt +5 -0
  6. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/setup.py +4 -1
  7. pintest_cli-0.2.9/pintest/pytest_plugin.py +0 -26
  8. pintest_cli-0.2.9/pintest_cli.egg-info/entry_points.txt +0 -2
  9. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/README.md +0 -0
  10. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/__init__.py +0 -0
  11. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/build_mapping_iterative.py +0 -0
  12. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/cloud_mapping_db.py +0 -0
  13. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/config.py +0 -0
  14. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/coverage_mapper.py +0 -0
  15. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/git_diff_parser.py +0 -0
  16. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/post_commit_hook.py +0 -0
  17. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/pre_commit_hook.py +0 -0
  18. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/push_cache.py +0 -0
  19. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/range_set.py +0 -0
  20. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/test_mapping_db_v2.py +0 -0
  21. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest/update_mapping.py +0 -0
  22. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest_cli.egg-info/SOURCES.txt +0 -0
  23. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest_cli.egg-info/dependency_links.txt +0 -0
  24. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest_cli.egg-info/requires.txt +0 -0
  25. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/pintest_cli.egg-info/top_level.txt +0 -0
  26. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/setup.cfg +0 -0
  27. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/tests/__init__.py +0 -0
  28. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/tests/test_git_diff_parser.py +0 -0
  29. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/tests/test_new_feature.py +0 -0
  30. {pintest_cli-0.2.9 → pintest_cli-0.3.1}/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.9
3
+ Version: 0.3.1
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -133,37 +133,45 @@ class PintestRunner:
133
133
  Returns:
134
134
  Exit code from pytest (0 = success)
135
135
  """
136
- if not test_selection:
137
- print("No tests to run!")
138
- return 0
139
-
140
- # Build pytest command
141
- cmd = [sys.executable, "-m", "pytest", "-p", "pintest.pytest_plugin"]
142
-
143
- # Automatically generate coverage for local updates (suppress terminal report)
144
- cmd.extend(["--cov", "--cov-context=test", "--cov-append", "--cov-report="])
145
-
136
+ test_list = sorted(test_selection)
137
+ chunk_size = 500
138
+ needs_chunking = len(test_list) > chunk_size
139
+
140
+ # Base pytest command
141
+ base_cmd = [sys.executable, "-m", "pytest", "-p", "pintest.pytest_plugin"]
142
+ base_cmd.extend(["--cov", "--cov-context=test", "--cov-append", "--cov-report="])
146
143
  if verbose:
147
- cmd.append("-v")
148
-
149
- # Add custom pytest args
144
+ base_cmd.append("-v")
150
145
  if pytest_args:
151
- cmd.extend(pytest_args)
152
-
153
- # Add test files
154
- for test in sorted(test_selection):
155
- cmd.append(test)
156
-
146
+ base_cmd.extend(pytest_args)
147
+
157
148
  if dry_run:
158
149
  print(f"Would run {len(test_selection)} test(s):")
159
- for test in sorted(test_selection):
150
+ for test in test_list[:10]:
160
151
  print(f" {test}")
161
- print(f"\nCommand: {' '.join(cmd)}")
152
+ if len(test_list) > 10:
153
+ print(f" ... and {len(test_list) - 10} more")
154
+ print(f"\nBase Command: {' '.join(base_cmd)}")
162
155
  return 0
163
-
164
- print(f"Running {len(test_selection)} selected test(s)...")
165
- result = subprocess.run(cmd, cwd=self.repo_root)
166
- return result.returncode
156
+
157
+ if needs_chunking:
158
+ print(f"Running {len(test_selection)} selected test(s) in chunks of {chunk_size}...")
159
+ total_chunks = (len(test_list) + chunk_size - 1) // chunk_size
160
+ final_returncode = 0
161
+ for i in range(0, len(test_list), chunk_size):
162
+ chunk = test_list[i:i + chunk_size]
163
+ chunk_num = (i // chunk_size) + 1
164
+ print(f"\n📦 Running chunk {chunk_num}/{total_chunks} ({len(chunk)} tests)...", flush=True)
165
+ cmd = base_cmd + chunk
166
+ result = subprocess.run(cmd, cwd=self.repo_root)
167
+ if result.returncode != 0 and final_returncode == 0:
168
+ final_returncode = result.returncode
169
+ return final_returncode
170
+ else:
171
+ print(f"Running {len(test_selection)} selected test(s)...")
172
+ cmd = base_cmd + test_list
173
+ result = subprocess.run(cmd, cwd=self.repo_root)
174
+ return result.returncode
167
175
 
168
176
 
169
177
  def cmd_run(args):
@@ -237,12 +245,20 @@ def cmd_run(args):
237
245
  base_branch
238
246
  )
239
247
 
248
+ # Determine test_dir
249
+ test_dir = args.test_dir
250
+ if not test_dir and cfg.is_cloud_enabled and getattr(cfg.cloud, "test_dir", None):
251
+ test_dir = cfg.cloud.test_dir
252
+ if not test_dir:
253
+ test_dir = "tests"
254
+
240
255
  # Unmapped tests discovery (Cloud mode only)
241
256
  if use_cloud:
242
257
  from .pre_commit_hook import find_unmapped_tests
243
258
  unmapped = find_unmapped_tests(
244
259
  repo_root,
245
260
  mapping_db_obj,
261
+ test_dir=test_dir,
246
262
  verbose=args.verbose
247
263
  )
248
264
  if unmapped:
@@ -598,6 +614,10 @@ Examples:
598
614
  default=0,
599
615
  help="Minimum number of tests to run (exit with error if below)"
600
616
  )
617
+ run_parser.add_argument(
618
+ "--test-dir",
619
+ help="Directory containing tests (default: read from config or tests/)"
620
+ )
601
621
  run_parser.add_argument(
602
622
  "-v", "--verbose",
603
623
  action="store_true",
@@ -0,0 +1,166 @@
1
+ """Pytest plugin for Pintest (smart test selection and duration tracking)."""
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ # Stores test_node_id -> duration_ms
7
+ test_durations = {}
8
+
9
+
10
+ def pytest_addoption(parser):
11
+ """Add Pintest command line options."""
12
+ group = parser.getgroup("pintest")
13
+ group.addoption(
14
+ "--pintest",
15
+ action="store_true",
16
+ default=False,
17
+ help="Enable Pintest smart test selection (run only affected tests)",
18
+ )
19
+ group.addoption(
20
+ "--pintest-base",
21
+ action="store",
22
+ default="master",
23
+ help="Base branch for Pintest git diffing (default: master)",
24
+ )
25
+
26
+
27
+ def pytest_collection_modifyitems(session, config, items):
28
+ """Filter collected tests if --pintest is enabled."""
29
+ if not config.getoption("--pintest"):
30
+ return
31
+
32
+ from pintest.config import Config
33
+ from pintest.cloud_mapping_db import CloudMappingDB
34
+ from pintest.test_mapping_db_v2 import TestMappingDBV2
35
+ from pintest.cli import PintestRunner
36
+
37
+ repo_root = Path(config.rootdir).resolve()
38
+ base_branch = config.getoption("--pintest-base")
39
+
40
+ # ── Mapping source detection ───────────────────────────────────────────
41
+ mapping_db_obj = None
42
+
43
+ # 1. Try Cloud Mode
44
+ cfg = Config.load()
45
+ use_cloud = cfg.is_cloud_enabled and bool(cfg.cloud.repo_id)
46
+
47
+ if use_cloud:
48
+ print(f"☁️ Mapping Service: Pintest Cloud (repo {cfg.cloud.repo_id[:8]}...)", file=sys.stderr)
49
+ mapping_db_obj = CloudMappingDB(cfg.cloud)
50
+ else:
51
+ # 2. Try Local V2 Mapping DB
52
+ mapping_db = repo_root / ".pintest" / "test_mapping.db"
53
+ legacy_db = repo_root / ".test_mapping.db"
54
+ if not mapping_db.exists() and legacy_db.exists():
55
+ mapping_db.parent.mkdir(parents=True, exist_ok=True)
56
+ try:
57
+ legacy_db.rename(mapping_db)
58
+ except Exception:
59
+ mapping_db = legacy_db
60
+
61
+ if not mapping_db.exists():
62
+ print("🏗️ No local mapping DB found. Initializing build...", file=sys.stderr)
63
+ from pintest.build_mapping_iterative import build_mapping_iteratively
64
+
65
+ default_test_dir = "tests"
66
+ if getattr(cfg.cloud, "test_dir", None):
67
+ default_test_dir = cfg.cloud.test_dir
68
+
69
+ exit_code = build_mapping_iteratively(
70
+ repo_root,
71
+ mapping_db,
72
+ test_dir=default_test_dir,
73
+ verbose=False
74
+ )
75
+ if exit_code != 0:
76
+ print("❌ Failed to build mapping DB.", file=sys.stderr)
77
+ sys.exit(exit_code)
78
+
79
+ print(f"🖥️ Local mode: {mapping_db}", file=sys.stderr)
80
+ mapping_db_obj = TestMappingDBV2(mapping_db)
81
+ mapping_db_obj.connect()
82
+
83
+ try:
84
+ if base_branch == "master" and cfg.is_cloud_enabled and getattr(cfg.cloud, "branch", None):
85
+ base_branch = cfg.cloud.branch
86
+
87
+ # Initialize runner
88
+ runner = PintestRunner(
89
+ repo_root,
90
+ mapping_db=mapping_db_obj,
91
+ verbose=False
92
+ )
93
+
94
+ # Find affected tests
95
+ affected_tests = runner.find_affected_tests(base_branch)
96
+
97
+ # Unmapped tests discovery (Cloud mode only)
98
+ if use_cloud:
99
+ from pintest.pre_commit_hook import find_unmapped_tests
100
+ unmapped = find_unmapped_tests(
101
+ repo_root,
102
+ mapping_db_obj,
103
+ verbose=False
104
+ )
105
+ if unmapped:
106
+ affected_tests.update(unmapped)
107
+
108
+ # Filter items in-place
109
+ selected_items = []
110
+ deselected_items = []
111
+
112
+ for item in items:
113
+ # Check exact nodeid match
114
+ match = item.nodeid in affected_tests
115
+
116
+ # Check file match (nodeid prefix before ::)
117
+ if not match:
118
+ file_part = item.nodeid.split("::")[0]
119
+ if file_part in affected_tests:
120
+ match = True
121
+
122
+ # Check fspath/path relative match
123
+ if not match:
124
+ try:
125
+ item_path = getattr(item, 'path', None)
126
+ if not item_path and hasattr(item, 'fspath'):
127
+ item_path = Path(item.fspath)
128
+ if item_path:
129
+ rel_path = str(item_path.relative_to(repo_root))
130
+ if rel_path in affected_tests:
131
+ match = True
132
+ except Exception:
133
+ pass
134
+
135
+ if match:
136
+ selected_items.append(item)
137
+ else:
138
+ deselected_items.append(item)
139
+
140
+ items[:] = selected_items
141
+ if deselected_items:
142
+ config.hook.pytest_deselected(items=deselected_items)
143
+
144
+ finally:
145
+ if mapping_db_obj and hasattr(mapping_db_obj, 'close'):
146
+ mapping_db_obj.close()
147
+
148
+
149
+ def pytest_runtest_makereport(item, call):
150
+ """Record test execution time during the 'call' phase."""
151
+ if call.when == "call":
152
+ # call.duration is a float representing exact seconds
153
+ test_durations[item.nodeid] = int(call.duration * 1000)
154
+
155
+
156
+ def pytest_sessionfinish(session, exitstatus):
157
+ """Dump durations to a temporary file for the CLI push phase."""
158
+ try:
159
+ pintest_dir = Path(session.config.rootdir) / ".pintest"
160
+ pintest_dir.mkdir(parents=True, exist_ok=True)
161
+ out_file = pintest_dir / "durations.json"
162
+ with open(out_file, "w") as f:
163
+ json.dump(test_durations, f)
164
+ except Exception as e:
165
+ # Silently pass if we cannot write to rootdir
166
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pintest-cli
3
- Version: 0.2.9
3
+ Version: 0.3.1
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ pintest = pintest.cli:main
3
+
4
+ [pytest11]
5
+ pintest.pytest_plugin = pintest.pytest_plugin
@@ -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.9",
8
+ version="0.3.1",
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",
@@ -21,6 +21,9 @@ setup(
21
21
  "console_scripts": [
22
22
  "pintest=pintest.cli:main",
23
23
  ],
24
+ "pytest11": [
25
+ "pintest.pytest_plugin = pintest.pytest_plugin",
26
+ ],
24
27
  },
25
28
  python_requires=">=3.8",
26
29
  classifiers=[
@@ -1,26 +0,0 @@
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
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- pintest = pintest.cli:main
File without changes
File without changes