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