pintest-cli 0.3.1__tar.gz → 0.3.3__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.3.1 → pintest_cli-0.3.3}/PKG-INFO +1 -1
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/cli.py +30 -4
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/coverage_mapper.py +0 -7
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/PKG-INFO +1 -1
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/SOURCES.txt +1 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/setup.py +1 -1
- pintest_cli-0.3.3/tests/test_cli_e2e.py +184 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/README.md +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/__init__.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/build_mapping_iterative.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/cloud_mapping_db.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/config.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/git_diff_parser.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/post_commit_hook.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/pre_commit_hook.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/push_cache.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/pytest_plugin.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/range_set.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/test_mapping_db_v2.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest/update_mapping.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/dependency_links.txt +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/entry_points.txt +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/requires.txt +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/pintest_cli.egg-info/top_level.txt +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/setup.cfg +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/tests/__init__.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/tests/test_git_diff_parser.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/tests/test_new_feature.py +0 -0
- {pintest_cli-0.3.1 → pintest_cli-0.3.3}/tests/test_range_set.py +0 -0
|
@@ -133,6 +133,13 @@ class PintestRunner:
|
|
|
133
133
|
Returns:
|
|
134
134
|
Exit code from pytest (0 = success)
|
|
135
135
|
"""
|
|
136
|
+
if not test_selection:
|
|
137
|
+
if dry_run:
|
|
138
|
+
print("Would run 0 test(s)")
|
|
139
|
+
return 0
|
|
140
|
+
print("No tests to run!")
|
|
141
|
+
return 0
|
|
142
|
+
|
|
136
143
|
test_list = sorted(test_selection)
|
|
137
144
|
chunk_size = 500
|
|
138
145
|
needs_chunking = len(test_list) > chunk_size
|
|
@@ -158,14 +165,20 @@ class PintestRunner:
|
|
|
158
165
|
print(f"Running {len(test_selection)} selected test(s) in chunks of {chunk_size}...")
|
|
159
166
|
total_chunks = (len(test_list) + chunk_size - 1) // chunk_size
|
|
160
167
|
final_returncode = 0
|
|
168
|
+
fail_fast = pytest_args and any(arg == '-x' or arg.startswith('--maxfail') for arg in pytest_args)
|
|
169
|
+
|
|
161
170
|
for i in range(0, len(test_list), chunk_size):
|
|
162
171
|
chunk = test_list[i:i + chunk_size]
|
|
163
172
|
chunk_num = (i // chunk_size) + 1
|
|
164
173
|
print(f"\n📦 Running chunk {chunk_num}/{total_chunks} ({len(chunk)} tests)...", flush=True)
|
|
165
174
|
cmd = base_cmd + chunk
|
|
166
175
|
result = subprocess.run(cmd, cwd=self.repo_root)
|
|
167
|
-
if result.returncode != 0
|
|
168
|
-
final_returncode
|
|
176
|
+
if result.returncode != 0:
|
|
177
|
+
if final_returncode == 0:
|
|
178
|
+
final_returncode = result.returncode
|
|
179
|
+
if fail_fast:
|
|
180
|
+
print(f"\n⚠️ Fail-fast enabled (-x / --maxfail). Aborting remaining {total_chunks - chunk_num} chunk(s).", file=sys.stderr)
|
|
181
|
+
break
|
|
169
182
|
return final_returncode
|
|
170
183
|
else:
|
|
171
184
|
print(f"Running {len(test_selection)} selected test(s)...")
|
|
@@ -459,10 +472,23 @@ def cmd_track(args):
|
|
|
459
472
|
|
|
460
473
|
branch = args.branch
|
|
461
474
|
if not branch:
|
|
475
|
+
import subprocess
|
|
476
|
+
try:
|
|
477
|
+
default_branch = subprocess.run(
|
|
478
|
+
["git", "branch", "--show-current"],
|
|
479
|
+
capture_output=True,
|
|
480
|
+
text=True,
|
|
481
|
+
check=True
|
|
482
|
+
).stdout.strip()
|
|
483
|
+
if not default_branch:
|
|
484
|
+
default_branch = "main"
|
|
485
|
+
except Exception:
|
|
486
|
+
default_branch = "main"
|
|
487
|
+
|
|
462
488
|
try:
|
|
463
|
-
branch = input("🌿 Target branch to track [
|
|
489
|
+
branch = input(f"🌿 Target branch to track [{default_branch}]: ").strip()
|
|
464
490
|
if not branch:
|
|
465
|
-
branch =
|
|
491
|
+
branch = default_branch
|
|
466
492
|
except (KeyboardInterrupt, EOFError):
|
|
467
493
|
print("\n❌ Cancelled")
|
|
468
494
|
sys.exit(1)
|
|
@@ -264,13 +264,6 @@ class CoverageMapper:
|
|
|
264
264
|
except Exception:
|
|
265
265
|
pass
|
|
266
266
|
|
|
267
|
-
# Remove common source prefix
|
|
268
|
-
for prefix in ['src/']:
|
|
269
|
-
if prefix in path:
|
|
270
|
-
idx = path.index(prefix)
|
|
271
|
-
path = path[idx + len(prefix):]
|
|
272
|
-
break
|
|
273
|
-
|
|
274
267
|
# Remove absolute path prefixes
|
|
275
268
|
if path.startswith('/'):
|
|
276
269
|
parts = Path(path).parts
|
|
@@ -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.3.
|
|
8
|
+
version="0.3.3",
|
|
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",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import pytest
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def dummy_repo(tmp_path):
|
|
8
|
+
# 1. Create a dummy Python project in tmp_path
|
|
9
|
+
repo_dir = tmp_path / "dummy_repo"
|
|
10
|
+
repo_dir.mkdir()
|
|
11
|
+
|
|
12
|
+
# 2. Initialize git
|
|
13
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=repo_dir, check=True, capture_output=True)
|
|
14
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo_dir, check=True, capture_output=True)
|
|
15
|
+
subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo_dir, check=True, capture_output=True)
|
|
16
|
+
|
|
17
|
+
# 3. Create pintest.toml
|
|
18
|
+
pintest_toml = repo_dir / "pintest.toml"
|
|
19
|
+
pintest_toml.write_text("""
|
|
20
|
+
[cloud]
|
|
21
|
+
enabled = false
|
|
22
|
+
[local]
|
|
23
|
+
test_dir = "tests"
|
|
24
|
+
""")
|
|
25
|
+
|
|
26
|
+
# 4. Create source files
|
|
27
|
+
src_dir = repo_dir / "src"
|
|
28
|
+
src_dir.mkdir()
|
|
29
|
+
|
|
30
|
+
math_py = src_dir / "math_utils.py"
|
|
31
|
+
math_py.write_text("""def add(a, b):
|
|
32
|
+
return a + b
|
|
33
|
+
|
|
34
|
+
def subtract(a, b):
|
|
35
|
+
return a - b
|
|
36
|
+
""")
|
|
37
|
+
|
|
38
|
+
string_py = src_dir / "string_utils.py"
|
|
39
|
+
string_py.write_text("""def to_upper(s):
|
|
40
|
+
return s.upper()
|
|
41
|
+
""")
|
|
42
|
+
|
|
43
|
+
# 5. Create test files
|
|
44
|
+
tests_dir = repo_dir / "tests"
|
|
45
|
+
tests_dir.mkdir()
|
|
46
|
+
|
|
47
|
+
test_math_py = tests_dir / "test_math.py"
|
|
48
|
+
test_math_py.write_text("""import sys
|
|
49
|
+
import os
|
|
50
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
51
|
+
from src.math_utils import add, subtract
|
|
52
|
+
|
|
53
|
+
def test_add():
|
|
54
|
+
assert add(1, 2) == 3
|
|
55
|
+
|
|
56
|
+
def test_subtract():
|
|
57
|
+
assert subtract(2, 1) == 1
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
test_string_py = tests_dir / "test_string.py"
|
|
61
|
+
test_string_py.write_text("""import sys
|
|
62
|
+
import os
|
|
63
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
64
|
+
from src.string_utils import to_upper
|
|
65
|
+
|
|
66
|
+
def test_to_upper():
|
|
67
|
+
assert to_upper("a") == "A"
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
# 6. Commit everything
|
|
71
|
+
subprocess.run(["git", "add", "."], cwd=repo_dir, check=True, capture_output=True)
|
|
72
|
+
subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=repo_dir, check=True, capture_output=True)
|
|
73
|
+
|
|
74
|
+
return repo_dir
|
|
75
|
+
|
|
76
|
+
def run_pintest(args, cwd):
|
|
77
|
+
env = os.environ.copy()
|
|
78
|
+
env["HOME"] = str(cwd) # Prevent reading global ~/.pintest/config.toml
|
|
79
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
80
|
+
env["PYTHONPATH"] = repo_root + (os.pathsep + env["PYTHONPATH"] if "PYTHONPATH" in env else "")
|
|
81
|
+
cmd = [sys.executable, "-m", "pintest.cli"] + args
|
|
82
|
+
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env=env)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def test_pintest_track_and_run(dummy_repo):
|
|
86
|
+
# Scenario A (Baseline): Run pintest build-mapping
|
|
87
|
+
res = run_pintest(["build-mapping"], cwd=dummy_repo)
|
|
88
|
+
assert res.returncode == 0
|
|
89
|
+
|
|
90
|
+
# Check if mapping db is created
|
|
91
|
+
db_path = dummy_repo / ".pintest" / "test_mapping.db"
|
|
92
|
+
assert db_path.exists()
|
|
93
|
+
|
|
94
|
+
# Verify nothing to run on clean working tree
|
|
95
|
+
res = run_pintest(["run", "--dry-run", "--base-branch=main"], cwd=dummy_repo)
|
|
96
|
+
assert res.returncode == 0
|
|
97
|
+
# Expected: "Would run 0 test" or "No tests to run!" or "Running 0 selected"
|
|
98
|
+
# Actually, current code outputs "Would run 0 test(s)" if test_selection is not empty but dry-run is provided?
|
|
99
|
+
# No, cli.py has `if not test_selection: print("No tests to run!"); return 0`
|
|
100
|
+
assert "No tests to run" in res.stdout or "0 test(s)" in res.stdout
|
|
101
|
+
|
|
102
|
+
# Scenario C (True Positive): Modify a tracked line
|
|
103
|
+
math_py = dummy_repo / "src" / "math_utils.py"
|
|
104
|
+
content = math_py.read_text()
|
|
105
|
+
# Change add function
|
|
106
|
+
content = content.replace("return a + b", "return a + b + 0")
|
|
107
|
+
math_py.write_text(content)
|
|
108
|
+
|
|
109
|
+
res = run_pintest(["run", "--dry-run", "--base-branch=main", "-v"], cwd=dummy_repo)
|
|
110
|
+
print("STDOUT:", res.stdout)
|
|
111
|
+
print("STDERR:", res.stderr)
|
|
112
|
+
assert res.returncode == 0
|
|
113
|
+
assert "tests/test_math.py::test_add" in res.stdout or "test_math.py" in res.stdout
|
|
114
|
+
assert "test_string.py" not in res.stdout
|
|
115
|
+
|
|
116
|
+
# Revert math.py
|
|
117
|
+
subprocess.run(["git", "checkout", "--", "src/math_utils.py"], cwd=dummy_repo, check=True)
|
|
118
|
+
|
|
119
|
+
# Scenario D (True Negative): Add a comment
|
|
120
|
+
content = math_py.read_text()
|
|
121
|
+
content += "\n# This is a comment\n"
|
|
122
|
+
math_py.write_text(content)
|
|
123
|
+
|
|
124
|
+
res = run_pintest(["run", "--dry-run", "--base-branch=main"], cwd=dummy_repo)
|
|
125
|
+
assert res.returncode == 0
|
|
126
|
+
# The comment line was never executed, so adding it shouldn't trigger any tests.
|
|
127
|
+
assert "No tests to run" in res.stdout or "0 test(s)" in res.stdout
|
|
128
|
+
|
|
129
|
+
# Revert math.py
|
|
130
|
+
subprocess.run(["git", "checkout", "--", "src/math_utils.py"], cwd=dummy_repo, check=True)
|
|
131
|
+
|
|
132
|
+
# Scenario E (New Test Discovery): Add a new test file
|
|
133
|
+
test_new_py = dummy_repo / "tests" / "test_new.py"
|
|
134
|
+
test_new_py.write_text("""def test_something_new():\n assert True\n""")
|
|
135
|
+
subprocess.run(["git", "add", "tests/test_new.py"], cwd=dummy_repo, check=True)
|
|
136
|
+
|
|
137
|
+
res = run_pintest(["run", "--dry-run", "--base-branch=main"], cwd=dummy_repo)
|
|
138
|
+
assert res.returncode == 0
|
|
139
|
+
assert "test_new.py" in res.stdout
|
|
140
|
+
|
|
141
|
+
# Remove test_new.py and unstage
|
|
142
|
+
subprocess.run(["git", "rm", "-f", "tests/test_new.py"], cwd=dummy_repo, check=True)
|
|
143
|
+
|
|
144
|
+
# Scenario F (Deleted File): Delete a test file and modify related source
|
|
145
|
+
os.remove(dummy_repo / "tests" / "test_string.py")
|
|
146
|
+
subprocess.run(["git", "add", "."], cwd=dummy_repo, check=True)
|
|
147
|
+
subprocess.run(["git", "commit", "-m", "Remove test_string.py"], cwd=dummy_repo, check=True)
|
|
148
|
+
|
|
149
|
+
# Modify string_utils.py which used to be covered by the deleted test
|
|
150
|
+
string_py = dummy_repo / "src" / "string_utils.py"
|
|
151
|
+
content = string_py.read_text()
|
|
152
|
+
content = content.replace("return s.upper()", "return s.upper() + ''")
|
|
153
|
+
string_py.write_text(content)
|
|
154
|
+
|
|
155
|
+
res = run_pintest(["run", "--dry-run", "--base-branch=main"], cwd=dummy_repo)
|
|
156
|
+
assert res.returncode == 0
|
|
157
|
+
# test_string_py is gone. pintest run shouldn't crash trying to find it.
|
|
158
|
+
assert "Exception" not in res.stderr
|
|
159
|
+
assert "Traceback" not in res.stderr
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_pintest_chunking_and_fail_fast(dummy_repo):
|
|
163
|
+
from pintest.cli import PintestRunner
|
|
164
|
+
runner = PintestRunner(dummy_repo, verbose=True)
|
|
165
|
+
|
|
166
|
+
# Generate 505 dummy test names
|
|
167
|
+
dummy_tests = {f"tests/test_math.py::test_dummy_{i}" for i in range(505)}
|
|
168
|
+
|
|
169
|
+
# Run with fail-fast (-x)
|
|
170
|
+
# Since these dummy tests don't exist, pytest will fail on the first chunk.
|
|
171
|
+
# PintestRunner should catch the failure and abort the second chunk.
|
|
172
|
+
import io
|
|
173
|
+
from unittest.mock import patch
|
|
174
|
+
|
|
175
|
+
# Capture stderr to verify the abort message
|
|
176
|
+
stderr_buf = io.StringIO()
|
|
177
|
+
with patch("sys.stderr", stderr_buf):
|
|
178
|
+
returncode = runner.run_tests(dummy_tests, dry_run=False, verbose=True, pytest_args=["-x"])
|
|
179
|
+
|
|
180
|
+
assert returncode != 0
|
|
181
|
+
stderr_output = stderr_buf.getvalue()
|
|
182
|
+
assert "Fail-fast enabled" in stderr_output
|
|
183
|
+
assert "Aborting remaining 1 chunk(s)" in stderr_output
|
|
184
|
+
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|