You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
12 KiB
355 lines
12 KiB
#!/usr/bin/env python3 |
|
|
|
import queue |
|
import time |
|
import os |
|
import atexit |
|
import subprocess |
|
import shutil |
|
import threading |
|
import errno |
|
from typing import Any, Dict, List, TextIO, Optional |
|
|
|
|
|
class Runner: |
|
def __init__(self, |
|
log_dir: str, |
|
model: str, |
|
case: str, |
|
verbose: bool): |
|
self.name = "" |
|
self.cmd = "" |
|
self.cwd = "" |
|
self.args: List[str] |
|
self.env: Dict[str, str] = os.environ.copy() |
|
self.model = model |
|
self.case = case |
|
self.log_filename = "" |
|
self.log_fd: TextIO |
|
self.verbose = verbose |
|
self.output_queue: queue.Queue[str] = queue.Queue() |
|
self.start_time = time.time() |
|
self.log_dir = log_dir |
|
self.log_filename = "" |
|
self.stop_thread: Any[threading.Event] = None |
|
|
|
def set_log_filename(self, log_filename: str) -> None: |
|
self.log_filename = log_filename |
|
|
|
def get_log_filename(self) -> str: |
|
return self.log_filename |
|
|
|
def start(self) -> None: |
|
if self.verbose: |
|
print("Running: {}".format(" ".join([self.cmd] + self.args))) |
|
|
|
atexit.register(self.stop) |
|
|
|
if self.verbose: |
|
print("Logging to {}".format(self.log_filename)) |
|
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, |
|
stderr=subprocess.STDOUT, |
|
universal_newlines=True |
|
) |
|
|
|
self.stop_thread = threading.Event() |
|
self.thread = threading.Thread(target=self.process_output) |
|
self.thread.start() |
|
|
|
def has_started_ok(self) -> bool: |
|
return True |
|
|
|
def process_output(self) -> None: |
|
assert self.process.stdout is not None |
|
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 |
|
line = self.add_prefix(10, self.name, line) |
|
self.output_queue.put(line) |
|
self.log_fd.write(line) |
|
self.log_fd.flush() |
|
|
|
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) |
|
|
|
def poll(self) -> Optional[int]: |
|
return self.process.poll() |
|
|
|
def wait(self, timeout_min: float) -> Optional[int]: |
|
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 |
|
|
|
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 |
|
|
|
def stop(self) -> int: |
|
atexit.unregister(self.stop) |
|
|
|
if not self.stop_thread: |
|
return 0 |
|
|
|
returncode = self.process.poll() |
|
if returncode is None: |
|
|
|
if self.verbose: |
|
print("Terminating {}".format(self.name)) |
|
self.process.terminate() |
|
|
|
try: |
|
returncode = self.process.wait(timeout=1) |
|
except subprocess.TimeoutExpired: |
|
pass |
|
|
|
if returncode is None: |
|
if self.verbose: |
|
print("Killing {}".format(self.name)) |
|
self.process.kill() |
|
returncode = self.process.poll() |
|
|
|
if self.verbose: |
|
print("{} exited with {}".format( |
|
self.name, self.process.returncode)) |
|
|
|
self.stop_thread.set() |
|
self.thread.join() |
|
self.log_fd.close() |
|
|
|
return self.process.returncode |
|
|
|
def time_elapsed_s(self) -> float: |
|
return time.time() - self.start_time |
|
|
|
|
|
class Px4Runner(Runner): |
|
def __init__(self, workspace_dir: str, log_dir: str, |
|
model: str, case: str, speed_factor: float, |
|
debugger: str, verbose: bool, build_dir: str): |
|
super().__init__(log_dir, model, case, verbose) |
|
self.name = "px4" |
|
self.cmd = os.path.join(workspace_dir, build_dir, "bin/px4") |
|
self.cwd = os.path.join(workspace_dir, build_dir, |
|
"tmp_mavsdk_tests/rootfs") |
|
self.args = [ |
|
os.path.join(workspace_dir, build_dir, "etc"), |
|
"-s", |
|
"etc/init.d-posix/rcS", |
|
"-t", |
|
os.path.join(workspace_dir, "test_data"), |
|
"-d" |
|
] |
|
self.env["PX4_SIM_MODEL"] = self.model |
|
self.env["PX4_SIM_SPEED_FACTOR"] = str(speed_factor) |
|
self.debugger = debugger |
|
self.clear_rootfs() |
|
self.create_rootfs() |
|
|
|
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 |
|
|
|
def clear_rootfs(self) -> None: |
|
rootfs_path = self.cwd |
|
if self.verbose: |
|
print("Clearing rootfs (except logs): {}".format(rootfs_path)) |
|
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) |
|
|
|
def create_rootfs(self) -> None: |
|
rootfs_path = self.cwd |
|
if self.verbose: |
|
print("Creating rootfs: {}".format(rootfs_path)) |
|
try: |
|
os.makedirs(rootfs_path) |
|
except FileExistsError: |
|
pass |
|
|
|
|
|
class GzserverRunner(Runner): |
|
def __init__(self, |
|
workspace_dir: str, |
|
log_dir: str, |
|
model: str, |
|
case: str, |
|
speed_factor: float, |
|
verbose: bool, |
|
build_dir: str): |
|
super().__init__(log_dir, model, case, verbose) |
|
self.name = "gzserver" |
|
self.cwd = workspace_dir |
|
self.env["GAZEBO_PLUGIN_PATH"] = \ |
|
os.path.join(workspace_dir, build_dir, "build_gazebo") |
|
self.env["GAZEBO_MODEL_PATH"] = \ |
|
os.path.join(workspace_dir, "Tools/sitl_gazebo/models") |
|
self.env["PX4_SIM_SPEED_FACTOR"] = str(speed_factor) |
|
self.cmd = "stdbuf" |
|
self.args = ["-o0", "-e0", "gzserver", "--verbose", |
|
os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/worlds", |
|
"empty.world")] |
|
|
|
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 |
|
|
|
|
|
class GzmodelspawnRunner(Runner): |
|
def __init__(self, |
|
workspace_dir: str, |
|
log_dir: str, |
|
model: str, |
|
case: str, |
|
verbose: bool, |
|
build_dir: str): |
|
super().__init__(log_dir, model, case, verbose) |
|
self.name = "gzmodelspawn" |
|
self.cwd = workspace_dir |
|
self.env["GAZEBO_PLUGIN_PATH"] = \ |
|
os.path.join(workspace_dir, build_dir, "build_gazebo") |
|
self.env["GAZEBO_MODEL_PATH"] = \ |
|
os.path.join(workspace_dir, "Tools/sitl_gazebo/models") |
|
self.cmd = "gz" |
|
|
|
if os.path.isfile(os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/models", |
|
self.model, self.model + ".sdf")): |
|
model_path = os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/models", |
|
self.model, self.model + ".sdf") |
|
elif os.path.isfile(os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/models", |
|
self.model, self.model + "-gen.sdf")): |
|
model_path = os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/models", |
|
self.model, self.model + "-gen.sdf") |
|
else: |
|
raise Exception("Model not found") |
|
|
|
self.cmd = "stdbuf" |
|
self.args = ["-o0", "-e0", |
|
"gz", "model", |
|
"--verbose", |
|
"--spawn-file", model_path, |
|
"--model-name", self.model, |
|
"-x", "1.01", "-y", "0.98", "-z", "0.83"] |
|
|
|
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 |
|
# using has_started_ok() whether it was successful or not. |
|
timeout_s = 20 |
|
steps = 10 |
|
for _ in range(steps): |
|
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 |
|
|
|
print("gzmodelspawn did not return within {}s" |
|
.format(timeout_s)) |
|
return False |
|
|
|
|
|
class GzclientRunner(Runner): |
|
def __init__(self, |
|
workspace_dir: str, |
|
log_dir: str, |
|
model: str, |
|
case: str, |
|
verbose: bool): |
|
super().__init__(log_dir, model, case, verbose) |
|
self.name = "gzclient" |
|
self.cwd = workspace_dir |
|
self.env = dict(os.environ, **{ |
|
"GAZEBO_MODEL_PATH": os.path.join(workspace_dir, |
|
"Tools/sitl_gazebo/models")}) |
|
self.cmd = "gzclient" |
|
self.args = ["--verbose"] |
|
|
|
|
|
class TestRunner(Runner): |
|
def __init__(self, |
|
workspace_dir: str, |
|
log_dir: str, |
|
model: str, |
|
case: str, |
|
mavlink_connection: str, |
|
speed_factor: float, |
|
verbose: bool, |
|
build_dir: str): |
|
super().__init__(log_dir, model, case, verbose) |
|
self.name = "mavsdk_tests" |
|
self.cwd = workspace_dir |
|
self.cmd = "nice" |
|
self.args = ["-5", |
|
os.path.join( |
|
workspace_dir, |
|
build_dir, |
|
"mavsdk_tests/mavsdk_tests"), |
|
"--url", mavlink_connection, |
|
"--speed-factor", str(speed_factor), |
|
case]
|
|
|