pintest-cli 0.2.0__py3-none-any.whl

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.
pintest/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Pintest - Run only tests affected by code changes."""
2
+
3
+ __version__ = "0.2.0"
4
+ __author__ = "Pintest Contributors"
5
+ __description__ = "Intelligently select and run only tests affected by code changes"
6
+
7
+ # Range-based storage (v2)
8
+ from .test_mapping_db_v2 import TestMappingDBV2
9
+ from .range_set import RangeSet
10
+
11
+ __all__ = [
12
+ "TestMappingDBV2", # New range-based storage
13
+ "RangeSet", # Range compression utility
14
+ ]
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Iteratively build test mapping database by running tests one-by-one.
4
+
5
+ This script is designed to be resumable - if it fails or is interrupted,
6
+ you can run it again and it will continue from where it left off.
7
+
8
+ Usage:
9
+ python build_mapping_iterative.py --repo-root ~/workspace/myproject
10
+
11
+ # Continue after interruption:
12
+ python build_mapping_iterative.py --repo-root ~/workspace/myproject --resume
13
+ """
14
+
15
+ import argparse
16
+ import subprocess
17
+ import sys
18
+ import json
19
+ from pathlib import Path
20
+ from datetime import datetime
21
+ from typing import Set
22
+
23
+ # Add pintest to path
24
+ SCRIPT_DIR = Path(__file__).parent
25
+ sys.path.insert(0, str(SCRIPT_DIR))
26
+
27
+ from pintest.test_mapping_db_v2 import TestMappingDBV2
28
+ from pintest.pre_commit_hook import (
29
+ collect_all_tests,
30
+ find_unmapped_tests,
31
+ ensure_docker_containers,
32
+ run_preflight_scripts
33
+ )
34
+
35
+
36
+ def run_test_chunk_with_mapping(
37
+ repo_root: Path,
38
+ test_names: list,
39
+ mapping_db_path: Path,
40
+ verbose: bool = False
41
+ ) -> tuple[Set[str], Set[str]]:
42
+ """
43
+ Run a chunk of tests with coverage and update mapping.
44
+
45
+ Args:
46
+ repo_root: Repository root directory
47
+ test_names: List of test names to run
48
+ mapping_db_path: Path to mapping database
49
+ verbose: Print verbose output
50
+
51
+ Returns:
52
+ Tuple of (passed_tests, failed_tests)
53
+ """
54
+ # Run chunk of tests with coverage
55
+ cmd = [
56
+ sys.executable, "-m", "pytest",
57
+ "--cov=src",
58
+ "--cov-context=test",
59
+ "--cov-append",
60
+ "--cov-report=",
61
+ "-v",
62
+ "--tb=short",
63
+ ] + test_names
64
+
65
+ print(f" Running: {' '.join(cmd[:6])} ... {len(test_names)} tests", flush=True)
66
+
67
+ result = subprocess.run(
68
+ cmd,
69
+ cwd=repo_root,
70
+ text=True
71
+ )
72
+
73
+ # Parse pytest exit code
74
+ # 0 = all passed, 1 = some failed, 2+ = errors/interrupted
75
+ passed = set()
76
+ failed = set()
77
+
78
+ if result.returncode == 0:
79
+ # All tests passed
80
+ passed = set(test_names)
81
+ else:
82
+ # Some failed - for mapping purposes, we still got coverage
83
+ # Mark all as "passed" (meaning: we have coverage data for them)
84
+ passed = set(test_names)
85
+
86
+ # Find and use coverage data files
87
+ coverage_file = repo_root / ".coverage"
88
+ coverage_chunks = list(repo_root.glob(".coverage.*"))
89
+
90
+ # Filter out empty files
91
+ valid_chunks = []
92
+ for chunk_file in coverage_chunks:
93
+ try:
94
+ import sqlite3
95
+ conn = sqlite3.connect(chunk_file)
96
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
97
+ tables = [row[0] for row in cursor.fetchall()]
98
+ conn.close()
99
+ if tables: # Has tables, so it's valid
100
+ valid_chunks.append(chunk_file)
101
+ except:
102
+ pass # Skip invalid files
103
+
104
+ # Try to combine if needed
105
+ if valid_chunks and not coverage_file.exists():
106
+ combine_cmd = [sys.executable, "-m", "coverage", "combine"]
107
+ subprocess.run(
108
+ combine_cmd,
109
+ cwd=repo_root,
110
+ text=True
111
+ )
112
+
113
+ # Update mapping database - try .coverage first, then fall back to chunks
114
+ try:
115
+ import_source = None
116
+
117
+ if coverage_file.exists():
118
+ # Check if .coverage is valid (has tables)
119
+ import sqlite3
120
+ try:
121
+ conn = sqlite3.connect(coverage_file)
122
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
123
+ tables = [row[0] for row in cursor.fetchall()]
124
+ conn.close()
125
+ if tables:
126
+ import_source = coverage_file
127
+ except:
128
+ pass
129
+
130
+ # If .coverage is empty/invalid, use the largest chunk file
131
+ if not import_source and valid_chunks:
132
+ # Sort by size, use largest
133
+ import_source = max(valid_chunks, key=lambda f: f.stat().st_size)
134
+ if verbose:
135
+ print(f" šŸ“Œ Using chunk file: {import_source.name}")
136
+
137
+ if import_source:
138
+ with TestMappingDBV2(mapping_db_path) as db:
139
+ db.import_from_coverage(import_source, incremental=True)
140
+ else:
141
+ if verbose:
142
+ print(f" āš ļø No valid coverage file found")
143
+ except Exception as e:
144
+ if verbose:
145
+ print(f" āš ļø Could not update mapping: {e}")
146
+
147
+ return passed, failed
148
+
149
+
150
+ def build_mapping_iteratively(
151
+ repo_root: Path,
152
+ mapping_db: Path,
153
+ test_dir: str = "unit_tests",
154
+ verbose: bool = False
155
+ ):
156
+ """
157
+ Build mapping database by running unmapped tests.
158
+ Uses database queries to determine which tests still need mapping.
159
+ Resumable by design - just run again and it continues from where it left off.
160
+
161
+ Args:
162
+ repo_root: Repository root directory
163
+ mapping_db: Path to mapping database
164
+ test_dir: Test directory to collect from (default: "unit_tests")
165
+ verbose: Print verbose output
166
+ """
167
+ repo_root = repo_root.resolve()
168
+
169
+ print("šŸ—ļø Test Mapping Builder")
170
+ print("=" * 80)
171
+ print(f"Repository: {repo_root}")
172
+ print(f"Mapping DB: {mapping_db}")
173
+
174
+ # Ensure Docker and preflight checks
175
+ print("\nšŸ”§ Pre-checks...")
176
+ if not ensure_docker_containers(repo_root, verbose):
177
+ print("āŒ Docker container check failed.")
178
+ return 1
179
+
180
+ if not run_preflight_scripts(repo_root, verbose):
181
+ print("āŒ Preflight checks failed.")
182
+ return 1
183
+
184
+ # Initialize mapping database
185
+ with TestMappingDBV2(mapping_db) as db:
186
+ db.initialize_schema()
187
+
188
+ # Find unmapped tests by querying database
189
+ print("\nšŸ” Finding unmapped tests...")
190
+ unmapped_tests = find_unmapped_tests(repo_root, mapping_db, test_dir, verbose)
191
+
192
+ if not unmapped_tests:
193
+ print("\nāœ… All tests already mapped!")
194
+ return 0
195
+
196
+ # Determine if we need chunking (OS ARG_MAX limit protection)
197
+ remaining_list = sorted(unmapped_tests)
198
+ total_tests = len(remaining_list)
199
+ chunk_size = 1000 # Safe threshold to avoid "Argument list too long"
200
+
201
+ needs_chunking = total_tests > chunk_size
202
+
203
+ print(f"\nšŸ“Š Test Statistics:")
204
+ print(f" Unmapped tests: {total_tests}")
205
+
206
+ print(f"\n{'='*80}")
207
+ if needs_chunking:
208
+ num_chunks = (total_tests + chunk_size - 1) // chunk_size
209
+ print(f"Running {total_tests} tests in {num_chunks} chunks of up to {chunk_size}...")
210
+ print(f"šŸ’” Each chunk updates the database immediately - safe to interrupt/resume")
211
+ else:
212
+ print(f"Running {total_tests} test(s) at once...")
213
+ print(f"{'='*80}")
214
+ print()
215
+
216
+ # Run tests (in chunks if needed)
217
+ all_passed = set()
218
+ all_failed = set()
219
+
220
+ try:
221
+ if needs_chunking:
222
+ # Run in chunks
223
+ for i in range(0, total_tests, chunk_size):
224
+ chunk = remaining_list[i:i + chunk_size]
225
+ chunk_num = (i // chunk_size) + 1
226
+ total_chunks = (total_tests + chunk_size - 1) // chunk_size
227
+
228
+ print(f"šŸ“¦ Chunk {chunk_num}/{total_chunks}: Running {len(chunk)} tests...", flush=True)
229
+
230
+ try:
231
+ passed, failed = run_test_chunk_with_mapping(
232
+ repo_root,
233
+ chunk,
234
+ mapping_db,
235
+ verbose
236
+ )
237
+
238
+ # Track for final statistics
239
+ all_passed.update(passed)
240
+ all_failed.update(failed)
241
+
242
+ print(f" āœ“ Chunk {chunk_num} complete: {len(passed)} passed, {len(failed)} failed", flush=True)
243
+
244
+ except Exception as e:
245
+ print(f" āŒ Chunk {chunk_num} failed: {e}", flush=True)
246
+ # Continue with next chunk
247
+ continue
248
+ else:
249
+ # Run all at once (small enough)
250
+ print(f"Running {len(remaining_list)} tests with coverage...", flush=True)
251
+
252
+ passed, failed = run_test_chunk_with_mapping(
253
+ repo_root,
254
+ remaining_list,
255
+ mapping_db,
256
+ verbose
257
+ )
258
+
259
+ # Track for final statistics
260
+ all_passed.update(passed)
261
+ all_failed.update(failed)
262
+
263
+ print(f"\nāœ“ {len(passed)} passed, āœ— {len(failed)} failed")
264
+
265
+ except KeyboardInterrupt:
266
+ print(f"\n\nāš ļø Interrupted by user!")
267
+ print(f"šŸ’” Next time you run, it will automatically continue from where it left off")
268
+ print(f" (unmapped tests are determined by querying the database)")
269
+ return 130
270
+
271
+ # Final summary
272
+ print(f"\n{'='*80}")
273
+ print("āœ… Mapping build complete!")
274
+ print(f"{'='*80}")
275
+ print(f"\nšŸ“ˆ Final Statistics:")
276
+ print(f" Successfully mapped: {len(all_passed)} tests")
277
+ print(f" Failed (skipped): {len(all_failed)} tests")
278
+
279
+ # Report failed tests
280
+ if all_failed:
281
+ print(f"\nāŒ Failed Tests ({len(all_failed)}):")
282
+ for test_name in sorted(all_failed)[:10]: # Show first 10
283
+ print(f" - {test_name}")
284
+ if len(all_failed) > 10:
285
+ print(f" ... and {len(all_failed) - 10} more")
286
+
287
+ # Show mapping DB stats
288
+ with TestMappingDBV2(mapping_db) as db:
289
+ stats = db.get_stats()
290
+ print(f"\nšŸ“Š Mapping Database:")
291
+ print(f" Total tests: {stats['total_tests']}")
292
+ print(f" Total files: {stats['total_files']}")
293
+ print(f" Total mappings: {stats['total_mappings']}")
294
+
295
+ return 0
296
+
297
+
298
+ def main():
299
+ parser = argparse.ArgumentParser(
300
+ description="Iteratively build test mapping database"
301
+ )
302
+ parser.add_argument(
303
+ "--repo-root",
304
+ type=Path,
305
+ default=Path.cwd(),
306
+ help="Repository root directory (default: current directory)"
307
+ )
308
+ parser.add_argument(
309
+ "--mapping-db",
310
+ type=Path,
311
+ help="Path to mapping database (default: <repo>/.test_mapping.db)"
312
+ )
313
+ parser.add_argument(
314
+ "--test-dir",
315
+ type=str,
316
+ default="unit_tests",
317
+ help="Test directory to collect from (default: unit_tests from pytest.ini)"
318
+ )
319
+ parser.add_argument(
320
+ "-v", "--verbose",
321
+ action="store_true",
322
+ help="Verbose output"
323
+ )
324
+
325
+ args = parser.parse_args()
326
+
327
+ repo_root = args.repo_root.resolve()
328
+ mapping_db = args.mapping_db or (repo_root / ".test_mapping.db")
329
+
330
+ return build_mapping_iteratively(
331
+ repo_root,
332
+ mapping_db,
333
+ args.test_dir,
334
+ args.verbose
335
+ )
336
+
337
+
338
+ if __name__ == "__main__":
339
+ sys.exit(main())