testpick 0.2.0__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.
- testpick-0.2.0/.github/workflows/ci.yml +85 -0
- testpick-0.2.0/.gitignore +35 -0
- testpick-0.2.0/CHANGELOG.md +32 -0
- testpick-0.2.0/LICENSE +21 -0
- testpick-0.2.0/PKG-INFO +211 -0
- testpick-0.2.0/README.md +173 -0
- testpick-0.2.0/pyproject.toml +86 -0
- testpick-0.2.0/src/testpick/__init__.py +10 -0
- testpick-0.2.0/src/testpick/__main__.py +3 -0
- testpick-0.2.0/src/testpick/cli.py +228 -0
- testpick-0.2.0/src/testpick/git/__init__.py +21 -0
- testpick-0.2.0/src/testpick/git/commit.py +28 -0
- testpick-0.2.0/src/testpick/git/diff.py +59 -0
- testpick-0.2.0/src/testpick/git/interactive.py +106 -0
- testpick-0.2.0/src/testpick/graph/__init__.py +7 -0
- testpick-0.2.0/src/testpick/graph/builder.py +201 -0
- testpick-0.2.0/src/testpick/graph/edge.py +13 -0
- testpick-0.2.0/src/testpick/graph/node.py +16 -0
- testpick-0.2.0/src/testpick/parser/__init__.py +18 -0
- testpick-0.2.0/src/testpick/parser/base.py +46 -0
- testpick-0.2.0/src/testpick/parser/python.py +225 -0
- testpick-0.2.0/src/testpick/parser/registry.py +24 -0
- testpick-0.2.0/src/testpick/parser/typescript.py +207 -0
- testpick-0.2.0/src/testpick/reporter/__init__.py +7 -0
- testpick-0.2.0/src/testpick/reporter/github.py +58 -0
- testpick-0.2.0/src/testpick/reporter/json_reporter.py +49 -0
- testpick-0.2.0/src/testpick/reporter/terminal.py +108 -0
- testpick-0.2.0/src/testpick/selector/__init__.py +6 -0
- testpick-0.2.0/src/testpick/selector/matcher.py +58 -0
- testpick-0.2.0/src/testpick/selector/walker.py +42 -0
- testpick-0.2.0/tests/conftest.py +0 -0
- testpick-0.2.0/tests/test_cli.py +85 -0
- testpick-0.2.0/tests/test_git/__init__.py +0 -0
- testpick-0.2.0/tests/test_git/test_commit.py +75 -0
- testpick-0.2.0/tests/test_git/test_diff.py +78 -0
- testpick-0.2.0/tests/test_git/test_interactive.py +5 -0
- testpick-0.2.0/tests/test_graph/__init__.py +0 -0
- testpick-0.2.0/tests/test_graph/test_builder.py +221 -0
- testpick-0.2.0/tests/test_parser/__init__.py +0 -0
- testpick-0.2.0/tests/test_parser/test_python.py +164 -0
- testpick-0.2.0/tests/test_parser/test_typescript.py +158 -0
- testpick-0.2.0/tests/test_selection_pipeline.py +218 -0
- testpick-0.2.0/tests/test_selector/__init__.py +1 -0
- testpick-0.2.0/tests/test_selector/test_matcher.py +126 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
pull_request:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
tag:
|
|
12
|
+
description: 'Tag to release (e.g., v0.3.0)'
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
- run: pip install -e ".[dev,tree-sitter]"
|
|
24
|
+
- run: ruff check .
|
|
25
|
+
- run: mypy
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
runs-on: ${{ matrix.os }}
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest]
|
|
33
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
34
|
+
include:
|
|
35
|
+
- os: windows-latest
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
- os: macos-latest
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
- run: pip install -e ".[dev,tree-sitter]"
|
|
45
|
+
- run: pytest tests/ --cov=testpick --cov-report=term-missing
|
|
46
|
+
|
|
47
|
+
build:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: "3.12"
|
|
54
|
+
- run: pip install build
|
|
55
|
+
- run: python -m build
|
|
56
|
+
|
|
57
|
+
- name: Upload Build Artifacts
|
|
58
|
+
uses: actions/upload-artifact@v4
|
|
59
|
+
with:
|
|
60
|
+
name: python-package-distributions
|
|
61
|
+
path: dist/
|
|
62
|
+
|
|
63
|
+
release:
|
|
64
|
+
needs: [lint, test, build]
|
|
65
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag)
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
permissions:
|
|
68
|
+
contents: write
|
|
69
|
+
id-token: write
|
|
70
|
+
steps:
|
|
71
|
+
- name: Download Build Artifacts
|
|
72
|
+
uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: python-package-distributions
|
|
75
|
+
path: dist/
|
|
76
|
+
|
|
77
|
+
- name: Create GitHub Release
|
|
78
|
+
uses: softprops/action-gh-release@v2
|
|
79
|
+
with:
|
|
80
|
+
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
|
81
|
+
files: dist/*
|
|
82
|
+
generate_release_notes: true
|
|
83
|
+
|
|
84
|
+
- name: Publish to PyPI
|
|
85
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.whl
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
ENV/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# Test
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.mypy_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
|
|
30
|
+
# Local tool state
|
|
31
|
+
.freebuff/
|
|
32
|
+
|
|
33
|
+
# Misc
|
|
34
|
+
*.log
|
|
35
|
+
*.tgz
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- TypeScript and JavaScript imports now produce real graph edges. Previously only
|
|
8
|
+
Python files were ever linked, so chains came back empty on JS/TS projects.
|
|
9
|
+
- Relative specifiers starting with `./` or `../` no longer resolve against the
|
|
10
|
+
filesystem root.
|
|
11
|
+
- `--commit` analyzes the commit you pass; `--base <branch>` compares against the
|
|
12
|
+
branch point instead of the working tree.
|
|
13
|
+
- The `tree-sitter` extra installs the grammars the parsers actually import, so AST
|
|
14
|
+
parsing activates instead of silently falling back to regex.
|
|
15
|
+
- Dependency chains in terminal, JSON and GitHub output are real paths through the
|
|
16
|
+
graph rather than a copy of the changed-file list.
|
|
17
|
+
- Renamed files no longer report a mangled `old<TAB>new` path, and deleted tests are
|
|
18
|
+
never selected.
|
|
19
|
+
- JSON and GitHub Actions output use forward slashes, so paths work on Windows
|
|
20
|
+
runners.
|
|
21
|
+
- Everything resolves from the repository root, so the command works from any
|
|
22
|
+
subdirectory.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- CI: ruff, mypy and pytest on Python 3.10-3.13 across Linux, macOS and Windows,
|
|
27
|
+
plus a package build check.
|
|
28
|
+
- End-to-end tests for the selection pipeline (77 tests, 82% coverage).
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- Unused `--dry-run` option and the unwired `SymbolTracker`.
|
testpick-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 krauzX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
testpick-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: testpick
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Pick the tests affected by your git diff before you run them
|
|
5
|
+
Project-URL: Homepage, https://github.com/krauzX/testpick
|
|
6
|
+
Project-URL: Repository, https://github.com/krauzX/testpick
|
|
7
|
+
Project-URL: Issues, https://github.com/krauzX/testpick/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ci,git,pytest,static-analysis,testing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: gitpython>=3.1
|
|
25
|
+
Requires-Dist: readchar>=4.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
32
|
+
Provides-Extra: tree-sitter
|
|
33
|
+
Requires-Dist: tree-sitter-javascript>=0.23; extra == 'tree-sitter'
|
|
34
|
+
Requires-Dist: tree-sitter-python>=0.23; extra == 'tree-sitter'
|
|
35
|
+
Requires-Dist: tree-sitter-typescript>=0.23; extra == 'tree-sitter'
|
|
36
|
+
Requires-Dist: tree-sitter>=0.25; extra == 'tree-sitter'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# testpick
|
|
40
|
+
|
|
41
|
+
[](https://github.com/krauzX/testpick/actions/workflows/ci.yml)
|
|
42
|
+
|
|
43
|
+
A Python CLI that looks at your git diff and tells you which tests are going to break. Before you run them.
|
|
44
|
+
|
|
45
|
+
## Why this exists
|
|
46
|
+
|
|
47
|
+
You change a function in `utils.py`. Now what? You could run the full test suite and wait, or you could guess
|
|
48
|
+
which tests matter and hope you're right. `pytest --lf` reruns failures only after they've already failed.
|
|
49
|
+
Jest's `--changedSince` follows direct file changes.
|
|
50
|
+
|
|
51
|
+
testpick parses your git diff, builds an import graph of your source files, and walks it backwards from what
|
|
52
|
+
you changed to find every test that depends on it — transitively. Each selected test comes with the chain that
|
|
53
|
+
explains why, so you can sanity-check the answer instead of trusting it.
|
|
54
|
+
|
|
55
|
+
## Getting started
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/krauzX/testpick.git
|
|
59
|
+
cd testpick
|
|
60
|
+
pip install -e .
|
|
61
|
+
|
|
62
|
+
# optional but recommended: real AST parsing instead of the regex fallback
|
|
63
|
+
pip install -e ".[tree-sitter]"
|
|
64
|
+
|
|
65
|
+
# for development (tests, linter, type checker)
|
|
66
|
+
pip install -e ".[dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Python 3.10+, runs on Linux, macOS and Windows.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# what changed since your last commit, including uncommitted work?
|
|
75
|
+
testpick
|
|
76
|
+
|
|
77
|
+
# committed changes on your branch since it diverged from main
|
|
78
|
+
testpick --base main
|
|
79
|
+
|
|
80
|
+
# a single commit
|
|
81
|
+
testpick --commit abc123
|
|
82
|
+
|
|
83
|
+
# output for scripts and CI
|
|
84
|
+
testpick --format json
|
|
85
|
+
testpick --format github # GitHub Actions annotations
|
|
86
|
+
|
|
87
|
+
# only analyze Python and TypeScript files
|
|
88
|
+
testpick --languages python,typescript
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Everything resolves from the repository root, so it works from any directory inside the repo.
|
|
92
|
+
|
|
93
|
+
### Example output
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
97
|
+
│ testpick - Test Selection Results │
|
|
98
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
99
|
+
|
|
100
|
+
Changed Files
|
|
101
|
+
┌────────────┬────────────────────────────────────────┐
|
|
102
|
+
│ Status │ File │
|
|
103
|
+
├────────────┼────────────────────────────────────────┤
|
|
104
|
+
│ Modified │ src/testpick/graph/builder.py │
|
|
105
|
+
│ Modified │ src/testpick/reporter/github.py │
|
|
106
|
+
│ Modified │ src/testpick/reporter/json_reporter.py │
|
|
107
|
+
│ Added │ tests/test_selection_pipeline.py │
|
|
108
|
+
└────────────┴────────────────────────────────────────┘
|
|
109
|
+
|
|
110
|
+
Selected Tests
|
|
111
|
+
┌───────┬──────────────────────────────────┬──────────────────────────────────┐
|
|
112
|
+
│ # │ Test File │ Dependency Path │
|
|
113
|
+
├───────┼──────────────────────────────────┼──────────────────────────────────┤
|
|
114
|
+
│ 1 │ tests/test_cli.py │ tests/test_cli.py -> │
|
|
115
|
+
│ │ │ src/testpick/cli.py -> │
|
|
116
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
117
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
118
|
+
│ 2 │ tests/test_graph/test_builder.py │ tests/test_graph/test_builder.py │
|
|
119
|
+
│ │ │ -> │
|
|
120
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
121
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
122
|
+
│ 3 │ tests/test_selection_pipeline.py │ direct │
|
|
123
|
+
│ 4 │ tests/test_selector/test_matche… │ tests/test_selector/test_matche… │
|
|
124
|
+
│ │ │ -> │
|
|
125
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
126
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
127
|
+
└───────┴──────────────────────────────────┴──────────────────────────────────┘
|
|
128
|
+
|
|
129
|
+
Statistics
|
|
130
|
+
┌────────────────┬─────┐
|
|
131
|
+
│ Files Changed │ 4 │
|
|
132
|
+
│ Tests Selected │ 4 │
|
|
133
|
+
│ Insertions │ 258 │
|
|
134
|
+
│ Deletions │ 25 │
|
|
135
|
+
└────────────────┴─────┘
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`direct` means the test file itself changed. The JSON format carries the same
|
|
139
|
+
information, with chains as arrays:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"changed_files": [
|
|
144
|
+
{ "status": "M", "path": "src/utils.ts" }
|
|
145
|
+
],
|
|
146
|
+
"test_files": [
|
|
147
|
+
{
|
|
148
|
+
"path": "tests/test_page.ts",
|
|
149
|
+
"dependency_chain": ["tests/test_page.ts", "src/page.ts", "src/utils.ts"]
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"stats": {
|
|
153
|
+
"files_changed": 1,
|
|
154
|
+
"insertions": 6,
|
|
155
|
+
"deletions": 2,
|
|
156
|
+
"tests_selected": 1
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## How it works
|
|
162
|
+
|
|
163
|
+
1. Read the diff with `git diff --name-status` and resolve every path against the repository root.
|
|
164
|
+
2. Parse each source file for imports. tree-sitter grammars are used when installed, otherwise a regex pass
|
|
165
|
+
handles Python, TypeScript and JavaScript.
|
|
166
|
+
3. Resolve each import to a file: relative specifiers against the importing file's directory, absolute ones
|
|
167
|
+
against `src/`, the project root, then the file's own directory — preferring the importing file's language
|
|
168
|
+
when both a `.py` and a `.ts` file would match.
|
|
169
|
+
4. Walk reverse edges from the changed files with a breadth-first search, recording the parent of every hit so
|
|
170
|
+
the chain can be explained afterwards.
|
|
171
|
+
5. Report the files that are test files, direct changes first.
|
|
172
|
+
|
|
173
|
+
## How it compares
|
|
174
|
+
|
|
175
|
+
Other tools pick tests from a diff too; they differ in how far they can see:
|
|
176
|
+
|
|
177
|
+
- **pytest-testmon** records which code each test actually executed, so it catches runtime coupling a static
|
|
178
|
+
graph misses — but it needs a baseline run and is pytest-only.
|
|
179
|
+
- **Jest `--changedSince`** maps changed files to tests, but inside a Jest project and mostly direct
|
|
180
|
+
relationships.
|
|
181
|
+
- **Nx and Turborepo affected tasks** pick packages to rebuild, which is coarser than picking individual tests.
|
|
182
|
+
- **snob, pytest-impacted, pytest-git-selector** do static graph selection like this one, but for Python only.
|
|
183
|
+
|
|
184
|
+
testpick's corner: one CLI covering Python and TypeScript/JavaScript, no baseline run required, and every
|
|
185
|
+
selection arrives with the chain that produced it.
|
|
186
|
+
|
|
187
|
+
## Limitations
|
|
188
|
+
|
|
189
|
+
Static analysis only sees imports it can read:
|
|
190
|
+
|
|
191
|
+
- Dynamic imports, dependency injection, plugin registries and other runtime wiring are invisible to it.
|
|
192
|
+
- A file deleted from the working tree is gone from the parse tree, so testpick reports the deletion but
|
|
193
|
+
cannot trace what used to import it.
|
|
194
|
+
- `--languages` scopes which files are analyzed for dependencies; changed files are always reported.
|
|
195
|
+
|
|
196
|
+
Treat the output as a starting point. A full suite run still belongs in CI before a release.
|
|
197
|
+
|
|
198
|
+
## Running tests
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pytest tests/ -v
|
|
202
|
+
pytest tests/ --cov=testpick
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
77 tests covering diff parsing, import resolution, graph building, the selection pipeline and all three
|
|
206
|
+
reporters, with 82% branch coverage. CI runs ruff, mypy and the suite on Python 3.10-3.13 across Linux, macOS
|
|
207
|
+
and Windows.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
testpick-0.2.0/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# testpick
|
|
2
|
+
|
|
3
|
+
[](https://github.com/krauzX/testpick/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A Python CLI that looks at your git diff and tells you which tests are going to break. Before you run them.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
You change a function in `utils.py`. Now what? You could run the full test suite and wait, or you could guess
|
|
10
|
+
which tests matter and hope you're right. `pytest --lf` reruns failures only after they've already failed.
|
|
11
|
+
Jest's `--changedSince` follows direct file changes.
|
|
12
|
+
|
|
13
|
+
testpick parses your git diff, builds an import graph of your source files, and walks it backwards from what
|
|
14
|
+
you changed to find every test that depends on it — transitively. Each selected test comes with the chain that
|
|
15
|
+
explains why, so you can sanity-check the answer instead of trusting it.
|
|
16
|
+
|
|
17
|
+
## Getting started
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/krauzX/testpick.git
|
|
21
|
+
cd testpick
|
|
22
|
+
pip install -e .
|
|
23
|
+
|
|
24
|
+
# optional but recommended: real AST parsing instead of the regex fallback
|
|
25
|
+
pip install -e ".[tree-sitter]"
|
|
26
|
+
|
|
27
|
+
# for development (tests, linter, type checker)
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Python 3.10+, runs on Linux, macOS and Windows.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# what changed since your last commit, including uncommitted work?
|
|
37
|
+
testpick
|
|
38
|
+
|
|
39
|
+
# committed changes on your branch since it diverged from main
|
|
40
|
+
testpick --base main
|
|
41
|
+
|
|
42
|
+
# a single commit
|
|
43
|
+
testpick --commit abc123
|
|
44
|
+
|
|
45
|
+
# output for scripts and CI
|
|
46
|
+
testpick --format json
|
|
47
|
+
testpick --format github # GitHub Actions annotations
|
|
48
|
+
|
|
49
|
+
# only analyze Python and TypeScript files
|
|
50
|
+
testpick --languages python,typescript
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Everything resolves from the repository root, so it works from any directory inside the repo.
|
|
54
|
+
|
|
55
|
+
### Example output
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
59
|
+
│ testpick - Test Selection Results │
|
|
60
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
61
|
+
|
|
62
|
+
Changed Files
|
|
63
|
+
┌────────────┬────────────────────────────────────────┐
|
|
64
|
+
│ Status │ File │
|
|
65
|
+
├────────────┼────────────────────────────────────────┤
|
|
66
|
+
│ Modified │ src/testpick/graph/builder.py │
|
|
67
|
+
│ Modified │ src/testpick/reporter/github.py │
|
|
68
|
+
│ Modified │ src/testpick/reporter/json_reporter.py │
|
|
69
|
+
│ Added │ tests/test_selection_pipeline.py │
|
|
70
|
+
└────────────┴────────────────────────────────────────┘
|
|
71
|
+
|
|
72
|
+
Selected Tests
|
|
73
|
+
┌───────┬──────────────────────────────────┬──────────────────────────────────┐
|
|
74
|
+
│ # │ Test File │ Dependency Path │
|
|
75
|
+
├───────┼──────────────────────────────────┼──────────────────────────────────┤
|
|
76
|
+
│ 1 │ tests/test_cli.py │ tests/test_cli.py -> │
|
|
77
|
+
│ │ │ src/testpick/cli.py -> │
|
|
78
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
79
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
80
|
+
│ 2 │ tests/test_graph/test_builder.py │ tests/test_graph/test_builder.py │
|
|
81
|
+
│ │ │ -> │
|
|
82
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
83
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
84
|
+
│ 3 │ tests/test_selection_pipeline.py │ direct │
|
|
85
|
+
│ 4 │ tests/test_selector/test_matche… │ tests/test_selector/test_matche… │
|
|
86
|
+
│ │ │ -> │
|
|
87
|
+
│ │ │ src/testpick/graph/__init__.py │
|
|
88
|
+
│ │ │ -> src/testpick/graph/builder.py │
|
|
89
|
+
└───────┴──────────────────────────────────┴──────────────────────────────────┘
|
|
90
|
+
|
|
91
|
+
Statistics
|
|
92
|
+
┌────────────────┬─────┐
|
|
93
|
+
│ Files Changed │ 4 │
|
|
94
|
+
│ Tests Selected │ 4 │
|
|
95
|
+
│ Insertions │ 258 │
|
|
96
|
+
│ Deletions │ 25 │
|
|
97
|
+
└────────────────┴─────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`direct` means the test file itself changed. The JSON format carries the same
|
|
101
|
+
information, with chains as arrays:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"changed_files": [
|
|
106
|
+
{ "status": "M", "path": "src/utils.ts" }
|
|
107
|
+
],
|
|
108
|
+
"test_files": [
|
|
109
|
+
{
|
|
110
|
+
"path": "tests/test_page.ts",
|
|
111
|
+
"dependency_chain": ["tests/test_page.ts", "src/page.ts", "src/utils.ts"]
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"stats": {
|
|
115
|
+
"files_changed": 1,
|
|
116
|
+
"insertions": 6,
|
|
117
|
+
"deletions": 2,
|
|
118
|
+
"tests_selected": 1
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How it works
|
|
124
|
+
|
|
125
|
+
1. Read the diff with `git diff --name-status` and resolve every path against the repository root.
|
|
126
|
+
2. Parse each source file for imports. tree-sitter grammars are used when installed, otherwise a regex pass
|
|
127
|
+
handles Python, TypeScript and JavaScript.
|
|
128
|
+
3. Resolve each import to a file: relative specifiers against the importing file's directory, absolute ones
|
|
129
|
+
against `src/`, the project root, then the file's own directory — preferring the importing file's language
|
|
130
|
+
when both a `.py` and a `.ts` file would match.
|
|
131
|
+
4. Walk reverse edges from the changed files with a breadth-first search, recording the parent of every hit so
|
|
132
|
+
the chain can be explained afterwards.
|
|
133
|
+
5. Report the files that are test files, direct changes first.
|
|
134
|
+
|
|
135
|
+
## How it compares
|
|
136
|
+
|
|
137
|
+
Other tools pick tests from a diff too; they differ in how far they can see:
|
|
138
|
+
|
|
139
|
+
- **pytest-testmon** records which code each test actually executed, so it catches runtime coupling a static
|
|
140
|
+
graph misses — but it needs a baseline run and is pytest-only.
|
|
141
|
+
- **Jest `--changedSince`** maps changed files to tests, but inside a Jest project and mostly direct
|
|
142
|
+
relationships.
|
|
143
|
+
- **Nx and Turborepo affected tasks** pick packages to rebuild, which is coarser than picking individual tests.
|
|
144
|
+
- **snob, pytest-impacted, pytest-git-selector** do static graph selection like this one, but for Python only.
|
|
145
|
+
|
|
146
|
+
testpick's corner: one CLI covering Python and TypeScript/JavaScript, no baseline run required, and every
|
|
147
|
+
selection arrives with the chain that produced it.
|
|
148
|
+
|
|
149
|
+
## Limitations
|
|
150
|
+
|
|
151
|
+
Static analysis only sees imports it can read:
|
|
152
|
+
|
|
153
|
+
- Dynamic imports, dependency injection, plugin registries and other runtime wiring are invisible to it.
|
|
154
|
+
- A file deleted from the working tree is gone from the parse tree, so testpick reports the deletion but
|
|
155
|
+
cannot trace what used to import it.
|
|
156
|
+
- `--languages` scopes which files are analyzed for dependencies; changed files are always reported.
|
|
157
|
+
|
|
158
|
+
Treat the output as a starting point. A full suite run still belongs in CI before a release.
|
|
159
|
+
|
|
160
|
+
## Running tests
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pytest tests/ -v
|
|
164
|
+
pytest tests/ --cov=testpick
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
77 tests covering diff parsing, import resolution, graph building, the selection pipeline and all three
|
|
168
|
+
reporters, with 82% branch coverage. CI runs ruff, mypy and the suite on Python 3.10-3.13 across Linux, macOS
|
|
169
|
+
and Windows.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "testpick"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Pick the tests affected by your git diff before you run them"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["testing", "pytest", "git", "static-analysis", "ci"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"click>=8.0",
|
|
28
|
+
"gitpython>=3.1",
|
|
29
|
+
"rich>=13.0",
|
|
30
|
+
"readchar>=4.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
tree-sitter = [
|
|
35
|
+
"tree-sitter>=0.25",
|
|
36
|
+
"tree-sitter-python>=0.23",
|
|
37
|
+
"tree-sitter-typescript>=0.23",
|
|
38
|
+
"tree-sitter-javascript>=0.23",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-cov>=5.0",
|
|
43
|
+
"ruff>=0.5",
|
|
44
|
+
"mypy>=1.10",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/krauzX/testpick"
|
|
49
|
+
Repository = "https://github.com/krauzX/testpick"
|
|
50
|
+
Issues = "https://github.com/krauzX/testpick/issues"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
testpick = "testpick.cli:main"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/testpick"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 120
|
|
60
|
+
target-version = "py310"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "W", "B"]
|
|
64
|
+
|
|
65
|
+
[tool.mypy]
|
|
66
|
+
python_version = "3.10"
|
|
67
|
+
files = ["src/testpick"]
|
|
68
|
+
check_untyped_defs = true
|
|
69
|
+
no_implicit_optional = true
|
|
70
|
+
warn_unused_ignores = true
|
|
71
|
+
|
|
72
|
+
# rich imports IPython, whose numpy stubs use syntax newer than our target
|
|
73
|
+
[[tool.mypy.overrides]]
|
|
74
|
+
module = ["IPython", "IPython.*", "numpy", "numpy.*"]
|
|
75
|
+
follow_imports = "skip"
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
testpaths = ["tests"]
|
|
79
|
+
|
|
80
|
+
[tool.coverage.run]
|
|
81
|
+
source = ["testpick"]
|
|
82
|
+
branch = true
|
|
83
|
+
|
|
84
|
+
[tool.coverage.report]
|
|
85
|
+
show_missing = true
|
|
86
|
+
skip_covered = true
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""testpick - Select which tests to run based on code changes."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("testpick")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|