Skip to content

Commit 2f61adc

Browse files
committed
Merge pull request #199 from diydrones/tcr-gototest
Adds goto test.
2 parents a6b5b55 + ba6c16e commit 2f61adc

File tree

5 files changed

+225
-46
lines changed

5 files changed

+225
-46
lines changed

.travis.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
before_install: []
22
env:
3-
global: []
3+
global:
4+
- TEST_SPEEDUP=10
5+
- TEST_RATE=200
6+
- TEST_RETRY=1
47
install:
58
- 'a() { set -e; }'
69
- "z() { E=$?; test $E -eq 0 && return 0; printf \"\\n\\033[1;31mThe command failed with exit code $?.\\033[0m\"; set -e; return $E; }"
@@ -9,7 +12,7 @@ install:
912
script:
1013
- (a; sudo python setup.py install );z
1114
- (a; nosetests tests/web );z
12-
- (a; cd tests; python -m sitl );z
15+
- (a; cd tests; python -um sitl );z
1316
git:
1417
depth: 10
1518
language: objective-c

appveyor.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
environment:
2-
global: {}
2+
global:
3+
TEST_SPEEDUP: 10
4+
TEST_RATE: 200
5+
TEST_RETRY: 4
36
init: []
47
cache:
58
- "c:\\Users\\appveyor\\.pip-wheelhouse"
@@ -19,6 +22,8 @@ install:
1922
- cmd: "setlocal & SET PATH=%PYTHON%;c:\\Python27\\Scripts;%PATH% & pip2 install git+https://github.com/3drobotics/dronekit-sitl-runner.git & endlocal"
2023
build_script:
2124
- cmd: 'setlocal & python setup.py install & endlocal'
25+
- cmd: "setlocal & SET PATH=%PYTHON%;c:\\Python27\\Scripts;%PATH% & nosetests tests\\web & endlocal"
26+
- cmd: "setlocal & SET PATH=%PYTHON%;c:\\Python27\\Scripts;%PATH% & cd tests & python -um sitl & endlocal"
2227
clone_depth: 10
2328
test: 'off'
2429
branches:

circle.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
machine:
2-
environment: {}
2+
environment:
3+
TEST_SPEEDUP: 10
4+
TEST_RATE: 200
5+
TEST_RETRY: 1
36
dependencies:
47
override:
58
- python setup.py install:
69
environment: {}
710
- nosetests tests/web:
811
environment: {}
9-
- cd tests; python -m sitl:
12+
- cd tests; python -um sitl:
1013
environment: {}
1114
pre:
1215
- pip2 install nose psutil:

tests/sitl/__main__.py

+115-41
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@
1414
import time
1515
import psutil
1616

17-
def wait_timeout(proc, seconds):
18-
"""Wait for a process to finish, or raise exception after timeout"""
19-
start = time.time()
20-
end = start + seconds
21-
interval = min(seconds / 1000.0, .25)
22-
23-
while True:
24-
result = proc.poll()
25-
if result is not None:
26-
return result
27-
if time.time() >= end:
28-
raise RuntimeError("Process timed out")
29-
time.sleep(interval)
30-
3117
def kill(proc_pid):
3218
process = psutil.Process(proc_pid)
3319
for proc in process.children(recursive=True):
@@ -54,6 +40,67 @@ def cleanup_processes():
5440

5541
testpath = os.path.dirname(__file__)
5642

43+
from threading import Thread
44+
from Queue import Queue, Empty
45+
46+
class NonBlockingStreamReader:
47+
48+
def __init__(self, stream):
49+
'''
50+
stream: the stream to read from.
51+
Usually a process' stdout or stderr.
52+
'''
53+
54+
self._s = stream
55+
self._q = Queue()
56+
57+
def _populateQueue(stream, queue):
58+
'''
59+
Collect lines from 'stream' and put them in 'quque'.
60+
'''
61+
62+
while True:
63+
line = stream.readline()
64+
if line:
65+
queue.put(line)
66+
else:
67+
break
68+
69+
self._t = Thread(target = _populateQueue,
70+
args = (self._s, self._q))
71+
self._t.daemon = True
72+
self._t.start() #start collecting lines from the stream
73+
74+
def readline(self, timeout = None):
75+
try:
76+
return self._q.get(block = timeout is not None,
77+
timeout = timeout)
78+
except Empty:
79+
return None
80+
81+
class UnexpectedEndOfStream(Exception):
82+
pass
83+
84+
def wait_timeout(proc, seconds, verbose):
85+
"""Wait for a process to finish, or raise exception after timeout"""
86+
start = time.time()
87+
end = start + seconds
88+
interval = min(seconds / 1000.0, .25)
89+
90+
stdout = NonBlockingStreamReader(proc.stdout)
91+
while True:
92+
# Read text in verbose mode. Also sleep for a bit.
93+
nextline = stdout.readline(interval)
94+
if nextline and verbose:
95+
sys.stdout.write(nextline)
96+
sys.stdout.flush()
97+
98+
# Poll for end of text.
99+
result = proc.poll()
100+
if result is not None:
101+
return result
102+
if time.time() >= end:
103+
raise RuntimeError("Process timed out")
57104

