pintest-cli 0.3.1__tar.gz → 0.3.2__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.
Files changed (29) hide show
  1. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/PKG-INFO +1 -1
  2. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/cli.py +15 -2
  3. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/coverage_mapper.py +0 -7
  4. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/PKG-INFO +1 -1
  5. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/SOURCES.txt +1 -0
  6. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/setup.py +1 -1
  7. pintest_cli-0.3.2/tests/test_cli_e2e.py +184 -0
  8. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/README.md +0 -0
  9. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/__init__.py +0 -0
  10. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/build_mapping_iterative.py +0 -0
  11. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/cloud_mapping_db.py +0 -0
  12. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/config.py +0 -0
  13. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/git_diff_parser.py +0 -0
  14. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/post_commit_hook.py +0 -0
  15. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/pre_commit_hook.py +0 -0
  16. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/push_cache.py +0 -0
  17. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/pytest_plugin.py +0 -0
  18. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/range_set.py +0 -0
  19. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/test_mapping_db_v2.py +0 -0
  20. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest/update_mapping.py +0 -0
  21. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/dependency_links.txt +0 -0
  22. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/entry_points.txt +0 -0
  23. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/requires.txt +0 -0
  24. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/pintest_cli.egg-info/top_level.txt +0 -0
  25. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/setup.cfg +0 -0
  26. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/tests/__init__.py +0 -0
  27. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/tests/test_git_diff_parser.py +0 -0
  28. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/tests/test_new_feature.py +0 -0
  29. {pintest_cli-0.3.1 → pintest_cli-0.3.2}/tests/test_range_set.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pintest-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -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 and final_returncode == 0:
168
- final_returncode = result.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)...")
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pintest-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Run only the tests affected by your code changes.
5
5
  Author: Pintest Contributors
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -21,6 +21,7 @@ pintest_cli.egg-info/entry_points.txt
21
21
  pintest_cli.egg-info/requires.txt
22
22
  pintest_cli.egg-info/top_level.txt
23
23
  tests/__init__.py
24
+ tests/test_cli_e2e.py
24
25
  tests/test_git_diff_parser.py
25
26
  tests/test_new_feature.py
26
27
  tests/test_range_set.py
@@ -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.1",
8
+ version="0.3.2",
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