CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
Ardupilot

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

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