2020-03-11 14:17:04 -03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import queue
|
|
|
|
import time
|
|
|
|
import os
|
|
|
|
import atexit
|
|
|
|
import subprocess
|
2020-09-01 10:15:36 -03:00
|
|
|
import shutil
|
2020-03-11 14:17:04 -03:00
|
|
|
import threading
|
|
|
|
import errno
|
2020-03-31 05:55:32 -03:00
|
|
|
from typing import Any, Dict, List, TextIO, Optional
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2023-01-15 12:36:12 -04:00
|
|
|
PX4_SITL_GAZEBO_PATH = "Tools/simulation/gazebo-classic/sitl_gazebo-classic"
|
|
|
|
|
|
|
|
PX4_GAZEBO_MODELS = PX4_SITL_GAZEBO_PATH + "/models"
|
|
|
|
PX4_GAZEBO_WORLDS = PX4_SITL_GAZEBO_PATH + "/worlds"
|
2022-08-22 12:00:03 -03:00
|
|
|
|
2020-03-11 14:17:04 -03:00
|
|
|
|
|
|
|
class Runner:
|
2020-03-13 06:02:58 -03:00
|
|
|
def __init__(self,
|
|
|
|
log_dir: str,
|
2020-03-16 12:05:12 -03:00
|
|
|
model: str,
|
|
|
|
case: str,
|
2020-03-13 06:02:58 -03:00
|
|
|
verbose: bool):
|
2020-03-11 14:17:04 -03:00
|
|
|
self.name = ""
|
|
|
|
self.cmd = ""
|
2020-03-13 05:18:10 -03:00
|
|
|
self.cwd = ""
|
|
|
|
self.args: List[str]
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env: Dict[str, str] = os.environ.copy()
|
2020-03-16 12:05:12 -03:00
|
|
|
self.model = model
|
|
|
|
self.case = case
|
2020-03-11 14:17:04 -03:00
|
|
|
self.log_filename = ""
|
2020-03-13 06:02:58 -03:00
|
|
|
self.log_fd: TextIO
|
2020-03-11 14:17:04 -03:00
|
|
|
self.verbose = verbose
|
2020-03-13 06:02:58 -03:00
|
|
|
self.output_queue: queue.Queue[str] = queue.Queue()
|
2020-03-11 14:17:04 -03:00
|
|
|
self.start_time = time.time()
|
2020-03-16 12:05:12 -03:00
|
|
|
self.log_dir = log_dir
|
|
|
|
self.log_filename = ""
|
2020-03-31 05:55:32 -03:00
|
|
|
self.stop_thread: Any[threading.Event] = None
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2020-03-16 15:26:05 -03:00
|
|
|
def set_log_filename(self, log_filename: str) -> None:
|
|
|
|
self.log_filename = log_filename
|
2020-03-16 12:05:12 -03:00
|
|
|
|
|
|
|
def get_log_filename(self) -> str:
|
|
|
|
return self.log_filename
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def start(self) -> None:
|
2020-03-11 14:17:04 -03:00
|
|
|
if self.verbose:
|
|
|
|
print("Running: {}".format(" ".join([self.cmd] + self.args)))
|
|
|
|
|
|
|
|
atexit.register(self.stop)
|
|
|
|
|
2020-03-16 12:05:12 -03:00
|
|
|
if self.verbose:
|
|
|
|
print("Logging to {}".format(self.log_filename))
|
2020-03-11 14:17:04 -03:00
|
|
|
self.log_fd = open(self.log_filename, 'w')
|
|
|
|
|
|
|
|
self.process = subprocess.Popen(
|
|
|
|
[self.cmd] + self.args,
|
|
|
|
cwd=self.cwd,
|
|
|
|
env=self.env,
|
|
|
|
stdout=subprocess.PIPE,
|
2020-05-20 09:23:52 -03:00
|
|
|
stderr=subprocess.STDOUT,
|
2020-03-11 14:17:04 -03:00
|
|
|
universal_newlines=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self.stop_thread = threading.Event()
|
|
|
|
self.thread = threading.Thread(target=self.process_output)
|
|
|
|
self.thread.start()
|
|
|
|
|
2021-11-26 11:23:39 -04:00
|
|
|
def has_started_ok(self) -> bool:
|
|
|
|
return True
|
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def process_output(self) -> None:
|
|
|
|
assert self.process.stdout is not None
|
2021-12-20 03:16:27 -04:00
|
|
|
while True:
|
|
|
|
line = self.process.stdout.readline()
|
|
|
|
if not line and \
|
|
|
|
(self.stop_thread.is_set() or self.poll is not None):
|
|
|
|
break
|
|
|
|
if not line or line == "\n":
|
|
|
|
continue
|
2021-12-22 09:40:24 -04:00
|
|
|
line = self.add_prefix(10, self.name, line)
|
2021-12-20 03:16:27 -04:00
|
|
|
self.output_queue.put(line)
|
|
|
|
self.log_fd.write(line)
|
|
|
|
self.log_fd.flush()
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2021-12-22 09:40:24 -04:00
|
|
|
def add_prefix(self, width: int, name: str, text: str) -> str:
|
|
|
|
return "[" + self.seconds() + "|" + name.ljust(width) + "] " + text
|
|
|
|
|
|
|
|
def seconds(self) -> str:
|
|
|
|
dt = time.time() - self.start_time
|
|
|
|
return "{: 8.03f}".format(dt)
|
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def poll(self) -> Optional[int]:
|
2020-03-11 14:17:04 -03:00
|
|
|
return self.process.poll()
|
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def wait(self, timeout_min: float) -> Optional[int]:
|
2020-03-11 14:17:04 -03:00
|
|
|
try:
|
|
|
|
return self.process.wait(timeout=timeout_min*60)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
print("Timeout of {} min{} reached, stopping...".
|
|
|
|
format(timeout_min, "s" if timeout_min > 1 else ""))
|
|
|
|
self.stop()
|
|
|
|
print("stopped.")
|
|
|
|
return errno.ETIMEDOUT
|
|
|
|
|
2020-04-03 03:47:41 -03:00
|
|
|
def get_output_line(self) -> Optional[str]:
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return self.output_queue.get(block=True, timeout=0.1)
|
|
|
|
except queue.Empty:
|
|
|
|
return None
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def stop(self) -> int:
|
2020-03-11 14:17:04 -03:00
|
|
|
atexit.unregister(self.stop)
|
|
|
|
|
2020-03-31 05:55:32 -03:00
|
|
|
if not self.stop_thread:
|
|
|
|
return 0
|
|
|
|
|
2020-03-11 14:17:04 -03:00
|
|
|
returncode = self.process.poll()
|
|
|
|
if returncode is None:
|
|
|
|
|
|
|
|
if self.verbose:
|
2021-12-02 10:38:18 -04:00
|
|
|
print("Terminating {}".format(self.name))
|
2020-03-11 14:17:04 -03:00
|
|
|
self.process.terminate()
|
|
|
|
|
|
|
|
try:
|
|
|
|
returncode = self.process.wait(timeout=1)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if returncode is None:
|
|
|
|
if self.verbose:
|
2021-12-02 10:38:18 -04:00
|
|
|
print("Killing {}".format(self.name))
|
2020-03-11 14:17:04 -03:00
|
|
|
self.process.kill()
|
|
|
|
returncode = self.process.poll()
|
|
|
|
|
|
|
|
if self.verbose:
|
|
|
|
print("{} exited with {}".format(
|
2021-12-02 10:38:18 -04:00
|
|
|
self.name, self.process.returncode))
|
2020-03-11 14:17:04 -03:00
|
|
|
|
2020-05-20 08:22:26 -03:00
|
|
|
self.stop_thread.set()
|
2020-03-11 14:17:04 -03:00
|
|
|
self.thread.join()
|
|
|
|
self.log_fd.close()
|
|
|
|
|
|
|
|
return self.process.returncode
|
|
|
|
|
2020-03-13 06:02:58 -03:00
|
|
|
def time_elapsed_s(self) -> float:
|
2020-03-11 14:17:04 -03:00
|
|
|
return time.time() - self.start_time
|
|
|
|
|
|
|
|
|
|
|
|
class Px4Runner(Runner):
|
2020-03-13 05:18:10 -03:00
|
|
|
def __init__(self, workspace_dir: str, log_dir: str,
|
2020-03-16 12:05:12 -03:00
|
|
|
model: str, case: str, speed_factor: float,
|
2021-07-06 10:38:13 -03:00
|
|
|
debugger: str, verbose: bool, build_dir: str):
|
2020-03-16 12:05:12 -03:00
|
|
|
super().__init__(log_dir, model, case, verbose)
|
2020-03-11 14:17:04 -03:00
|
|
|
self.name = "px4"
|
2021-07-06 10:38:13 -03:00
|
|
|
self.cmd = os.path.join(workspace_dir, build_dir, "bin/px4")
|
|
|
|
self.cwd = os.path.join(workspace_dir, build_dir,
|
|
|
|
"tmp_mavsdk_tests/rootfs")
|
2020-03-11 14:17:04 -03:00
|
|
|
self.args = [
|
2021-07-06 10:38:13 -03:00
|
|
|
os.path.join(workspace_dir, build_dir, "etc"),
|
2020-03-11 14:17:04 -03:00
|
|
|
"-s",
|
|
|
|
"etc/init.d-posix/rcS",
|
|
|
|
"-t",
|
2021-07-06 10:38:13 -03:00
|
|
|
os.path.join(workspace_dir, "test_data"),
|
2020-03-11 14:17:04 -03:00
|
|
|
"-d"
|
|
|
|
]
|
2023-01-15 12:36:12 -04:00
|
|
|
self.env["PX4_SIM_MODEL"] = "gazebo-classic_" + self.model
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["PX4_SIM_SPEED_FACTOR"] = str(speed_factor)
|
2020-03-11 14:17:04 -03:00
|
|
|
self.debugger = debugger
|
2020-09-01 10:15:36 -03:00
|
|
|
self.clear_rootfs()
|
|
|
|
self.create_rootfs()
|
2020-03-11 14:17:04 -03:00
|
|
|
|
|
|
|
if not self.debugger:
|
|
|
|
pass
|
|
|
|
elif self.debugger == "valgrind":
|
|
|
|
self.args = ["--track-origins=yes", "--leak-check=full", "-v",
|
|
|
|
self.cmd] + self.args
|
|
|
|
self.cmd = "valgrind"
|
|
|
|
elif self.debugger == "callgrind":
|
|
|
|
self.args = ["--tool=callgrind", "-v", self.cmd] + self.args
|
|
|
|
self.cmd = "valgrind"
|
|
|
|
elif self.debugger == "gdb":
|
|
|
|
self.args = ["--args", self.cmd] + self.args
|
|
|
|
self.cmd = "gdb"
|
|
|
|
else:
|
|
|
|
print("Using custom debugger " + self.debugger)
|
|
|
|
self.args = [self.cmd] + self.args
|
|
|
|
self.cmd = self.debugger
|
|
|
|
|
2020-09-01 10:15:36 -03:00
|
|
|
def clear_rootfs(self) -> None:
|
|
|
|
rootfs_path = self.cwd
|
|
|
|
if self.verbose:
|
2021-02-11 04:59:04 -04:00
|
|
|
print("Clearing rootfs (except logs): {}".format(rootfs_path))
|
2021-02-11 06:18:14 -04:00
|
|
|
if os.path.isdir(rootfs_path):
|
|
|
|
for item in os.listdir(rootfs_path):
|
|
|
|
if item == 'log':
|
|
|
|
continue
|
|
|
|
path = os.path.join(rootfs_path, item)
|
|
|
|
if os.path.isfile(path) or os.path.islink(path):
|
|
|
|
os.remove(path)
|
|
|
|
else:
|
|
|
|
shutil.rmtree(path)
|
2020-09-01 10:15:36 -03:00
|
|
|
|
|
|
|
def create_rootfs(self) -> None:
|
|
|
|
rootfs_path = self.cwd
|
|
|
|
if self.verbose:
|
|
|
|
print("Creating rootfs: {}".format(rootfs_path))
|
2021-02-11 04:59:04 -04:00
|
|
|
try:
|
|
|
|
os.makedirs(rootfs_path)
|
|
|
|
except FileExistsError:
|
|
|
|
pass
|
2020-09-01 10:15:36 -03:00
|
|
|
|
2020-03-11 14:17:04 -03:00
|
|
|
|
|
|
|
class GzserverRunner(Runner):
|
2020-03-13 06:02:58 -03:00
|
|
|
def __init__(self,
|
|
|
|
workspace_dir: str,
|
|
|
|
log_dir: str,
|
2020-03-16 12:05:12 -03:00
|
|
|
model: str,
|
|
|
|
case: str,
|
2020-03-13 06:02:58 -03:00
|
|
|
speed_factor: float,
|
2021-07-06 10:38:13 -03:00
|
|
|
verbose: bool,
|
2023-07-19 06:29:28 -03:00
|
|
|
build_dir: str,
|
|
|
|
world_name: str):
|
2020-03-16 12:05:12 -03:00
|
|
|
super().__init__(log_dir, model, case, verbose)
|
2020-03-11 14:17:04 -03:00
|
|
|
self.name = "gzserver"
|
2020-03-13 05:18:10 -03:00
|
|
|
self.cwd = workspace_dir
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["GAZEBO_PLUGIN_PATH"] = \
|
2023-01-15 12:36:12 -04:00
|
|
|
os.path.join(workspace_dir, build_dir, "build_gazebo-classic")
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["GAZEBO_MODEL_PATH"] = \
|
2022-08-22 12:00:03 -03:00
|
|
|
os.path.join(workspace_dir, PX4_GAZEBO_MODELS)
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["PX4_SIM_SPEED_FACTOR"] = str(speed_factor)
|
2021-12-02 10:38:18 -04:00
|
|
|
self.cmd = "stdbuf"
|
|
|
|
self.args = ["-o0", "-e0", "gzserver", "--verbose",
|
|
|
|
os.path.join(workspace_dir,
|
2022-08-22 12:00:03 -03:00
|
|
|
PX4_GAZEBO_WORLDS,
|
2023-07-19 06:29:28 -03:00
|
|
|
world_name)]
|
2021-12-02 10:38:18 -04:00
|
|
|
|
|
|
|
def has_started_ok(self) -> bool:
|
|
|
|
# Wait until gzerver has started and connected to gazebo master.
|
|
|
|
timeout_s = 20
|
|
|
|
steps = 10
|
|
|
|
for step in range(steps):
|
|
|
|
with open(self.log_filename, 'r') as f:
|
|
|
|
for line in f.readlines():
|
|
|
|
if 'Connected to gazebo master' in line:
|
|
|
|
return True
|
|
|
|
time.sleep(float(timeout_s)/float(steps))
|
|
|
|
|
|
|
|
print("gzserver did not connect within {}s"
|
|
|
|
.format(timeout_s))
|
|
|
|
return False
|
2020-03-20 10:52:11 -03:00
|
|
|
|
|
|
|
|
|
|
|
class GzmodelspawnRunner(Runner):
|
|
|
|
def __init__(self,
|
|
|
|
workspace_dir: str,
|
|
|
|
log_dir: str,
|
|
|
|
model: str,
|
|
|
|
case: str,
|
2021-07-06 10:38:13 -03:00
|
|
|
verbose: bool,
|
|
|
|
build_dir: str):
|
2020-03-20 10:52:11 -03:00
|
|
|
super().__init__(log_dir, model, case, verbose)
|
|
|
|
self.name = "gzmodelspawn"
|
|
|
|
self.cwd = workspace_dir
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["GAZEBO_PLUGIN_PATH"] = \
|
2023-01-15 12:36:12 -04:00
|
|
|
os.path.join(workspace_dir, build_dir, "build_gazebo-classic")
|
2020-07-29 09:41:25 -03:00
|
|
|
self.env["GAZEBO_MODEL_PATH"] = \
|
2022-08-22 12:00:03 -03:00
|
|
|
os.path.join(workspace_dir, PX4_GAZEBO_MODELS)
|
2020-03-20 10:52:11 -03:00
|
|
|
self.cmd = "gz"
|
2020-09-29 10:44:58 -03:00
|
|
|
|
2021-07-06 10:38:13 -03:00
|
|
|
if os.path.isfile(os.path.join(workspace_dir,
|
2022-08-22 12:00:03 -03:00
|
|
|
PX4_GAZEBO_MODELS,
|
2021-07-06 10:38:13 -03:00
|
|
|
self.model, self.model + ".sdf")):
|
2022-08-22 12:00:03 -03:00
|
|
|
|
2021-07-06 10:38:13 -03:00
|
|
|
model_path = os.path.join(workspace_dir,
|
2022-08-22 12:00:03 -03:00
|
|
|
PX4_GAZEBO_MODELS,
|
2021-07-06 10:38:13 -03:00
|
|
|
self.model, self.model + ".sdf")
|
2022-08-22 12:00:03 -03:00
|
|
|
|
2020-09-29 10:44:58 -03:00
|
|
|
else:
|
|
|
|
raise Exception("Model not found")
|
|
|
|
|
2021-12-02 10:38:18 -04:00
|
|
|
self.cmd = "stdbuf"
|
|
|
|
self.args = ["-o0", "-e0",
|
|
|
|
"gz", "model",
|
|
|
|
"--verbose",
|
|
|
|
"--spawn-file", model_path,
|
2020-03-20 10:52:11 -03:00
|
|
|
"--model-name", self.model,
|
|
|
|
"-x", "1.01", "-y", "0.98", "-z", "0.83"]
|
2020-03-18 09:31:18 -03:00
|
|
|
|
2021-11-26 11:23:39 -04:00
|
|
|
def has_started_ok(self) -> bool:
|
|
|
|
# The problem is that sometimes gzserver does not seem to start
|
|
|
|
# quickly enough and gz model spawn fails with the error:
|
|
|
|
# "An instance of Gazebo is not running." but still returns 0
|
|
|
|
# as a result.
|
|
|
|
# We work around this by trying to start and then check whether
|
2021-12-02 10:38:18 -04:00
|
|
|
# using has_started_ok() whether it was successful or not.
|
|
|
|
timeout_s = 20
|
2021-11-26 11:23:39 -04:00
|
|
|
steps = 10
|
2021-12-02 10:38:18 -04:00
|
|
|
for _ in range(steps):
|
2021-11-26 11:23:39 -04:00
|
|
|
returncode = self.process.poll()
|
|
|
|
if returncode is None:
|
|
|
|
time.sleep(float(timeout_s)/float(steps))
|
|
|
|
continue
|
|
|
|
|
|
|
|
with open(self.log_filename, 'r') as f:
|
|
|
|
for line in f.readlines():
|
|
|
|
if 'An instance of Gazebo is not running' in line:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2021-12-02 10:38:18 -04:00
|
|
|
print("gzmodelspawn did not return within {}s"
|
|
|
|
.format(timeout_s))
|
2021-11-26 11:23:39 -04:00
|
|
|
return False
|
|
|
|
|
2020-03-11 14:17:04 -03:00
|
|
|
|
|
|
|
class GzclientRunner(Runner):
|
2020-03-13 06:02:58 -03:00
|
|
|
def __init__(self,
|
|
|
|
workspace_dir: str,
|
|
|
|
log_dir: str,
|
2020-03-16 12:05:12 -03:00
|
|
|
model: str,
|
|
|
|
case: str,
|
2020-03-13 06:02:58 -03:00
|
|
|
verbose: bool):
|
2020-03-16 12:05:12 -03:00
|
|
|
super().__init__(log_dir, model, case, verbose)
|
2020-03-11 14:17:04 -03:00
|
|
|
self.name = "gzclient"
|
2020-03-13 05:18:10 -03:00
|
|
|
self.cwd = workspace_dir
|
2020-07-09 06:40:15 -03:00
|
|
|
self.env = dict(os.environ, **{
|
2022-08-22 12:00:03 -03:00
|
|
|
"GAZEBO_MODEL_PATH":
|
|
|
|
os.path.join(workspace_dir, PX4_GAZEBO_MODELS)})
|
2020-03-11 14:17:04 -03:00
|
|
|
self.cmd = "gzclient"
|
|
|
|
self.args = ["--verbose"]
|
|
|
|
|
|
|
|
|
|
|
|
class TestRunner(Runner):
|
2020-03-13 06:02:58 -03:00
|
|
|
def __init__(self,
|
|
|
|
workspace_dir: str,
|
|
|
|
log_dir: str,
|
2020-03-16 12:05:12 -03:00
|
|
|
model: str,
|
|
|
|
case: str,
|
2020-03-13 06:02:58 -03:00
|
|
|
mavlink_connection: str,
|
2021-03-17 05:38:37 -03:00
|
|
|
speed_factor: float,
|
2021-07-06 10:38:13 -03:00
|
|
|
verbose: bool,
|
|
|
|
build_dir: str):
|
2020-03-16 12:05:12 -03:00
|
|
|
super().__init__(log_dir, model, case, verbose)
|
2020-03-18 12:11:27 -03:00
|
|
|
self.name = "mavsdk_tests"
|
2020-03-13 05:18:10 -03:00
|
|
|
self.cwd = workspace_dir
|
2021-12-22 07:59:20 -04:00
|
|
|
self.cmd = "nice"
|
|
|
|
self.args = ["-5",
|
|
|
|
os.path.join(
|
|
|
|
workspace_dir,
|
|
|
|
build_dir,
|
|
|
|
"mavsdk_tests/mavsdk_tests"),
|
|
|
|
"--url", mavlink_connection,
|
2021-03-17 05:38:37 -03:00
|
|
|
"--speed-factor", str(speed_factor),
|
|
|
|
case]
|