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 +14 -0
- pintest/build_mapping_iterative.py +339 -0
- pintest/cli.py +681 -0
- pintest/cloud_mapping_db.py +218 -0
- pintest/config.py +102 -0
- pintest/coverage_mapper.py +356 -0
- pintest/git_diff_parser.py +232 -0
- pintest/post_commit_hook.py +78 -0
- pintest/pre_commit_hook.py +1472 -0
- pintest/range_set.py +173 -0
- pintest/test_mapping_db_v2.py +381 -0
- pintest/update_mapping.py +130 -0
- pintest_cli-0.2.0.dist-info/METADATA +527 -0
- pintest_cli-0.2.0.dist-info/RECORD +21 -0
- pintest_cli-0.2.0.dist-info/WHEEL +5 -0
- pintest_cli-0.2.0.dist-info/entry_points.txt +2 -0
- pintest_cli-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_git_diff_parser.py +60 -0
- tests/test_new_feature.py +1 -0
- tests/test_range_set.py +261 -0
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())
|