Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ardupilot
GitHub Repository: Ardupilot/ardupilot
Path: blob/master/Tools/autotest/bisect-helper.py
9692 views
1
#!/usr/bin/env python3
2
3
'''A helper script for bisecting common problems when working with ArduPilot
4
5
When running bisections, you should
6
7
export SITL_PANIC_EXIT=1
8
9
Bisect between a commit which builds and one which doesn't,
10
finding the first commit which broke the build with a
11
specific failure:
12
13
git bisect reset
14
git bisect good a7647e77d9
15
git bisect bad 153ad9539866f8d93a99e9998118bb090d2f747f
16
cp -a Tools/autotest/bisect-helper.py /tmp
17
git bisect run /tmp/bisect-helper.py --build \
18
--build-failure-string= \
19
"reference to 'OpticalFlow' is ambiguous"
20
21
Work out who killed bebop:
22
cp -a Tools/autotest/bisect-helper.py /tmp
23
git bisect reset
24
git bisect good a7647e77d9 &&
25
git bisect bad 153ad9539866f8d93a99e9998118bb090d2f747f &&
26
git bisect run /tmp/bisect-helper.py --build \
27
--waf-configure-arg="--board pixflamingo" \
28
--build-failure-string="note: this is the location of the previous definition"
29
30
# Use a failing test to work out which commit broke things:
31
cp Tools/autotest/bisect-helper.py /tmp
32
git bisect reset
33
git bisect start
34
git bisect bad
35
git bisect good HEAD~1024
36
time git bisect run /tmp/bisect-helper.py --autotest --autotest-vehicle=Plane --autotest-test=NeedEKFToArm --autotest-branch=wip/bisection-using-named-test # noqa
37
38
Work out who overflowed Omnbusf4pro:
39
cp -a Tools Tools2
40
GOOD=c4ce6fa3851f93df34393c376fee5b37e0a270d2
41
BAD=f00bf77af75f828334f735580d6b19698b639a74
42
BFS="overflowed by"
43
git bisect reset
44
git bisect start
45
git bisect good $GOOD &&
46
git bisect bad $BAD &&
47
git bisect run Tools2/autotest/bisect-helper.py --build \
48
--waf-configure-arg="--board OmniBusF4Pro" \
49
--build-failure-string="$BFS"
50
51
# Use a flapping test to work out which commit broke things. The
52
# "autotest-branch" is the branch containing the flapping test (which
53
# may be master)
54
rm /tmp/bisect-debug/*; git commit -m "stuff" -a ; cp Tools/autotest/bisect-helper.py /tmp; git bisect reset; git bisect start; git bisect bad d24e569b20; git bisect good 3f6fd49507f286ad8f6ccc9e29b110d5e9fc9207^
55
time git bisect run /tmp/bisect-helper.py --autotest --autotest-vehicle=Copter --autotest-test=Replay --autotest-branch=wip/bisection-using-flapping-test --autotest-test-passes=40 --autotest-failure-require-string="Mismatch in field XKF1.Pitch" --autotest-failure-ignore-string="HALSITL::SITL_State::_check_rc_input"
56
57
AP_FLAKE8_CLEAN
58
59
'''
60
61
import optparse
62
import os
63
import subprocess
64
import shlex
65
import sys
66
import time
67
import traceback
68
69
70
def get_exception_stacktrace(e):
71
ret = "%s\n" % e
72
ret += ''.join(traceback.format_exception(type(e),
73
value=e,
74
tb=e.__traceback__))
75
return ret
76
77
78
class Bisect(object):
79
def __init__(self, opts):
80
self.opts = opts
81
82
def exit_skip_code(self):
83
return 125
84
85
def exit_pass_code(self):
86
return 0
87
88
def exit_fail_code(self):
89
return 1
90
91
def exit_abort_code(self):
92
return 129
93
94
def exit_skip(self):
95
self.progress("SKIP")
96
sys.exit(self.exit_skip_code())
97
98
def exit_pass(self):
99
self.progress("PASS")
100
sys.exit(self.exit_pass_code())
101
102
def exit_fail(self):
103
self.progress("FAIL")
104
sys.exit(self.exit_fail_code())
105
106
def exit_abort(self):
107
'''call when this harness has failed (e.g. to reset to required
108
state)'''
109
self.progress("ABORT")
110
sys.exit(self.exit_abort_code())
111
112
def progress(self, string):
113
'''pretty-print progress'''
114
print("BH: %s" % string)
115
116
def run_program(self, prefix, cmd_list):
117
'''copied in from build_binaries.py'''
118
'''run cmd_list, spewing and setting output in self'''
119
self.progress("Running (%s)" % " ".join(cmd_list))
120
p = subprocess.Popen(cmd_list,
121
stdin=None,
122
close_fds=True,
123
stdout=subprocess.PIPE,
124
stderr=subprocess.STDOUT)
125
self.program_output = ""
126
while True:
127
x = p.stdout.readline()
128
if len(x) == 0:
129
waitpid_result = os.waitpid(p.pid, 0)
130
if waitpid_result:
131
break
132
# select not available on Windows... probably...
133
time.sleep(0.1)
134
continue
135
if isinstance(x, bytes):
136
x = x.decode('utf-8')
137
self.program_output += x
138
x = x.rstrip()
139
print("%s: %s" % (prefix, x))
140
(pid, status) = waitpid_result
141
if status != 0:
142
self.progress("Process failed (%s)" %
143
str(waitpid_result))
144
raise subprocess.CalledProcessError(
145
status, cmd_list)
146
147
def build(self):
148
'''run ArduCopter build. May exit with skip or fail'''
149
self.run_program("WAF-clean", ["./waf", "clean"])
150
cmd_configure = ["./waf", "configure"]
151
pieces = [shlex.split(x)
152
for x in self.opts.waf_configure_args]
153
for piece in pieces:
154
cmd_configure.extend(piece)
155
self.run_program("WAF-configure", cmd_configure)
156
cmd_build = ["./waf", "build"]
157
pieces = [shlex.split(x)
158
for x in self.opts.waf_build_args]
159
for piece in pieces:
160
cmd_build.extend(piece)
161
try:
162
self.run_program("WAF-build", cmd_build)
163
except subprocess.CalledProcessError:
164
# well, it definitely failed....
165
if self.opts.build_failure_string is not None:
166
if self.opts.build_failure_string in self.program_output:
167
self.progress("Found relevant build failure")
168
self.exit_fail()
169
# it failed, but not for the reason we're looking
170
# for...
171
self.exit_skip()
172
else:
173
self.exit_fail()
174
175
def update_submodules(self):
176
try:
177
self.run_program("Update submodules",
178
["git", "submodule", "update", "--init", "--recursive"])
179
except subprocess.CalledProcessError:
180
self.exit_abort()
181
182
183
class BisectBuild(Bisect):
184
185
def __init__(self, opts):
186
super(BisectBuild, self).__init__(opts)
187
188
def run(self):
189
if self.opts.build_failure_string is None:
190
self.progress("--build-failure-string is required when using --build")
191
self.exit_abort()
192
193
self.update_submodules()
194
self.build() # may exit with skip or fail
195
self.exit_pass()
196
197
198
class BisectCITest(Bisect):
199
200
def __init__(self, opts):
201
super(BisectCITest, self).__init__(opts)
202
203
def autotest_script(self):
204
return os.path.join("Tools", "autotest", "autotest.py")
205
206
def git_reset(self):
207
try:
208
self.run_program("Reset autotest directory", ["git", "reset", "--hard"])
209
except subprocess.CalledProcessError:
210
self.exit_abort()
211
212
def get_current_hash(self):
213
self.run_program("Get current hash", ["git", "rev-parse", "HEAD"])
214
x = self.program_output
215
return x.strip()
216
217
def run(self):
218
219
current_hash = self.get_current_hash()
220
221
self.debug_dir = os.path.join("/tmp", "bisect-debug")
222
if not os.path.exists(self.debug_dir):
223
os.mkdir(self.debug_dir)
224
225
if self.opts.autotest_branch is None:
226
raise ValueError("expected autotest branch")
227
228
self.update_submodules()
229
230
try:
231
self.run_program("Check autotest directory out from master",
232
["git", "checkout", self.opts.autotest_branch, "Tools/autotest"])
233
except subprocess.CalledProcessError:
234
self.exit_abort()
235
236
self.progress("Build")
237
cmd = [self.autotest_script()]
238
if self.opts.autotest_valgrind:
239
cmd.append("--debug")
240
cmd.append("build.%s" % self.opts.autotest_vehicle)
241
print("build cmd: %s" % str(cmd))
242
243
try:
244
self.run_program("Run autotest (build)", cmd)
245
except subprocess.CalledProcessError:
246
self.git_reset()
247
self.exit_skip()
248
249
cmd = [self.autotest_script()]
250
if self.opts.autotest_valgrind:
251
cmd.append("--valgrind")
252
cmd.append("test.%s.%s" % (self.opts.autotest_vehicle, self.opts.autotest_test))
253
254
code = self.exit_pass_code()
255
for i in range(0, self.opts.autotest_test_passes):
256
ignore = False
257
try:
258
self.run_program(
259
"Run autotest (%u/%u)" % (i+1, self.opts.autotest_test_passes),
260
cmd)
261
except subprocess.CalledProcessError:
262
for ignore_string in self.opts.autotest_failure_ignore_string:
263
if ignore_string in self.program_output:
264
self.progress("Found ignore string (%s) in program output" % ignore_string)
265
ignore = True
266
if not ignore and self.opts.autotest_failure_require_string is not None:
267
if self.opts.autotest_failure_require_string not in self.program_output:
268
# it failed, but not for the reason we're looking
269
# for...
270
self.progress("Did not find test failure string (%s); skipping" %
271
self.opts.autotest_failure_require_string)
272
code = self.exit_skip_code()
273
break
274
if not ignore:
275
code = self.exit_fail_code()
276
277
with open(os.path.join(self.debug_dir, "run-%s-%u.txt" % (current_hash, i+1)), "w") as f:
278
f.write(self.program_output)
279
280
if code == self.exit_fail_code():
281
with open("/tmp/fail-counts", "a") as f:
282
f.write("Failed on run %u\n" % (i+1,))
283
if ignore:
284
self.progress("Ignoring this run")
285
continue
286
if code != self.exit_pass_code():
287
break
288
289
self.git_reset()
290
291
self.progress("Exit code is %u" % code)
292
293
sys.exit(code)
294
295
296
if __name__ == '__main__':
297
298
parser = optparse.OptionParser("bisect.py ")
299
parser.add_option("--build",
300
action='store_true',
301
default=False,
302
help="Help bisect a build failure")
303
parser.add_option("--build-failure-string",
304
type='string',
305
help="Must be present in"
306
"build output to count as a failure")
307
308
group_autotest = optparse.OptionGroup(parser, "Run-AutoTest Options")
309
group_autotest.add_option("--autotest",
310
action='store_true',
311
default=False,
312
help="Bisect a failure with an autotest test")
313
group_autotest.add_option("", "--autotest-vehicle",
314
dest="autotest_vehicle",
315
type="string",
316
default="ArduCopter",
317
help="Which vehicle to run tests for")
318
group_autotest.add_option("", "--autotest-test",
319
dest="autotest_test",
320
type="string",
321
default="NavDelayAbsTime",
322
help="Test to run to find failure")
323
group_autotest.add_option("", "--autotest-valgrind",
324
dest="autotest_valgrind",
325
action='store_true',
326
default=False,
327
help="Run autotest under valgrind")
328
group_autotest.add_option("", "--autotest-test-passes",
329
dest="autotest_test_passes",
330
type=int,
331
default=1,
332
help="Number of times to run test before declaring it is good")
333
group_autotest.add_option("", "--autotest-branch",
334
dest="autotest_branch",
335
type="string",
336
help="Branch on which the test exists. The autotest directory will be reset to this branch")
337
group_autotest.add_option("--autotest-failure-require-string",
338
type='string',
339
default=None,
340
help="If supplied, must be present in"
341
"test output to count as a failure")
342
group_autotest.add_option("--autotest-failure-ignore-string",
343
type='string',
344
default=[],
345
action="append",
346
help="If supplied and present in"
347
"test output run will be ignored")
348
349
group_build = optparse.OptionGroup(parser, "Build options")
350
group_build.add_option("", "--waf-configure-arg",
351
action="append",
352
dest="waf_configure_args",
353
type="string",
354
default=["--board skyviper-v2450"],
355
help="extra arguments to pass to"
356
"waf in configure step")
357
group_build.add_option("", "--waf-build-arg",
358
action="append",
359
dest="waf_build_args",
360
type="string",
361
default=["--target bin/arducopter"],
362
help="extra arguments to pass"
363
"to waf in its build step")
364
365
parser.add_option_group(group_build)
366
367
(opts, args) = parser.parse_args()
368
369
if opts.build:
370
bisecter = BisectBuild(opts)
371
elif opts.autotest:
372
bisecter = BisectCITest(opts)
373
else:
374
raise ValueError("Not told how to bisect")
375
376
try:
377
bisecter.run()
378
except Exception as e:
379
print("Caught exception in bisect-helper: %s" % str(e))
380
print(get_exception_stacktrace(e))
381
sys.exit(129) # should abort the bisect process
382
383