58105
def wrap_fd(pipeout):
59106
# Prepare to pass to child process
@@ -67,7 +114,15 @@ def wrap_fd(pipeout):
67114

68115
def lets_run_a_test(name):
69116
sitl_args = ['dronekit-sitl', 'copter-3.3-rc5', '-I0', '-S', '--model', 'quad', '--home=-35.363261,149.165230,584,353']
117+
118+
speedup = os.environ.get('TEST_SPEEDUP', '1')
119+
rate = os.environ.get('TEST_RATE', '200')
120+
sitl_args += ['--speedup', str(speedup), '-r', str(rate)]
121+
122+
# Change CPU core affinity.
123+
# TODO change affinity on osx/linux
70124
if sys.platform == 'win32':
125+
# 0x14 = 0b1110 = all cores except cpu 1
71126
sitl = Popen(['start', '/affinity', '14', '/realtime', '/b', '/wait'] + sitl_args, shell=True, stdout=PIPE, stderr=PIPE)
72127
else:
73128
sitl = Popen(sitl_args, stdout=PIPE, stderr=PIPE)
@@ -104,36 +159,41 @@ def lets_run_a_test(name):
104159
sys.stdout.flush()
105160
sys.stderr.flush()
106161

107-
# APPVEYOR = SLOW
108-
timeout = 15*60 if sys.platform == 'win32' else 5*60
162+
mavproxy_verbose = os.environ.get('TEST_VERBOSE', '0') != '0'
163+
timeout = 5*60
164+
109165
try:
110-
p = Popen([sys.executable, '-m', 'MAVProxy.mavproxy', '--logfile=' + tempfile.mkstemp()[1], '--master=tcp:127.0.0.1:5760'], cwd=testpath, env=newenv, stdin=PIPE, stdout=PIPE)#, stderr=PIPE)
166+
p = Popen([sys.executable, '-m', 'MAVProxy.mavproxy', '--logfile=' + tempfile.mkstemp()[1], '--master=tcp:127.0.0.1:5760'],
167+
cwd=testpath, env=newenv, stdin=PIPE, stdout=PIPE,
168+
stderr=PIPE if not mavproxy_verbose else None)
111169
bg.append(p)
112170

171+
# TODO this sleep is only for us to waiting until
172+
# all parameters to be received; would prefer to
173+
# move this to testlib.py and happen asap
113174
while p.poll() == None:
114175
line = p.stdout.readline()
115-
sys.stdout.write(line)
116-
sys.stdout.flush()
176+
if mavproxy_verbose:
177+
sys.stdout.write(line)
178+
sys.stdout.flush()
117179
if 'parameters' in line:
118180
break
119181

120-
# TODO this sleep is only for us to waiting until
121-
# all parameters to be received; would prefer to
122-
# move this to testlib.py and happen asap
123182
time.sleep(3)
124-
p.stdin.write('module load droneapi.module.api\n')
183+
184+
# NOTE these are *very inappropriate settings*
185+
# to make on a real vehicle. They are leveraged
186+
# exclusively for simulation. Take heed!!!
125187
p.stdin.write('param set ARMING_CHECK 0\n')
188+
p.stdin.write('param set FS_THR_ENABLE 0\n')
189+
p.stdin.write('param set FS_GCS_ENABLE 0\n')
190+
p.stdin.write('param set EKF_CHECK_THRESH 0\n')
191+
192+
p.stdin.write('module load droneapi.module.api\n')
126193
p.stdin.write('api start testlib.py\n')
127194
p.stdin.flush()
128195

129-
while True:
130-
nextline = p.stdout.readline()
131-
if nextline == '' and p.poll() != None:
132-
break
133-
sys.stdout.write(nextline)
134-
sys.stdout.flush()
135-
136-
# wait_timeout(p, timeout)
196+
wait_timeout(p, timeout, mavproxy_verbose)
137197
except RuntimeError:
138198
kill(p.pid)
139199
p.returncode = 143
@@ -148,17 +208,31 @@ def lets_run_a_test(name):
148208
bg.remove(sitl)
149209

150210
if p.returncode != 0:
151-
print('[runner] ...aborting with dronekit error code ' + str(p.returncode))
152-
sys.stdout.flush()
153-
sys.stderr.flush()
154-
sys.exit(p.returncode)
155-
156-
print('[runner] ...success.')
157-
time.sleep(5)
211+
print('[runner] ...failed with dronekit error code ' + str(p.returncode))
212+
else:
213+
print('[runner] ...success.')
158214

