Skip to content

Commit 2570489

Browse files
authored
test: add bisect-script.sh to help bisect CI tests (#1215)
Signed-off-by: Terry Kong <terryk@nvidia.com>
1 parent 16e08cd commit 2570489

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed

tools/bisect-script.sh

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -euo pipefail
17+
18+
# When we bisect, we need to ensure that the venvs are refreshed b/c the commit could
19+
# habe changed the uv.lock or 3rdparty submoduels, so we need to force a rebuild to be safe
20+
export NRL_FORCE_REBUILD_VENVS=true
21+
print_usage() {
22+
cat <<EOF
23+
Usage: GOOD=<good_ref> BAD=<bad_ref> tools/bisect-script.sh [command ...]
24+
25+
Runs a git bisect session between GOOD and BAD to find the first bad commit.
26+
Sets NRL_FORCE_REBUILD_VENVS=true to ensure test environments are rebuilt to match commit's uv.lock.
27+
28+
Examples:
29+
GOOD=56a6225 BAD=32faafa tools/bisect-script.sh uv run --group dev pre-commit run --all-files
30+
GOOD=464ed38 BAD=c843f1b tools/bisect-script.sh uv run --group test pytest tests/unit/test_foobar.py
31+
32+
# Example ouptut:
33+
# 1. Will run until hits the first bad commit.
34+
# 2. Will show the bisect log (what was run) and visualize the bisect.
35+
# 3. Reset git bisect state to return you to the git state you were originally.
36+
#
37+
# 25e05a3d557dfe59a14df43048e16b6eea04436e is the first bad commit
38+
# commit 25e05a3d557dfe59a14df43048e16b6eea04436e
39+
# Author: Terry Kong <terryk@nvidia.com>
40+
# Date: Fri Sep 26 17:24:45 2025 +0000
41+
#
42+
# 3==4
43+
#
44+
# Signed-off-by: Terry Kong <terryk@nvidia.com>
45+
#
46+
# tests/unit/test_foobar.py | 2 +-
47+
# 1 file changed, 1 insertion(+), 1 deletion(-)
48+
# bisect found first bad commit
49+
# + RUN_STATUS=0
50+
# + set +x
51+
# [bisect] --- bisect log ---
52+
# # bad: [c843f1b994cb7e331aa8bc41c3206a6e76e453ef] try echo
53+
# # good: [464ed38e68dcd23f0c1951784561dc8c78410ffe] add passing foobar
54+
# git bisect start 'c843f1b' '464ed38'
55+
# # good: [8b8b3961e9cdbc1b4a9b6a912f7d36d117952f62] try visualize
56+
# git bisect good 8b8b3961e9cdbc1b4a9b6a912f7d36d117952f62
57+
# # bad: [25e05a3d557dfe59a14df43048e16b6eea04436e] 3==4
58+
# git bisect bad 25e05a3d557dfe59a14df43048e16b6eea04436e
59+
# # good: [c82e0b69d52b8e1641226c022cb487afebe8ba99] 2==2
60+
# git bisect good c82e0b69d52b8e1641226c022cb487afebe8ba99
61+
# # first bad commit: [25e05a3d557dfe59a14df43048e16b6eea04436e] 3==4
62+
# [bisect] --- bisect visualize (oneline) ---
63+
# 25e05a3d (HEAD) 3==4
64+
65+
Exit codes inside the command determine good/bad:
66+
0 -> good commit
67+
non-zero -> bad commit
68+
125 -> skip this commit (per git-bisect convention)
69+
70+
Environment variables:
71+
GOOD Commit-ish known to be good (required)
72+
BAD Commit-ish suspected bad (required)
73+
(The script will automatically restore the repo state with 'git bisect reset' on exit.)
74+
75+
Notes:
76+
- The working tree will be reset by git bisect. Ensure you have no uncommitted changes.
77+
- If GOOD is an ancestor of BAD with 0 or 1 commits in between, git can
78+
conclude immediately; the script will show the result and exit without
79+
running your command.
80+
EOF
81+
}
82+
83+
# Minimal color helpers: blue for info, red for errors (TTY-only; NO_COLOR disables)
84+
BLUE=""; RED=""; NC=""
85+
if [[ -z "${NO_COLOR:-}" ]] && { [[ -t 1 ]] || [[ -t 2 ]]; }; then
86+
BLUE=$'\033[34m'
87+
RED=$'\033[31m'
88+
NC=$'\033[0m'
89+
fi
90+
91+
iecho() { printf "%b%s%b\n" "$BLUE" "$*" "$NC"; }
92+
fecho() { printf "%b%s%b\n" "$RED" "$*" "$NC" >&2; }
93+
94+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
95+
print_usage
96+
exit 0
97+
fi
98+
99+
if [[ -z "${GOOD:-}" || -z "${BAD:-}" ]]; then
100+
fecho "ERROR: GOOD and BAD environment variables are required."
101+
echo >&2
102+
print_usage >&2
103+
exit 2
104+
fi
105+
106+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
107+
fecho "ERROR: Not inside a git repository."
108+
exit 2
109+
fi
110+
111+
# Ensure there is a command to run
112+
if [[ $# -lt 1 ]]; then
113+
fecho "ERROR: Missing command to evaluate during bisect."
114+
echo >&2
115+
print_usage >&2
116+
exit 2
117+
fi
118+
119+
USER_CMD=("$@")
120+
121+
# Require a clean working tree
122+
git update-index -q --refresh || true
123+
if ! git diff --quiet; then
124+
fecho "ERROR: Unstaged changes present. Commit or stash before bisect."
125+
exit 2
126+
fi
127+
if ! git diff --cached --quiet; then
128+
fecho "ERROR: Staged changes present. Commit or stash before bisect."
129+
exit 2
130+
fi
131+
132+
# On interruption or script error, print helpful message
133+
on_interrupt_or_error() {
134+
local status=$?
135+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
136+
if git bisect log >/dev/null 2>&1; then
137+
iecho "[bisect] Script interrupted or failed (exit ${status})."
138+
iecho "[bisect] Restoring original state with 'git bisect reset' on exit."
139+
fi
140+
fi
141+
}
142+
trap on_interrupt_or_error INT TERM ERR
143+
144+
# Always reset bisect on exit to restore original state
145+
cleanup_reset() {
146+
if [[ -n "${BISECT_NO_RESET:-}" ]]; then
147+
# Respect user's request to not reset the bisect
148+
return
149+
fi
150+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
151+
if git bisect log >/dev/null 2>&1; then
152+
git bisect reset >/dev/null 2>&1 || true
153+
fi
154+
fi
155+
}
156+
trap cleanup_reset EXIT
157+
158+
# Check if we are already in a bisect session
159+
if git bisect log >/dev/null 2>&1; then
160+
fecho "[bisect] We are already in a bisect session. Please reset the bisect manually if you want to start a new one."
161+
exit 1
162+
fi
163+
164+
set -x
165+
git bisect start "$BAD" "$GOOD"
166+
set +x
167+
168+
# Detect immediate conclusion (no midpoints to test)
169+
if git bisect log >/dev/null 2>&1; then
170+
if git bisect log | grep -q "first bad commit:"; then
171+
iecho "[bisect] Immediate conclusion from endpoints; no midpoints to test."
172+
iecho "[bisect] --- bisect log ---"
173+
git bisect log | cat
174+
exit 0
175+
fi
176+
fi
177+
178+
set -x
179+
set +e # Temporarily allow the command to fail to capture the exit status
180+
git bisect run "${USER_CMD[@]}"
181+
RUN_STATUS=$?
182+
set -e
183+
set +x
184+
185+
# Show bisect details before cleanup
186+
if git bisect log >/dev/null 2>&1; then
187+
iecho "[bisect] --- bisect log ---"
188+
git bisect log | cat
189+
fi
190+
191+
exit $RUN_STATUS
192+
193+

0 commit comments

Comments
 (0)