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.
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/PKG-INFO +1 -1
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/build_mapping_iterative.py +15 -2
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/cli.py +22 -5
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/cloud_mapping_db.py +29 -2
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/pre_commit_hook.py +15 -4
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/push_cache.py +15 -9
- pintest_cli-0.2.8/pintest/pytest_plugin.py +26 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/update_mapping.py +11 -2
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/PKG-INFO +1 -1
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/SOURCES.txt +1 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/setup.py +1 -1
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/README.md +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/__init__.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/config.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/coverage_mapper.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/git_diff_parser.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/post_commit_hook.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/range_set.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest/test_mapping_db_v2.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/dependency_links.txt +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/entry_points.txt +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/requires.txt +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/pintest_cli.egg-info/top_level.txt +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/setup.cfg +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/__init__.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_git_diff_parser.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_new_feature.py +0 -0
- {pintest_cli-0.2.6 → pintest_cli-0.2.8}/tests/test_range_set.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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",
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|