smartselect 0.1.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.
- smartselect-0.1.3/.github/workflows/ci.yml +48 -0
- smartselect-0.1.3/.github/workflows/publish.yml +28 -0
- smartselect-0.1.3/.gitignore +35 -0
- smartselect-0.1.3/.testwise.example.yml +54 -0
- smartselect-0.1.3/CLAUDE.md +75 -0
- smartselect-0.1.3/CONTRIBUTING.md +130 -0
- smartselect-0.1.3/LICENSE +21 -0
- smartselect-0.1.3/PKG-INFO +269 -0
- smartselect-0.1.3/README.md +234 -0
- smartselect-0.1.3/action.yml +82 -0
- smartselect-0.1.3/pyproject.toml +72 -0
- smartselect-0.1.3/src/testwise/__init__.py +3 -0
- smartselect-0.1.3/src/testwise/cli.py +204 -0
- smartselect-0.1.3/src/testwise/config.py +135 -0
- smartselect-0.1.3/src/testwise/context_builder.py +185 -0
- smartselect-0.1.3/src/testwise/diff_analyzer.py +273 -0
- smartselect-0.1.3/src/testwise/exceptions.py +33 -0
- smartselect-0.1.3/src/testwise/llm_selector.py +239 -0
- smartselect-0.1.3/src/testwise/models.py +190 -0
- smartselect-0.1.3/src/testwise/parsers/__init__.py +78 -0
- smartselect-0.1.3/src/testwise/parsers/generic_parser.py +81 -0
- smartselect-0.1.3/src/testwise/parsers/pytest_parser.py +204 -0
- smartselect-0.1.3/src/testwise/reporter.py +200 -0
- smartselect-0.1.3/src/testwise/test_discovery.py +165 -0
- smartselect-0.1.3/src/testwise/test_runner.py +188 -0
- smartselect-0.1.3/tests/__init__.py +0 -0
- smartselect-0.1.3/tests/test_models.py +16 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: ruff check src/ tests/
|
|
29
|
+
|
|
30
|
+
- name: Test
|
|
31
|
+
run: pytest --cov=testwise --cov-report=term-missing
|
|
32
|
+
|
|
33
|
+
lint:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.12"
|
|
41
|
+
|
|
42
|
+
- run: pip install -e ".[dev]"
|
|
43
|
+
|
|
44
|
+
- name: Ruff format check
|
|
45
|
+
run: ruff format --check src/ tests/
|
|
46
|
+
|
|
47
|
+
- name: Type check
|
|
48
|
+
run: mypy src/testwise/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install build tools
|
|
22
|
+
run: pip install build
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.idea/
|
|
15
|
+
.vscode/
|
|
16
|
+
*.swp
|
|
17
|
+
*.swo
|
|
18
|
+
|
|
19
|
+
# OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
|
|
23
|
+
# Testing
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
|
|
28
|
+
# mypy
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
|
|
31
|
+
# ruff
|
|
32
|
+
.ruff_cache/
|
|
33
|
+
|
|
34
|
+
# Environment
|
|
35
|
+
.env
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Testwise Configuration
|
|
2
|
+
# Copy this file to .testwise.yml in your repo root
|
|
3
|
+
|
|
4
|
+
runners:
|
|
5
|
+
# Python/pytest example
|
|
6
|
+
- name: pytest
|
|
7
|
+
command: pytest
|
|
8
|
+
args: ["-v", "--tb=short"]
|
|
9
|
+
test_patterns: ["tests/**/*.py", "test_*.py", "*_test.py"]
|
|
10
|
+
parser: pytest # Use the built-in pytest parser for test-level selection
|
|
11
|
+
select_mode: test # "test" for individual test functions, "file" for file-level
|
|
12
|
+
timeout_seconds: 300
|
|
13
|
+
|
|
14
|
+
# JavaScript/Jest example (uncomment to use)
|
|
15
|
+
# - name: jest
|
|
16
|
+
# command: npx jest
|
|
17
|
+
# args: ["--verbose"]
|
|
18
|
+
# test_patterns: ["**/*.test.ts", "**/*.test.js", "**/*.spec.ts", "**/*.spec.js"]
|
|
19
|
+
# parser: generic # No built-in JS parser yet (file-level selection)
|
|
20
|
+
# select_mode: file
|
|
21
|
+
# file_arg_style: flag
|
|
22
|
+
# file_arg_flag: "--testPathPattern"
|
|
23
|
+
# timeout_seconds: 300
|
|
24
|
+
|
|
25
|
+
# Go example (uncomment to use)
|
|
26
|
+
# - name: gotest
|
|
27
|
+
# command: go
|
|
28
|
+
# args: ["test", "-v"]
|
|
29
|
+
# test_patterns: ["**/*_test.go"]
|
|
30
|
+
# parser: generic
|
|
31
|
+
# select_mode: file
|
|
32
|
+
# file_arg_style: none
|
|
33
|
+
# timeout_seconds: 300
|
|
34
|
+
|
|
35
|
+
# LLM configuration
|
|
36
|
+
llm:
|
|
37
|
+
model: anthropic/claude-sonnet-4-20250514 # Any model supported by litellm
|
|
38
|
+
api_key_env: ANTHROPIC_API_KEY # Environment variable containing the API key
|
|
39
|
+
max_context_tokens: 100000 # Token budget for context
|
|
40
|
+
temperature: 0.0 # Keep deterministic
|
|
41
|
+
timeout_seconds: 60
|
|
42
|
+
|
|
43
|
+
# Context assembly
|
|
44
|
+
context:
|
|
45
|
+
include_test_contents: true # Send test file contents to LLM (more accurate, more tokens)
|
|
46
|
+
max_diff_lines: 5000 # Truncate diffs larger than this
|
|
47
|
+
|
|
48
|
+
# Safety
|
|
49
|
+
fallback_on_error: true # Run all tests if LLM fails
|
|
50
|
+
run_should_run: true # Also run "should_run" tests (not just "must_run")
|
|
51
|
+
|
|
52
|
+
# Optional: filter which files are analyzed
|
|
53
|
+
# include_patterns: ["src/**", "lib/**"]
|
|
54
|
+
# exclude_patterns: ["**/*.md", "docs/**"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What is Testwise
|
|
6
|
+
|
|
7
|
+
Testwise is an LLM-powered test selection tool for CI/CD pipelines. It analyzes git diffs, uses an LLM (via litellm) to classify tests as `must_run`, `should_run`, or `skip`, then executes only the relevant tests. It ships as both a CLI (`testwise`) and a GitHub Actions composite action.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install in dev mode
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
|
|
15
|
+
# Run all tests
|
|
16
|
+
pytest
|
|
17
|
+
|
|
18
|
+
# Run tests with coverage
|
|
19
|
+
pytest --cov=testwise --cov-report=term-missing
|
|
20
|
+
|
|
21
|
+
# Run a single test file
|
|
22
|
+
pytest tests/test_parsers/test_pytest_parser.py
|
|
23
|
+
|
|
24
|
+
# Run a single test
|
|
25
|
+
pytest tests/test_parsers/test_pytest_parser.py::test_function_name
|
|
26
|
+
|
|
27
|
+
# Linting
|
|
28
|
+
ruff check src/ tests/
|
|
29
|
+
ruff format src/ tests/
|
|
30
|
+
|
|
31
|
+
# Type checking
|
|
32
|
+
mypy src/testwise/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
The pipeline flows through these stages in order, orchestrated by `cli.py`:
|
|
38
|
+
|
|
39
|
+
1. **Config** (`config.py`) — Loads `.testwise.yml`, resolves defaults via `TestwiseConfig` Pydantic model
|
|
40
|
+
2. **Diff** (`diff_analyzer.py`) — Extracts git diff between base/head refs, filters and truncates
|
|
41
|
+
3. **Discovery** (`test_discovery.py`) — Finds test files matching runner glob patterns
|
|
42
|
+
4. **Parsing** (`test_discovery.py` → `parsers/`) — Delegates to parser plugins to extract individual test functions with metadata
|
|
43
|
+
5. **Context** (`context_builder.py`) — Assembles diff + parsed tests into LLM messages, respecting token budget
|
|
44
|
+
6. **LLM Selection** (`llm_selector.py`) — Calls LLM via litellm with structured output (falls back to text mode JSON extraction). Returns `LLMSelectionResponse` with per-test classifications
|
|
45
|
+
7. **Execution** (`test_runner.py`) — Runs selected tests using parser's `build_run_command()`
|
|
46
|
+
8. **Reporting** (`reporter.py`) — Outputs results as text, JSON, or GitHub Actions annotations
|
|
47
|
+
|
|
48
|
+
### Parser Plugin System
|
|
49
|
+
|
|
50
|
+
Parsers are registered via Python entry points (`testwise.parsers` group) and loaded at runtime by `parsers/__init__.py`. Each parser extends `BaseParser` ABC and implements:
|
|
51
|
+
- `parse_test_file()` — extracts `ParsedTest` objects with names, tags, `@covers` annotations, imports
|
|
52
|
+
- `build_run_command()` — builds the CLI command to run specific tests
|
|
53
|
+
|
|
54
|
+
Built-in parsers: `pytest` (test-level granularity) and `generic` (file-level fallback).
|
|
55
|
+
|
|
56
|
+
### Key Models (`models.py`)
|
|
57
|
+
|
|
58
|
+
All data flows through Pydantic models: `DiffResult` → `ParsedTestFile`/`ParsedTest` → `LLMSelectionResponse`/`TestSelection` → `TestResult` → `RunReport`. The `TestClassification` enum (`must_run`, `should_run`, `skip`) is central to the selection logic.
|
|
59
|
+
|
|
60
|
+
### LLM Fallback Strategy
|
|
61
|
+
|
|
62
|
+
`llm_selector.py` has a three-tier approach: (1) structured JSON output via `response_format`, (2) text mode with JSON schema in prompt, (3) raise `LLMError`. The caller in `cli.py` catches `LLMError` and falls back to running all tests when `fallback_on_error: true`.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
- Config file: `.testwise.yml` (see `.testwise.example.yml` for all options)
|
|
67
|
+
- Uses litellm model format (e.g., `anthropic/claude-sonnet-4-20250514`, `openai/gpt-4o`)
|
|
68
|
+
- API key is read from the env var specified by `llm.api_key_env`
|
|
69
|
+
|
|
70
|
+
## Project Setup
|
|
71
|
+
|
|
72
|
+
- Python >=3.10, build system: hatchling
|
|
73
|
+
- Ruff config: line-length 100, target py310, rules: E, F, I, N, W, UP
|
|
74
|
+
- mypy: strict mode
|
|
75
|
+
- Tests directory: `tests/` with `fixtures/` and `test_parsers/` subdirectories
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Contributing to Testwise
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing! Here's how to get started.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/mattfrautnick/testwise.git
|
|
9
|
+
cd testwise
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Running Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pytest
|
|
17
|
+
pytest --cov=testwise --cov-report=term-missing
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Linting
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ruff check src/ tests/
|
|
24
|
+
ruff format src/ tests/
|
|
25
|
+
mypy src/testwise/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Project Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/testwise/
|
|
32
|
+
cli.py # CLI entry point + orchestration
|
|
33
|
+
models.py # All Pydantic data models
|
|
34
|
+
config.py # Configuration loading
|
|
35
|
+
diff_analyzer.py # Git diff extraction
|
|
36
|
+
test_discovery.py # Test file discovery
|
|
37
|
+
context_builder.py # LLM context assembly
|
|
38
|
+
llm_selector.py # LLM interaction
|
|
39
|
+
test_runner.py # Test execution
|
|
40
|
+
reporter.py # Results formatting
|
|
41
|
+
parsers/ # Parser plugin system
|
|
42
|
+
__init__.py # BaseParser ABC + registry
|
|
43
|
+
pytest_parser.py # Python/pytest parser
|
|
44
|
+
generic_parser.py # File-level fallback
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Writing a Parser Plugin
|
|
48
|
+
|
|
49
|
+
Parser plugins enable test-level selection for new languages/frameworks.
|
|
50
|
+
|
|
51
|
+
### 1. Create the Parser
|
|
52
|
+
|
|
53
|
+
Implement `BaseParser`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from testwise.parsers import BaseParser
|
|
57
|
+
from testwise.models import ParsedTest, ParsedTestFile, RunnerConfig
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
class MyParser(BaseParser):
|
|
61
|
+
name = "myframework"
|
|
62
|
+
languages = ["javascript"]
|
|
63
|
+
file_patterns = ["*.test.js"]
|
|
64
|
+
|
|
65
|
+
def parse_test_file(self, file_path: Path, content: str) -> ParsedTestFile:
|
|
66
|
+
"""Parse a test file and extract individual tests."""
|
|
67
|
+
tests = []
|
|
68
|
+
# Your parsing logic here - extract test names, tags, etc.
|
|
69
|
+
return ParsedTestFile(
|
|
70
|
+
file_path=str(file_path),
|
|
71
|
+
language="javascript",
|
|
72
|
+
tests=tests,
|
|
73
|
+
imports=[], # Module imports for dependency mapping
|
|
74
|
+
fixtures_used=[], # Shared setup/fixtures
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def build_run_command(
|
|
78
|
+
self,
|
|
79
|
+
tests: list[ParsedTest],
|
|
80
|
+
runner_config: RunnerConfig,
|
|
81
|
+
repo_root: Path,
|
|
82
|
+
) -> list[str]:
|
|
83
|
+
"""Build CLI command to run specific tests."""
|
|
84
|
+
cmd = [runner_config.command, *runner_config.args]
|
|
85
|
+
# Add test selection flags specific to your framework
|
|
86
|
+
return cmd
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Register via Entry Points
|
|
90
|
+
|
|
91
|
+
In your package's `pyproject.toml`:
|
|
92
|
+
|
|
93
|
+
```toml
|
|
94
|
+
[project.entry-points."testwise.parsers"]
|
|
95
|
+
myframework = "my_package.parser:MyParser"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Test Your Parser
|
|
99
|
+
|
|
100
|
+
Write tests that verify:
|
|
101
|
+
- Test functions are correctly extracted from sample files
|
|
102
|
+
- Tags/annotations are properly parsed
|
|
103
|
+
- The `build_run_command` produces valid CLI commands
|
|
104
|
+
- Edge cases: empty files, syntax errors, nested classes
|
|
105
|
+
|
|
106
|
+
### What Makes a Good Parser
|
|
107
|
+
|
|
108
|
+
- **Extract test names** with qualified paths (file::class::test)
|
|
109
|
+
- **Parse annotations/decorators** into tags and covers lists
|
|
110
|
+
- **Detect parametrized tests** (the LLM uses this info)
|
|
111
|
+
- **Extract imports** (helps the LLM map code dependencies)
|
|
112
|
+
- **Handle errors gracefully** (syntax errors should fall back to file-level)
|
|
113
|
+
|
|
114
|
+
## Pull Request Guidelines
|
|
115
|
+
|
|
116
|
+
1. Fork and create a feature branch
|
|
117
|
+
2. Write tests for new functionality
|
|
118
|
+
3. Ensure all tests pass: `pytest`
|
|
119
|
+
4. Ensure linting passes: `ruff check src/ tests/`
|
|
120
|
+
5. Keep commits focused and well-described
|
|
121
|
+
6. Open a PR with a clear description of what and why
|
|
122
|
+
|
|
123
|
+
## Reporting Issues
|
|
124
|
+
|
|
125
|
+
Open an issue with:
|
|
126
|
+
- What you expected to happen
|
|
127
|
+
- What actually happened
|
|
128
|
+
- Steps to reproduce
|
|
129
|
+
- Your testwise version (`testwise --version`)
|
|
130
|
+
- Your config file (redact API keys)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Frautnick
|
|
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.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartselect
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: LLM-powered test selection for CI/CD pipelines
|
|
5
|
+
Project-URL: Homepage, https://github.com/mattfrautnick/testwise
|
|
6
|
+
Project-URL: Repository, https://github.com/mattfrautnick/testwise
|
|
7
|
+
Project-URL: Issues, https://github.com/mattfrautnick/testwise/issues
|
|
8
|
+
Author: Matthew Frautnick
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cd,ci,llm,test-selection,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: click>=8.0
|
|
23
|
+
Requires-Dist: litellm>=1.40.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: tiktoken>=0.7.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
33
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<h1 align="center">Testwise</h1>
|
|
38
|
+
<p align="center">
|
|
39
|
+
LLM-powered test selection for CI/CD pipelines
|
|
40
|
+
<br />
|
|
41
|
+
<em>Run only the tests that matter. Save CI time without sacrificing coverage.</em>
|
|
42
|
+
</p>
|
|
43
|
+
<p align="center">
|
|
44
|
+
<a href="https://github.com/mattfrautnick/testwise/actions/workflows/ci.yml"><img src="https://github.com/mattfrautnick/testwise/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
45
|
+
<a href="https://pypi.org/project/testwise/"><img src="https://img.shields.io/pypi/v/testwise.svg" alt="PyPI"></a>
|
|
46
|
+
<a href="https://pypi.org/project/testwise/"><img src="https://img.shields.io/pypi/pyversions/testwise.svg" alt="Python"></a>
|
|
47
|
+
<a href="https://github.com/mattfrautnick/testwise/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mattfrautnick/testwise" alt="License"></a>
|
|
48
|
+
</p>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
Testwise analyzes your git diff and uses an LLM to classify every test as `must_run`, `should_run`, or `skip` — then executes only what's needed. It supports **test-level granularity** for languages with parser plugins and falls back to file-level selection for everything else.
|
|
54
|
+
|
|
55
|
+
## Why Testwise?
|
|
56
|
+
|
|
57
|
+
Large test suites slow down CI. Most changes only affect a fraction of your tests, but running the full suite every time wastes minutes (or hours). Existing static-analysis approaches miss indirect dependencies and cross-cutting concerns. Testwise uses an LLM that actually understands your code changes and test structure to make smarter decisions — with a safe fallback to run everything if it's ever uncertain.
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
git diff ─> Discover Tests ─> Parse with Plugins ─> LLM Classifies ─> Run Selected ─> Report
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
1. **Diff Analysis** — Extracts the git diff between base and head refs
|
|
66
|
+
2. **Test Discovery** — Finds all test files and parses individual test functions via parser plugins
|
|
67
|
+
3. **LLM Classification** — Sends diff + test inventory to an LLM with structured output
|
|
68
|
+
4. **Selective Execution** — Runs only selected tests and reports results with GitHub annotations
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- **Hybrid Granularity** — Test-level selection for languages with parser plugins (pytest built-in), file-level fallback for others
|
|
73
|
+
- **Plugin Architecture** — Extensible parser system via Python entry points. [Write a parser](#writing-a-parser-plugin) for any test framework.
|
|
74
|
+
- **Any LLM Provider** — Uses [litellm](https://github.com/BerriAI/litellm) to support Claude, GPT, Gemini, and 100+ other models
|
|
75
|
+
- **GitHub Actions** — Ships as a composite action with step summary, annotations, and outputs
|
|
76
|
+
- **Safe Fallback** — If the LLM fails or is uncertain, falls back to running all tests
|
|
77
|
+
- **Test Annotations** — Supports `@pytest.mark.covers()` to explicitly map tests to code areas
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
### Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install smartselect
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Configure
|
|
88
|
+
|
|
89
|
+
Create `.testwise.yml` in your repo root:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
runners:
|
|
93
|
+
- name: pytest
|
|
94
|
+
command: pytest
|
|
95
|
+
args: ["-v", "--tb=short"]
|
|
96
|
+
test_patterns: ["tests/**/*.py", "test_*.py"]
|
|
97
|
+
parser: pytest
|
|
98
|
+
select_mode: test
|
|
99
|
+
|
|
100
|
+
llm:
|
|
101
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
102
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Run
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Dry run — see what the LLM would select
|
|
109
|
+
testwise --dry-run
|
|
110
|
+
|
|
111
|
+
# Run selected tests
|
|
112
|
+
testwise
|
|
113
|
+
|
|
114
|
+
# Force all tests (bypass LLM)
|
|
115
|
+
testwise --fallback
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### GitHub Actions
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
jobs:
|
|
122
|
+
test:
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
steps:
|
|
125
|
+
- uses: actions/checkout@v4
|
|
126
|
+
with:
|
|
127
|
+
fetch-depth: 0 # Full history needed for diff
|
|
128
|
+
|
|
129
|
+
- uses: mattfrautnick/testwise@v1
|
|
130
|
+
with:
|
|
131
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
132
|
+
run-level: should_run
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The action writes a Markdown summary to `$GITHUB_STEP_SUMMARY` and emits `::error::` annotations for failing tests inline in your PR diff.
|
|
136
|
+
|
|
137
|
+
## Test Annotations
|
|
138
|
+
|
|
139
|
+
Testwise's pytest parser understands standard markers and a custom `@covers` annotation that explicitly maps tests to code areas:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import pytest
|
|
143
|
+
|
|
144
|
+
@pytest.mark.covers("auth_module", "user.login")
|
|
145
|
+
def test_login_success(client, db):
|
|
146
|
+
"""Verify successful login flow."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
@pytest.mark.integration
|
|
150
|
+
@pytest.mark.covers("payment_service")
|
|
151
|
+
def test_checkout_flow(client):
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@pytest.mark.parametrize("role", ["admin", "user", "guest"])
|
|
155
|
+
def test_permissions(role):
|
|
156
|
+
...
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The parser also extracts imports and fixture references automatically — no annotation required for basic dependency mapping.
|
|
160
|
+
|
|
161
|
+
## Parser Plugins
|
|
162
|
+
|
|
163
|
+
Testwise uses a plugin architecture for language-specific test parsing. Plugins are registered via Python entry points.
|
|
164
|
+
|
|
165
|
+
### Built-in Parsers
|
|
166
|
+
|
|
167
|
+
| Parser | Language | Granularity | Features |
|
|
168
|
+
|--------|----------|-------------|----------|
|
|
169
|
+
| `pytest` | Python | Test-level | Markers, covers, parametrize, fixtures, imports |
|
|
170
|
+
| `generic` | Any | File-level | Fallback for unsupported languages |
|
|
171
|
+
|
|
172
|
+
### Writing a Parser Plugin
|
|
173
|
+
|
|
174
|
+
Implement `BaseParser` and register it as an entry point:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from testwise.parsers import BaseParser
|
|
178
|
+
from testwise.models import ParsedTest, ParsedTestFile, RunnerConfig
|
|
179
|
+
from pathlib import Path
|
|
180
|
+
|
|
181
|
+
class JestParser(BaseParser):
|
|
182
|
+
name = "jest"
|
|
183
|
+
languages = ["javascript", "typescript"]
|
|
184
|
+
file_patterns = ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"]
|
|
185
|
+
|
|
186
|
+
def parse_test_file(self, file_path: Path, content: str) -> ParsedTestFile:
|
|
187
|
+
# Parse describe/it blocks, extract test names
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
def build_run_command(self, tests, runner_config, repo_root):
|
|
191
|
+
# Build jest --testNamePattern command
|
|
192
|
+
...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
# pyproject.toml
|
|
197
|
+
[project.entry-points."testwise.parsers"]
|
|
198
|
+
jest = "my_package.jest_parser:JestParser"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for a full guide on writing and testing parser plugins.
|
|
202
|
+
|
|
203
|
+
## CLI Reference
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
testwise [OPTIONS]
|
|
207
|
+
|
|
208
|
+
Options:
|
|
209
|
+
-c, --config PATH Path to .testwise.yml
|
|
210
|
+
-b, --base-ref TEXT Base git ref to diff against
|
|
211
|
+
--head-ref TEXT Head git ref (default: HEAD)
|
|
212
|
+
-o, --output [text|json|github] Output format (default: text)
|
|
213
|
+
--output-file PATH Write JSON report to file
|
|
214
|
+
--dry-run Show selections without running tests
|
|
215
|
+
--fallback Skip LLM, run all tests
|
|
216
|
+
--run-level [must_run|should_run|all] Minimum classification to run
|
|
217
|
+
-v, --verbose Verbose logging
|
|
218
|
+
--version Show version
|
|
219
|
+
--help Show this message
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Configuration Reference
|
|
223
|
+
|
|
224
|
+
See [`.testwise.example.yml`](.testwise.example.yml) for a fully commented example.
|
|
225
|
+
|
|
226
|
+
| Key | Type | Default | Description |
|
|
227
|
+
|-----|------|---------|-------------|
|
|
228
|
+
| `runners[].name` | string | required | Runner identifier |
|
|
229
|
+
| `runners[].command` | string | required | Test runner command |
|
|
230
|
+
| `runners[].args` | list | `[]` | Additional arguments |
|
|
231
|
+
| `runners[].test_patterns` | list | `[]` | Glob patterns for test files |
|
|
232
|
+
| `runners[].parser` | string | `"generic"` | Parser plugin name |
|
|
233
|
+
| `runners[].select_mode` | string | `"file"` | `"test"` or `"file"` |
|
|
234
|
+
| `runners[].timeout_seconds` | int | `300` | Per-runner timeout |
|
|
235
|
+
| `llm.model` | string | `"anthropic/claude-sonnet-4-20250514"` | LLM model ([litellm format](https://docs.litellm.ai/docs/providers)) |
|
|
236
|
+
| `llm.api_key_env` | string | `"ANTHROPIC_API_KEY"` | Env var containing API key |
|
|
237
|
+
| `llm.max_context_tokens` | int | `100000` | Token budget for context |
|
|
238
|
+
| `llm.temperature` | float | `0.0` | LLM temperature |
|
|
239
|
+
| `fallback_on_error` | bool | `true` | Run all tests if LLM fails |
|
|
240
|
+
| `run_should_run` | bool | `true` | Also run "should_run" tests |
|
|
241
|
+
|
|
242
|
+
## Roadmap
|
|
243
|
+
|
|
244
|
+
Testwise is in early development. Here's what's planned:
|
|
245
|
+
|
|
246
|
+
- [ ] Jest/Vitest parser plugin
|
|
247
|
+
- [ ] Go test parser plugin
|
|
248
|
+
- [ ] Caching layer — skip LLM call for identical diffs
|
|
249
|
+
- [ ] Cost tracking — log token usage and estimated cost per run
|
|
250
|
+
- [ ] Confidence threshold — auto-fallback below a configurable confidence
|
|
251
|
+
- [ ] Test impact analysis — learn from historical runs which tests fail for which changes
|
|
252
|
+
- [ ] GitLab CI integration
|
|
253
|
+
|
|
254
|
+
Have an idea? [Open an issue](https://github.com/mattfrautnick/testwise/issues) or [start a discussion](https://github.com/mattfrautnick/testwise/discussions).
|
|
255
|
+
|
|
256
|
+
## Contributing
|
|
257
|
+
|
|
258
|
+
Contributions are welcome! Whether it's a bug fix, a new parser plugin, or documentation improvements — all contributions help.
|
|
259
|
+
|
|
260
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, architecture overview, and the full guide to writing parser plugins.
|
|
261
|
+
|
|
262
|
+
## Community
|
|
263
|
+
|
|
264
|
+
- [GitHub Issues](https://github.com/mattfrautnick/testwise/issues) — Bug reports and feature requests
|
|
265
|
+
- [GitHub Discussions](https://github.com/mattfrautnick/testwise/discussions) — Questions, ideas, and show & tell
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
[MIT](LICENSE)
|