159-
for i in os.listdir(testpath):
160-
if i.startswith('test_') and i.endswith('.py'):
161-
lets_run_a_test(i[:-3])
215+
sys.stdout.flush()
216+
sys.stderr.flush()
217+
time.sleep(5)
218+
return p.returncode
219+
220+
retry = int(os.environ.get('TEST_RETRY', '1'))
221+
filelist = sys.argv[1:] if len(sys.argv) > 1 else os.listdir(testpath)
222+
for path in filelist:
223+
assert os.path.isfile(os.path.join(testpath, path)), '"%s" is not a valid test file' % (path,)
224+
if path.startswith('test_') and path.endswith('.py'):
225+
name = path[:-3]
226+
i = retry
227+
while True:
228+
ret = lets_run_a_test(name)
229+
if ret == 0:
230+
break
231+
i = i - 1
232+
if i == 0:
233+
print('[runner] aborting after failed test.')
234+
sys.exit(ret)
235+
print('[runner] retrying %s %s more times' % (name, i, ))
162236

163237
print('[runner] finished.')
164238
sys.stdout.flush()

tests/sitl/test_goto.py

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
simple_goto.py: GUIDED mode "simple goto" example (Copter Only)
3+
4+
The example demonstrates how to arm and takeoff in Copter and how to navigate to
5+
points using Vehicle.commands.goto.
6+
7+
Full documentation is provided at http://python.dronekit.io/examples/simple_goto.html
8+
"""
9+
10+
import time
11+
from droneapi.lib import VehicleMode, Location
12+
from pymavlink import mavutil
13+
from testlib import assert_equals
14+
15+
def test_goto(local_connect):
16+
api = local_connect()
17+
vehicle = api.get_vehicles()[0]
18+
19+
def arm_and_takeoff(aTargetAltitude):
20+
"""
21+
Arms vehicle and fly to aTargetAltitude.
22+
"""
23+
24+
print "Basic pre-arm checks"
25+
# Don't let the user try to fly autopilot is booting
26+
if vehicle.mode.name == "INITIALISING":
27+
print "Waiting for vehicle to initialise"
28+
time.sleep(1)
29+
while vehicle.gps_0.fix_type < 2:
30+
print "Waiting for GPS...:", vehicle.gps_0.fix_type
31+
time.sleep(1)
32+
33+
print "Arming motors"
34+
# Copter should arm in GUIDED mode
35+
vehicle.mode = VehicleMode("GUIDED")
36+
vehicle.flush()
37+
38+
i = 60
39+
while not api.exit and vehicle.mode.name != 'GUIDED' and i > 0:
40+
print " Waiting for guided %s seconds..." % (i,)
41+
time.sleep(1)
42+
i = i - 1
43+
44+
vehicle.armed = True
45+
vehicle.flush()
46+
47+
i = 60
48+
while not api.exit and not vehicle.armed and vehicle.mode.name == 'GUIDED' and i > 0:
49+
print " Waiting for arming %s seconds..." % (i,)
50+
time.sleep(1)
51+
i = i - 1
52+
53+
# Failure will result in arming but immediately landing
54+
assert vehicle.armed
55+
assert_equals(vehicle.mode.name, 'GUIDED')
56+
57+
print "Taking off!"
58+
vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude
59+
vehicle.flush()
60+
61+
# Wait until the vehicle reaches a safe height before
62+
# processing the goto (otherwise the command after
63+
# Vehicle.commands.takeoff will execute immediately).
64+
while not api.exit:
65+
print " Altitude: ", vehicle.location.alt
66+
# Test for altitude just below target, in case of undershoot.
67+
if vehicle.location.alt >= aTargetAltitude * 0.95:
68+
print "Reached target altitude"
69+
break;
70+
71+
assert_equals(vehicle.mode.name, 'GUIDED')
72+
time.sleep(1)
73+
74+
arm_and_takeoff(10)
75+
76+
print "Going to first point..."
77+
point1 = Location(-35.361354, 149.165218, 20, is_relative=True)
78+
vehicle.commands.goto(point1)
79+
vehicle.flush()
80+
81+
# sleep so we can see the change in map
82+
time.sleep(3)
83+
84+
print "Going to second point..."
85+
point2 = Location(-35.363244, 149.168801, 20, is_relative=True)
86+
vehicle.commands.goto(point2)
87+
vehicle.flush()
88+
89+
# sleep so we can see the change in map
90+
time.sleep(3)
91+
92+
print "Returning to Launch"
93+
vehicle.mode = VehicleMode("RTL")
94+
vehicle.flush()

0 commit comments

Comments
 (0)