flarebisect 0.3.1__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.
- flarebisect-0.3.1/.gitignore +12 -0
- flarebisect-0.3.1/LICENSE +21 -0
- flarebisect-0.3.1/PKG-INFO +192 -0
- flarebisect-0.3.1/README.md +157 -0
- flarebisect-0.3.1/pyproject.toml +68 -0
- flarebisect-0.3.1/src/flarebisect/__init__.py +1 -0
- flarebisect-0.3.1/src/flarebisect/ai_explain.py +37 -0
- flarebisect-0.3.1/src/flarebisect/bisect.py +145 -0
- flarebisect-0.3.1/src/flarebisect/cli.py +309 -0
- flarebisect-0.3.1/src/flarebisect/config.py +89 -0
- flarebisect-0.3.1/src/flarebisect/git_ops.py +70 -0
- flarebisect-0.3.1/src/flarebisect/hardware.py +141 -0
- flarebisect-0.3.1/src/flarebisect/providers.py +109 -0
- flarebisect-0.3.1/src/flarebisect/py.typed +0 -0
- flarebisect-0.3.1/src/flarebisect/report.py +117 -0
- flarebisect-0.3.1/src/flarebisect/runner.py +45 -0
- flarebisect-0.3.1/tests/test_bisect.py +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tawin Tangsukson
|
|
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,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flarebisect
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: git bisect that treats flakiness as a signal, not noise
|
|
5
|
+
Project-URL: Homepage, https://github.com/kaorii-ako/FlareBisect
|
|
6
|
+
Project-URL: Repository, https://github.com/kaorii-ako/FlareBisect
|
|
7
|
+
Project-URL: Issues, https://github.com/kaorii-ako/FlareBisect/issues
|
|
8
|
+
Author: Tawin Tangsukson
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bisect,cli,debugging,flaky-tests,git,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: anthropic>=0.40
|
|
27
|
+
Requires-Dist: openai>=1.40
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# FlareBisect ๐ฅ
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/flarebisect/)
|
|
39
|
+
[](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
FlareBisect finds the commit that made your tests flaky, not just the one
|
|
43
|
+
that broke them: it bisects on flake-rate drift instead of pass/fail, then
|
|
44
|
+
flares the culprit with an AI-written root cause.
|
|
45
|
+
|
|
46
|
+
`git bisect` gives you confidently wrong answers when the test you're
|
|
47
|
+
bisecting on is flaky โ it treats every run as a clean pass/fail signal, so
|
|
48
|
+
a test that was already unstable *before* you started bisecting can point
|
|
49
|
+
straight at an innocent commit.
|
|
50
|
+
|
|
51
|
+
`flarebisect` bisects on **flake rate**, not pass/fail. It runs each
|
|
52
|
+
candidate commit's test N times in parallel, computes a failure rate, and
|
|
53
|
+
finds the commit where that rate jumps โ then asks an LLM to explain, in
|
|
54
|
+
plain English, whether the commit broke the test cleanly or made it flakier.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
12 commits in range ยท 20 runs per commit ยท parallel worktrees
|
|
58
|
+
|
|
59
|
+
commit flake rate result status
|
|
60
|
+
a1b2c3 5/5 good
|
|
61
|
+
4f9e21 5/5 stable
|
|
62
|
+
7d3aa0 โโโโโโโ 4/5 wobbling
|
|
63
|
+
9c1f88 โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 1/5 flare
|
|
64
|
+
HEAD โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 1/5 bad
|
|
65
|
+
|
|
66
|
+
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
|
|
67
|
+
โ โฆ culprit found ยท commit 9c1f88 โโโโโ โ
|
|
68
|
+
โ โโโโโโโโโ โ
|
|
69
|
+
โ flake rate jumped 20% โ 80% at this commit โโโโโโโโโ โ
|
|
70
|
+
โ "add request counter for rate limiting" โโโโโโโโโ โ
|
|
71
|
+
โ โโโโโโโโโ โ
|
|
72
|
+
โ ๐ก likely cause โ shared counter incremented without a lock.โโโโโโโโโ
|
|
73
|
+
โ concurrent test workers race on the same variable. โโโโโโโโโ โ
|
|
74
|
+
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
|
|
75
|
+
โฑ 8.4s โก 5 checked โ 4 parallel workers
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install flarebisect
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For local development:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e ".[dev]"
|
|
88
|
+
pytest -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Works on Linux, macOS, and Windows โ the only external dependency is `git`
|
|
92
|
+
on your `PATH`. Every candidate commit runs in its own `git worktree` under a
|
|
93
|
+
temp directory; your actual working tree and index are never touched.
|
|
94
|
+
|
|
95
|
+
## AI provider setup
|
|
96
|
+
|
|
97
|
+
The root-cause explanation step works with Claude, OpenAI, Gemini, or a
|
|
98
|
+
local model โ anything speaking an OpenAI-compatible chat API (Ollama, LM
|
|
99
|
+
Studio, llama.cpp server, vLLM, and similar all qualify).
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# cloud
|
|
103
|
+
flarebisect config set-key anthropic sk-ant-...
|
|
104
|
+
flarebisect config set-key openai sk-...
|
|
105
|
+
flarebisect config set-key google AI...
|
|
106
|
+
|
|
107
|
+
# local, no key needed - just have Ollama running
|
|
108
|
+
flarebisect config use ollama
|
|
109
|
+
flarebisect config set-model ollama llama3.1
|
|
110
|
+
|
|
111
|
+
# or let flarebisect pick a model sized to your GPU and pull it for you
|
|
112
|
+
flarebisect models detect # shows detected GPU/VRAM + recommended model
|
|
113
|
+
flarebisect models pull # downloads it and sets it active (run with no args)
|
|
114
|
+
|
|
115
|
+
# any other OpenAI-compatible endpoint
|
|
116
|
+
flarebisect config use custom
|
|
117
|
+
flarebisect config set-base-url custom http://localhost:8080/v1
|
|
118
|
+
flarebisect config set-key custom sk-local
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run `flarebisect config` with no arguments for a guided setup wizard โ for
|
|
122
|
+
Ollama it detects your GPU/VRAM, prefills a right-sized model, and offers to
|
|
123
|
+
pull it on the spot.
|
|
124
|
+
|
|
125
|
+
`flarebisect config show` lists the active provider and stored settings
|
|
126
|
+
(keys are masked). Config lives in a JSON file under the OS config dir
|
|
127
|
+
(`~/.config/flarebisect/config.json` on Linux/macOS, `%APPDATA%` on Windows),
|
|
128
|
+
written with owner-only permissions.
|
|
129
|
+
|
|
130
|
+
Per-run overrides skip the config file entirely:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
flarebisect run ... --provider openai --model gpt-4o-mini --api-key sk-...
|
|
134
|
+
flarebisect run ... --provider ollama --base-url http://localhost:11434/v1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`) work too,
|
|
138
|
+
as a fallback under whatever's in the config file.
|
|
139
|
+
|
|
140
|
+
## Usage
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
flarebisect run \
|
|
144
|
+
--repo /path/to/repo \
|
|
145
|
+
--good <known-good-sha> \
|
|
146
|
+
--bad <known-bad-sha> \
|
|
147
|
+
--test "pytest -k my_flaky_test" \
|
|
148
|
+
--runs 20 \
|
|
149
|
+
--threshold 0.3
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `--runs` โ test executions per commit (default 20), run in parallel
|
|
153
|
+
(capped at your core count so it doesn't oversubscribe). Lower counts are
|
|
154
|
+
faster but noisier โ a low sample can misattribute the culprit near the
|
|
155
|
+
threshold boundary.
|
|
156
|
+
- `--threshold` โ flake-rate jump (0-1) vs. the `good` baseline required to
|
|
157
|
+
call a commit the culprit (default 0.3).
|
|
158
|
+
- `--no-explain` โ skip the LLM root-cause call entirely (fully offline).
|
|
159
|
+
|
|
160
|
+
## Demo
|
|
161
|
+
|
|
162
|
+
See [`demo/`](demo/) for a self-contained, seeded-flaky-bug repo you can
|
|
163
|
+
bisect against with no network access required for the bisection itself.
|
|
164
|
+
|
|
165
|
+
## How it works
|
|
166
|
+
|
|
167
|
+
1. Measure the flake rate at `good` and `bad` as baselines.
|
|
168
|
+
2. Binary-search the commit range between them.
|
|
169
|
+
3. At each candidate, run the test `N` times in an isolated worktree and
|
|
170
|
+
compute `failed / N`.
|
|
171
|
+
4. The culprit is the first commit whose flake rate jumps by `>= threshold`
|
|
172
|
+
over the `good` baseline.
|
|
173
|
+
5. Verdict: **clean break** (rate goes ~0% โ ~100%) vs. **flakiness
|
|
174
|
+
regression** (rate goes ~0% โ somewhere in between).
|
|
175
|
+
6. The configured LLM gets the culprit's diff + the flake-rate evidence and
|
|
176
|
+
returns a short root-cause read (race condition, shared mutable state,
|
|
177
|
+
timing assumption, etc.).
|
|
178
|
+
|
|
179
|
+
Binary search assumes the flake rate is roughly monotonic across the range,
|
|
180
|
+
same as ordinary `git bisect` assumes pass/fail is. At low `--runs` counts
|
|
181
|
+
a noisy sample can occasionally violate that near the threshold boundary โ
|
|
182
|
+
bump `--runs` if a result looks off.
|
|
183
|
+
|
|
184
|
+
## Releasing
|
|
185
|
+
|
|
186
|
+
Version is single-sourced from `src/flarebisect/__init__.py`. Bump it, commit,
|
|
187
|
+
tag (`vX.Y.Z`), and cut a GitHub Release โ `.github/workflows/publish.yml`
|
|
188
|
+
builds and publishes to PyPI via trusted publishing (OIDC, no stored token).
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# FlareBisect ๐ฅ
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/flarebisect/)
|
|
4
|
+
[](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
FlareBisect finds the commit that made your tests flaky, not just the one
|
|
8
|
+
that broke them: it bisects on flake-rate drift instead of pass/fail, then
|
|
9
|
+
flares the culprit with an AI-written root cause.
|
|
10
|
+
|
|
11
|
+
`git bisect` gives you confidently wrong answers when the test you're
|
|
12
|
+
bisecting on is flaky โ it treats every run as a clean pass/fail signal, so
|
|
13
|
+
a test that was already unstable *before* you started bisecting can point
|
|
14
|
+
straight at an innocent commit.
|
|
15
|
+
|
|
16
|
+
`flarebisect` bisects on **flake rate**, not pass/fail. It runs each
|
|
17
|
+
candidate commit's test N times in parallel, computes a failure rate, and
|
|
18
|
+
finds the commit where that rate jumps โ then asks an LLM to explain, in
|
|
19
|
+
plain English, whether the commit broke the test cleanly or made it flakier.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
12 commits in range ยท 20 runs per commit ยท parallel worktrees
|
|
23
|
+
|
|
24
|
+
commit flake rate result status
|
|
25
|
+
a1b2c3 5/5 good
|
|
26
|
+
4f9e21 5/5 stable
|
|
27
|
+
7d3aa0 โโโโโโโ 4/5 wobbling
|
|
28
|
+
9c1f88 โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 1/5 flare
|
|
29
|
+
HEAD โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 1/5 bad
|
|
30
|
+
|
|
31
|
+
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
|
|
32
|
+
โ โฆ culprit found ยท commit 9c1f88 โโโโโ โ
|
|
33
|
+
โ โโโโโโโโโ โ
|
|
34
|
+
โ flake rate jumped 20% โ 80% at this commit โโโโโโโโโ โ
|
|
35
|
+
โ "add request counter for rate limiting" โโโโโโโโโ โ
|
|
36
|
+
โ โโโโโโโโโ โ
|
|
37
|
+
โ ๐ก likely cause โ shared counter incremented without a lock.โโโโโโโโโ
|
|
38
|
+
โ concurrent test workers race on the same variable. โโโโโโโโโ โ
|
|
39
|
+
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
|
|
40
|
+
โฑ 8.4s โก 5 checked โ 4 parallel workers
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install flarebisect
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For local development:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e ".[dev]"
|
|
53
|
+
pytest -q
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Works on Linux, macOS, and Windows โ the only external dependency is `git`
|
|
57
|
+
on your `PATH`. Every candidate commit runs in its own `git worktree` under a
|
|
58
|
+
temp directory; your actual working tree and index are never touched.
|
|
59
|
+
|
|
60
|
+
## AI provider setup
|
|
61
|
+
|
|
62
|
+
The root-cause explanation step works with Claude, OpenAI, Gemini, or a
|
|
63
|
+
local model โ anything speaking an OpenAI-compatible chat API (Ollama, LM
|
|
64
|
+
Studio, llama.cpp server, vLLM, and similar all qualify).
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# cloud
|
|
68
|
+
flarebisect config set-key anthropic sk-ant-...
|
|
69
|
+
flarebisect config set-key openai sk-...
|
|
70
|
+
flarebisect config set-key google AI...
|
|
71
|
+
|
|
72
|
+
# local, no key needed - just have Ollama running
|
|
73
|
+
flarebisect config use ollama
|
|
74
|
+
flarebisect config set-model ollama llama3.1
|
|
75
|
+
|
|
76
|
+
# or let flarebisect pick a model sized to your GPU and pull it for you
|
|
77
|
+
flarebisect models detect # shows detected GPU/VRAM + recommended model
|
|
78
|
+
flarebisect models pull # downloads it and sets it active (run with no args)
|
|
79
|
+
|
|
80
|
+
# any other OpenAI-compatible endpoint
|
|
81
|
+
flarebisect config use custom
|
|
82
|
+
flarebisect config set-base-url custom http://localhost:8080/v1
|
|
83
|
+
flarebisect config set-key custom sk-local
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run `flarebisect config` with no arguments for a guided setup wizard โ for
|
|
87
|
+
Ollama it detects your GPU/VRAM, prefills a right-sized model, and offers to
|
|
88
|
+
pull it on the spot.
|
|
89
|
+
|
|
90
|
+
`flarebisect config show` lists the active provider and stored settings
|
|
91
|
+
(keys are masked). Config lives in a JSON file under the OS config dir
|
|
92
|
+
(`~/.config/flarebisect/config.json` on Linux/macOS, `%APPDATA%` on Windows),
|
|
93
|
+
written with owner-only permissions.
|
|
94
|
+
|
|
95
|
+
Per-run overrides skip the config file entirely:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
flarebisect run ... --provider openai --model gpt-4o-mini --api-key sk-...
|
|
99
|
+
flarebisect run ... --provider ollama --base-url http://localhost:11434/v1
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`) work too,
|
|
103
|
+
as a fallback under whatever's in the config file.
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
flarebisect run \
|
|
109
|
+
--repo /path/to/repo \
|
|
110
|
+
--good <known-good-sha> \
|
|
111
|
+
--bad <known-bad-sha> \
|
|
112
|
+
--test "pytest -k my_flaky_test" \
|
|
113
|
+
--runs 20 \
|
|
114
|
+
--threshold 0.3
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `--runs` โ test executions per commit (default 20), run in parallel
|
|
118
|
+
(capped at your core count so it doesn't oversubscribe). Lower counts are
|
|
119
|
+
faster but noisier โ a low sample can misattribute the culprit near the
|
|
120
|
+
threshold boundary.
|
|
121
|
+
- `--threshold` โ flake-rate jump (0-1) vs. the `good` baseline required to
|
|
122
|
+
call a commit the culprit (default 0.3).
|
|
123
|
+
- `--no-explain` โ skip the LLM root-cause call entirely (fully offline).
|
|
124
|
+
|
|
125
|
+
## Demo
|
|
126
|
+
|
|
127
|
+
See [`demo/`](demo/) for a self-contained, seeded-flaky-bug repo you can
|
|
128
|
+
bisect against with no network access required for the bisection itself.
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
1. Measure the flake rate at `good` and `bad` as baselines.
|
|
133
|
+
2. Binary-search the commit range between them.
|
|
134
|
+
3. At each candidate, run the test `N` times in an isolated worktree and
|
|
135
|
+
compute `failed / N`.
|
|
136
|
+
4. The culprit is the first commit whose flake rate jumps by `>= threshold`
|
|
137
|
+
over the `good` baseline.
|
|
138
|
+
5. Verdict: **clean break** (rate goes ~0% โ ~100%) vs. **flakiness
|
|
139
|
+
regression** (rate goes ~0% โ somewhere in between).
|
|
140
|
+
6. The configured LLM gets the culprit's diff + the flake-rate evidence and
|
|
141
|
+
returns a short root-cause read (race condition, shared mutable state,
|
|
142
|
+
timing assumption, etc.).
|
|
143
|
+
|
|
144
|
+
Binary search assumes the flake rate is roughly monotonic across the range,
|
|
145
|
+
same as ordinary `git bisect` assumes pass/fail is. At low `--runs` counts
|
|
146
|
+
a noisy sample can occasionally violate that near the threshold boundary โ
|
|
147
|
+
bump `--runs` if a result looks off.
|
|
148
|
+
|
|
149
|
+
## Releasing
|
|
150
|
+
|
|
151
|
+
Version is single-sourced from `src/flarebisect/__init__.py`. Bump it, commit,
|
|
152
|
+
tag (`vX.Y.Z`), and cut a GitHub Release โ `.github/workflows/publish.yml`
|
|
153
|
+
builds and publishes to PyPI via trusted publishing (OIDC, no stored token).
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flarebisect"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "git bisect that treats flakiness as a signal, not noise"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Tawin Tangsukson" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["git", "bisect", "testing", "flaky-tests", "cli", "debugging"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Debuggers",
|
|
28
|
+
"Topic :: Software Development :: Testing",
|
|
29
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"typer>=0.12",
|
|
33
|
+
"rich>=13.7",
|
|
34
|
+
"anthropic>=0.40",
|
|
35
|
+
"openai>=1.40",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
"build>=1.2",
|
|
42
|
+
"twine>=5.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/kaorii-ako/FlareBisect"
|
|
47
|
+
Repository = "https://github.com/kaorii-ako/FlareBisect"
|
|
48
|
+
Issues = "https://github.com/kaorii-ako/FlareBisect/issues"
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
flarebisect = "flarebisect.cli:app"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.version]
|
|
54
|
+
path = "src/flarebisect/__init__.py"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/flarebisect"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = [
|
|
61
|
+
"/src",
|
|
62
|
+
"/tests",
|
|
63
|
+
"/README.md",
|
|
64
|
+
"/LICENSE",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Ask the configured LLM for a short root-cause read on the culprit commit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .bisect import BisectOutcome
|
|
6
|
+
from .providers import ProviderConfig, complete
|
|
7
|
+
|
|
8
|
+
PROMPT_TEMPLATE = """A commit changed a test's failure rate. Diagnose the likely cause.
|
|
9
|
+
|
|
10
|
+
commit {sha}: "{subject}"
|
|
11
|
+
|
|
12
|
+
flake rate before: {before_rate:.0%}
|
|
13
|
+
flake rate after: {culprit_rate:.0%}
|
|
14
|
+
bisection threshold: {threshold:.0%}
|
|
15
|
+
verdict: {verdict}
|
|
16
|
+
|
|
17
|
+
diff:
|
|
18
|
+
```diff
|
|
19
|
+
{diff}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Answer in exactly 1-2 short sentences, no preamble, no restating the numbers above.
|
|
23
|
+
Point at the specific code change responsible (shared state, missing lock, timing/ordering
|
|
24
|
+
assumption, resource leak, off-by-one, whatever it actually is)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def explain(outcome: BisectOutcome, diff: str, threshold: float, provider_cfg: ProviderConfig) -> str:
|
|
28
|
+
prompt = PROMPT_TEMPLATE.format(
|
|
29
|
+
sha=outcome.culprit.sha[:12],
|
|
30
|
+
subject=outcome.culprit.subject,
|
|
31
|
+
before_rate=outcome.before.result.flake_rate,
|
|
32
|
+
culprit_rate=outcome.culprit.result.flake_rate,
|
|
33
|
+
threshold=threshold,
|
|
34
|
+
verdict=outcome.verdict,
|
|
35
|
+
diff=diff[:6000],
|
|
36
|
+
)
|
|
37
|
+
return complete(provider_cfg, prompt)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Flake-rate bisection: binary search over commits for the point where the
|
|
2
|
+
flake rate jumps by >= threshold relative to the known-good baseline."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from . import git_ops
|
|
12
|
+
from .runner import RunResult, measure_flake_rate, worker_count_for
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CommitMeasurement:
|
|
17
|
+
sha: str
|
|
18
|
+
subject: str
|
|
19
|
+
result: RunResult
|
|
20
|
+
position: int # -1 = good baseline, 0..n-1 = index within the good..bad range
|
|
21
|
+
status: str = "" # filled in once the culprit is known: good/stable/wobbling/flare/bad
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BisectOutcome:
|
|
26
|
+
culprit: CommitMeasurement
|
|
27
|
+
before: CommitMeasurement # last known-good-ish measurement (parent side)
|
|
28
|
+
good_baseline: RunResult
|
|
29
|
+
bad_baseline: RunResult
|
|
30
|
+
verdict: str # "clean break" | "flakiness regression"
|
|
31
|
+
measurements: list[CommitMeasurement] # every distinct commit measured, in range order
|
|
32
|
+
total_commits: int
|
|
33
|
+
runs: int
|
|
34
|
+
workers: int
|
|
35
|
+
elapsed_seconds: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _measure(repo: Path, sha: str, test_cmd: str, runs: int, position: int) -> CommitMeasurement:
|
|
39
|
+
wt = git_ops.add_worktree(repo, sha)
|
|
40
|
+
try:
|
|
41
|
+
result = measure_flake_rate(test_cmd, wt.path, runs)
|
|
42
|
+
finally:
|
|
43
|
+
git_ops.remove_worktree(repo, wt)
|
|
44
|
+
return CommitMeasurement(
|
|
45
|
+
sha=sha,
|
|
46
|
+
subject=git_ops.commit_subject(repo, sha),
|
|
47
|
+
result=result,
|
|
48
|
+
position=position,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def classify(good_rate: float, culprit_rate: float) -> str:
|
|
53
|
+
if culprit_rate >= 0.9 and good_rate <= 0.1:
|
|
54
|
+
return "clean break"
|
|
55
|
+
return "flakiness regression"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def status_label(rate: float, is_first: bool, is_last: bool, is_culprit: bool) -> str:
|
|
59
|
+
if is_culprit:
|
|
60
|
+
return "flare"
|
|
61
|
+
if is_last:
|
|
62
|
+
return "bad"
|
|
63
|
+
if rate <= 0.0:
|
|
64
|
+
return "good" if is_first else "stable"
|
|
65
|
+
return "wobbling"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_bisect(
|
|
69
|
+
repo: Path,
|
|
70
|
+
good: str,
|
|
71
|
+
bad: str,
|
|
72
|
+
test_cmd: str,
|
|
73
|
+
runs: int = 5,
|
|
74
|
+
threshold: float = 0.3,
|
|
75
|
+
on_measure: Callable[[str, CommitMeasurement], None] | None = None,
|
|
76
|
+
) -> BisectOutcome:
|
|
77
|
+
started = time.perf_counter()
|
|
78
|
+
|
|
79
|
+
good_sha = git_ops.resolve_sha(repo, good)
|
|
80
|
+
bad_sha = git_ops.resolve_sha(repo, bad)
|
|
81
|
+
|
|
82
|
+
good_m = _measure(repo, good_sha, test_cmd, runs, position=-1)
|
|
83
|
+
if on_measure:
|
|
84
|
+
on_measure("good", good_m)
|
|
85
|
+
|
|
86
|
+
commits = git_ops.commit_list(repo, good_sha, bad_sha)
|
|
87
|
+
if not commits:
|
|
88
|
+
raise ValueError("no commits between good and bad")
|
|
89
|
+
|
|
90
|
+
baseline_rate = good_m.result.flake_rate
|
|
91
|
+
|
|
92
|
+
cache: dict[str, CommitMeasurement] = {}
|
|
93
|
+
|
|
94
|
+
def measure_idx(idx: int) -> CommitMeasurement:
|
|
95
|
+
sha = commits[idx]
|
|
96
|
+
if sha not in cache:
|
|
97
|
+
m = _measure(repo, sha, test_cmd, runs, position=idx)
|
|
98
|
+
cache[sha] = m
|
|
99
|
+
if on_measure:
|
|
100
|
+
on_measure(f"candidate[{idx}]", m)
|
|
101
|
+
return cache[sha]
|
|
102
|
+
|
|
103
|
+
bad_m = measure_idx(len(commits) - 1)
|
|
104
|
+
|
|
105
|
+
lo, hi = 0, len(commits) - 1
|
|
106
|
+
culprit_idx = hi # fall back to bad commit if search never narrows
|
|
107
|
+
culprit_measurement = bad_m
|
|
108
|
+
|
|
109
|
+
while lo <= hi:
|
|
110
|
+
mid = (lo + hi) // 2
|
|
111
|
+
m = measure_idx(mid)
|
|
112
|
+
jumped = (m.result.flake_rate - baseline_rate) >= threshold
|
|
113
|
+
if jumped:
|
|
114
|
+
culprit_idx = mid
|
|
115
|
+
culprit_measurement = m
|
|
116
|
+
hi = mid - 1
|
|
117
|
+
else:
|
|
118
|
+
lo = mid + 1
|
|
119
|
+
|
|
120
|
+
before_measurement = measure_idx(culprit_idx - 1) if culprit_idx > 0 else good_m
|
|
121
|
+
|
|
122
|
+
verdict = classify(before_measurement.result.flake_rate, culprit_measurement.result.flake_rate)
|
|
123
|
+
|
|
124
|
+
last_idx = len(commits) - 1
|
|
125
|
+
ordered = [good_m] + [cache[sha] for sha in commits if sha in cache]
|
|
126
|
+
for m in ordered:
|
|
127
|
+
m.status = status_label(
|
|
128
|
+
m.result.flake_rate,
|
|
129
|
+
is_first=(m.position == -1),
|
|
130
|
+
is_last=(m.position == last_idx),
|
|
131
|
+
is_culprit=(m.sha == culprit_measurement.sha),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return BisectOutcome(
|
|
135
|
+
culprit=culprit_measurement,
|
|
136
|
+
before=before_measurement,
|
|
137
|
+
good_baseline=good_m.result,
|
|
138
|
+
bad_baseline=bad_m.result,
|
|
139
|
+
verdict=verdict,
|
|
140
|
+
measurements=ordered,
|
|
141
|
+
total_commits=len(commits),
|
|
142
|
+
runs=runs,
|
|
143
|
+
workers=worker_count_for(runs),
|
|
144
|
+
elapsed_seconds=time.perf_counter() - started,
|
|
145
|
+
)
|