Skip to content

Commit 263d4c0

Browse files
committed
feat(bisect): Add new bisection feature
When kernel breaks we need to be able to use automated way bisection, using bisect command. This is initial version, that just works, but needs a bit more work to work reliably. I prefer to commit early, so we don't accumulate too much uncommited code. Signed-off-by: Denys Fedoryshchenko <[email protected]>
1 parent 46a699d commit 263d4c0

File tree

2 files changed

+295
-1
lines changed

2 files changed

+295
-1
lines changed

kcidev/main.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import click
55

66
from kcidev.libs.common import *
7-
from kcidev.subcommands import checkout, commit, patch, results, testretry
7+
from kcidev.subcommands import (bisect, checkout, commit, patch, results,
8+
testretry)
89

910

1011
@click.group(
@@ -35,6 +36,7 @@ def cli(ctx, settings, instance):
3536

3637

3738
def run():
39+
cli.add_command(bisect.bisect)
3840
cli.add_command(checkout.checkout)
3941
cli.add_command(commit.commit)
4042
cli.add_command(patch.patch)

kcidev/subcommands/bisect.py

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import json
5+
import os
6+
import re
7+
import subprocess
8+
import sys
9+
10+
import click
11+
import requests
12+
import toml
13+
from git import Repo
14+
15+
"""
16+
To not lose the state of the bisection, we need to store the state in a file
17+
The state file is a json file that contains the following keys:
18+
- giturl: the repository url
19+
- branch: the repository branch
20+
- good: the known good commit
21+
- bad: the known bad commit
22+
- retryfail: the number of times to retry the failed test
23+
- history: the list of commits that have been tested (each entry has "commitid": state)
24+
"""
25+
default_state = {
26+
"giturl": "",
27+
"branch": "",
28+
"retryfail": 0,
29+
"good": "",
30+
"bad": "",
31+
"history": [],
32+
"jobfilter": [],
33+
"platformfilter": [],
34+
"test": "",
35+
"workdir": "",
36+
"bisect_init": False,
37+
"next_commit": None,
38+
}
39+
40+
41+
def api_connection(host):
42+
click.secho("api connect: " + host, fg="green")
43+
return host
44+
45+
46+
def load_state(file="state.json"):
47+
if os.path.exists(file):
48+
with open(file, "r") as f:
49+
return json.load(f)
50+
else:
51+
return None
52+
53+
54+
def print_state(state):
55+
click.secho("Loaded state file", fg="green")
56+
click.secho("giturl: " + state["giturl"], fg="green")
57+
click.secho("branch: " + state["branch"], fg="green")
58+
click.secho("good: " + state["good"], fg="green")
59+
click.secho("bad: " + state["bad"], fg="green")
60+
click.secho("retryfail: " + str(state["retryfail"]), fg="green")
61+
click.secho("history: " + str(state["history"]), fg="green")
62+
click.secho("jobfilter: " + str(state["jobfilter"]), fg="green")
63+
click.secho("platformfilter: " + str(state["platformfilter"]), fg="green")
64+
click.secho("test: " + state["test"], fg="green")
65+
click.secho("workdir: " + state["workdir"], fg="green")
66+
click.secho("bisect_init: " + str(state["bisect_init"]), fg="green")
67+
click.secho("next_commit: " + str(state["next_commit"]), fg="green")
68+
69+
70+
def save_state(state, file="state.json"):
71+
with open(file, "w") as f:
72+
json.dump(state, f)
73+
74+
75+
def git_exec_getcommit(cmd):
76+
click.secho("Executing git command: " + " ".join(cmd), fg="green")
77+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
78+
if result.returncode != 0:
79+
click.secho("git command return failed", fg="red")
80+
sys.exit(1)
81+
lines = result.stdout.split(b"\n")
82+
if len(lines) < 2:
83+
click.secho(f"git command answer length failed: {lines}", fg="red")
84+
sys.exit(1)
85+
# is it last bisect?: "is the first bad commit"
86+
if "is the first bad commit" in str(lines[1]):
87+
click.secho(f"git command: {lines}", fg="green")
88+
# TBD save state somehow?
89+
sys.exit(0)
90+
re_commit = re.search(r"\[([a-f0-9]+)\]", str(lines[1]))
91+
if not re_commit:
92+
click.secho(f"git command regex failed: {lines}", fg="red")
93+
sys.exit(1)
94+
return re_commit.group(1)
95+
96+
97+
def kcidev_exec(cmd):
98+
"""
99+
Execute a kci-dev and return the return code
100+
"""
101+
process = None
102+
click.secho("Executing kci-dev command: " + " " + str(cmd), fg="green")
103+
with subprocess.Popen(
104+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
105+
) as process:
106+
try:
107+
for line in process.stdout:
108+
click.echo(line, nl=False)
109+
process.wait()
110+
except Exception as e:
111+
click.secho(f"Error executing kci-dev: {e}", fg="red")
112+
sys.exit(1)
113+
114+
return process
115+
116+
117+
def init_bisect(state):
118+
olddir = os.getcwd()
119+
os.chdir(state["workdir"])
120+
click.secho("init bisect", fg="green")
121+
r = os.system("git bisect start")
122+
if r != 0:
123+
click.secho("git bisect start failed", fg="red")
124+
sys.exit(1)
125+
r = os.system("git bisect good " + state["good"])
126+
if r != 0:
127+
click.secho("git bisect good failed", fg="red")
128+
sys.exit(1)
129+
# result = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE)
130+
cmd = ["git", "bisect", "bad", state["bad"]]
131+
commitid = git_exec_getcommit(cmd)
132+
os.chdir(olddir)
133+
return commitid
134+
135+
136+
def bisection_loop(state):
137+
olddir = os.getcwd()
138+
os.chdir(state["workdir"])
139+
commit = state["next_commit"]
140+
if commit is None:
141+
click.secho("Bisection error?", fg="green")
142+
return
143+
click.secho("Testing commit: " + commit, fg="green")
144+
cmd = [
145+
"kci-dev",
146+
"checkout",
147+
"--giturl",
148+
state["giturl"],
149+
"--branch",
150+
state["branch"],
151+
"--test",
152+
state["test"],
153+
"--commit",
154+
commit,
155+
"--watch",
156+
]
157+
# jobfilter is array, so we need to add each element as a separate argument
158+
for job in state["jobfilter"]:
159+
cmd.append("--jobfilter")
160+
cmd.append(job)
161+
for platform in state["platformfilter"]:
162+
cmd.append("--platformfilter")
163+
cmd.append(platform)
164+
result = kcidev_exec(cmd)
165+
try:
166+
testret = result.returncode
167+
except Exception as e:
168+
click.secho(f"Error executing kci-dev, no returncode: {e}", fg="red")
169+
sys.exit
170+
if testret == 0:
171+
bisect_result = "good"
172+
elif testret == 1:
173+
# TBD: Retry failed test to make sure it is not a flaky test
174+
bisect_result = "bad"
175+
elif testret == 2:
176+
# TBD: Retry failed test to make sure it is not a flaky test
177+
bisect_result = "skip"
178+
else:
179+
click.secho("Maestro failed to execute the test", fg="red")
180+
# Internal maestro error, retry procesure
181+
return None
182+
cmd = ["git", "bisect", bisect_result]
183+
commitid = git_exec_getcommit(cmd)
184+
if not commitid:
185+
click.secho("git bisect failed, commit return is empty", fg="red")
186+
sys.exit(1)
187+
state["history"].append({commit: bisect_result})
188+
state["next_commit"] = commitid
189+
os.chdir(olddir)
190+
return state
191+
192+
193+
@click.command(help="Bisect Linux Kernel regression")
194+
@click.option("--giturl", help="define the repository url")
195+
@click.option("--branch", help="define the repository branch")
196+
@click.option("--good", help="known good commit")
197+
@click.option("--bad", help="known bad commit")
198+
@click.option("--retryfail", help="retry failed test N times", default=2)
199+
@click.option("--workdir", help="define the repository origin", default="kcidev-src")
200+
@click.option("--ignorestate", help="ignore save state", is_flag=True)
201+
@click.option("--statefile", help="state file", default="state.json")
202+
@click.option("--jobfilter", help="filter the job", multiple=True)
203+
@click.option("--platformfilter", help="filter the platform", multiple=True)
204+
@click.option("--test", help="Test expected to fail")
205+
206+
# test
207+
@click.pass_context
208+
def bisect(
209+
ctx,
210+
giturl,
211+
branch,
212+
good,
213+
bad,
214+
retryfail,
215+
workdir,
216+
ignorestate,
217+
statefile,
218+
jobfilter,
219+
platformfilter,
220+
test,
221+
):
222+
config = ctx.obj.get("CFG")
223+
instance = ctx.obj.get("INSTANCE")
224+
p_url = api_connection(config[instance]["pipeline"])
225+
226+
state_file = os.path.join(workdir, statefile)
227+
state = load_state(state_file)
228+
if state is None or ignorestate:
229+
state = default_state
230+
if not giturl:
231+
click.secho("--giturl is required", fg="red")
232+
return
233+
if not branch:
234+
click.secho("--branch is required", fg="red")
235+
return
236+
if not good:
237+
click.secho("--good is required", fg="red")
238+
return
239+
if not bad:
240+
click.secho("--bad is required", fg="red")
241+
return
242+
if not jobfilter:
243+
click.secho("--jobfilter is required", fg="red")
244+
return
245+
if not platformfilter:
246+
click.secho("--platformfilter is required", fg="red")
247+
return
248+
if not test:
249+
click.secho("--test is required", fg="red")
250+
return
251+
252+
state["giturl"] = giturl
253+
state["branch"] = branch
254+
state["good"] = good
255+
state["bad"] = bad
256+
state["retryfail"] = retryfail
257+
state["jobfilter"] = jobfilter
258+
state["platformfilter"] = platformfilter
259+
state["test"] = test
260+
state["workdir"] = workdir
261+
save_state(state, state_file)
262+
else:
263+
print_state(state)
264+
265+
# if workdir doesnt exist, clone the repository
266+
if not os.path.exists(workdir):
267+
click.secho("Cloning repository", fg="green")
268+
repo = Repo.clone_from(giturl, workdir)
269+
repo.git.checkout(branch)
270+
else:
271+
click.secho("Pulling repository", fg="green")
272+
repo = Repo(workdir)
273+
repo.git.checkout(branch)
274+
repo.git.pull()
275+
276+
if not state["bisect_init"]:
277+
state["next_commit"] = init_bisect(state)
278+
state["bisect_init"] = True
279+
save_state(state, state_file)
280+
281+
while True:
282+
click.secho("Bisection loop", fg="green")
283+
new_state = bisection_loop(state)
284+
if new_state is None:
285+
click.secho("Retry failed test", fg="green")
286+
continue
287+
state = new_state
288+
save_state(state, state_file)
289+
290+
291+
if __name__ == "__main__":
292+
main_kcidev()

0 commit comments

Comments
 (0)