Files
paparazzi/tests/modules/test_modules.py
T
Gautier Hattenberger 494e3f3ad9 [tests] add a compilation test node to modules (#2653)
When adding a test node to a makefile section, with required compilation
flags, include and other options, all the files (not arch dependent
files) can be compiled with a TAP compatible program, included in the
standard tests of the CI servers.
Not all module's XML files are converted, but a large part of the most
important parts are already covered. More will be added later. The
number of tested airframes (full compilation of all targets) have been
reduced to speed the CI compile time but still covers the relevant
architecture and boards.
The main benefit is that the overall coverage is already better than
before as previous test aircraft were compiling more or less the same
part of the airborne code, while this new mechanism is more efficient to
test modules not included in any config.
2021-02-08 17:24:19 +01:00

293 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Copyright (C) 2021 Fabien Bonneval <fabien.bonneval@enac.fr>
# Gautier Hattenberger <gautier.hattenberger@enac.fr>
#
# This file is part of paparazzi.
#
# paparazzi is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# paparazzi is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with paparazzi; see the file COPYING. If not, see
# <http://www.gnu.org/licenses/>.
#
from lxml import etree
from typing import List, Optional, Tuple, Dict
import re
from os import path, getenv
import os
import argparse
import subprocess
import shlex
import TAP
PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../')))
ARCHS = ["chibios", "linux", "sim", "stm32"]
ALT_DIRS = ["../../var/share", "../ext"]
GCC_PARAMS: List[str] = ["-c", "-o", "/dev/null", "-W", "-Wall"]
OTHER_PARAMS: List[str] = [
"-I./", "-I../include",
"-I../../var/include",
"-I../../tests/modules",
"-Imodules", "-DPERIODIC_TELEMETRY",
"-DDOWNLINK",
"-DBOARD_CONFIG=\"../../tests/modules/dummy.h\""]
class Test:
def __init__(self, tst, files, files_arch, test_name):
self.configure_regex = re.compile(r"(\$\([a-zA-Z_][a-zA-Z_0-9]*\))")
self.files = files # type: List[str]
self.files_arch = files_arch # type: List[str]
self.firmware = None # type: Optional[str]
self.archs = [] # type: List[str]
self.defines = [] # type: List[Tuple[str,str, str]]
self.configures = {} # type: Dict[str:str]
self.includes = [] # type: List[str]
self.shells = [] # type: List[str]
self.test_name = test_name
self.parse(tst)
def parse(self, tst):
self.firmware = tst.attrib.get("firmware")
archs = tst.attrib.get("arch")
if archs is None:
# self.archs = ARCHS
self.archs = []
else:
self.archs = archs.split("|")
if len(self.archs) == 1:
if self.archs[0] == "":
self.archs = []
elif self.archs[0][0] == "!":
excluded = self.archs[0][1:]
self.archs = ARCHS
self.archs.remove(excluded)
print(f"archs : {self.archs}")
for define in tst.findall("define"):
def_name = define.attrib["name"]
def_val = define.attrib.get("value")
def_type = define.attrib.get("type")
self.defines.append((def_name, def_val, def_type))
for configure in tst.findall("configure"):
conf_name = configure.attrib["name"]
conf_value = configure.attrib["value"]
self.configures[conf_name] = conf_value
for include in tst.findall("include"):
self.includes.append(include.attrib["name"])
for shell in tst.findall("shell"):
self.shells.append(shell.attrib["cmd"])
if self.firmware is not None:
self.configures["SRC_FIRMWARE"] = f"firmwares/{self.firmware}"
self.includes.append(f"firmwares/{self.firmware}")
def substitute_configures(self):
def substitute(string: str):
if string is None:
return None
for m in re.findall(self.configure_regex, string):
if m[2:-1] in self.configures.keys():
string = string.replace(m, self.configures[m[2:-1]])
return string
self.files = map(substitute, self.files)
self.files_arch = map(substitute, self.files_arch)
self.defines = map(lambda x: (substitute(x[0]), substitute(x[1]), x[2]), self.defines)
self.includes = map(substitute, self.includes)
def make_commands(self) -> List[List[str]]:
self.substitute_configures()
self.files = list(self.files)
self.files_arch = list(self.files_arch)
gcc = "gcc"
for f in self.files + self.files_arch:
if re.match("cpp$", f) is not None:
gcc = "g++"
defines = list(map(self.define_str, self.defines))
includes = list((map(self.include_str, self.includes)))
shells = []
for shell in self.shells:
shell_args = shlex.split(shell)
p = subprocess.run(shell_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
if p.returncode == 0:
shells += shlex.split(p.stdout)
else:
print(p.stderr)
return [["exit", "1"]]
cmds = []
for file in self.files:
# build with the "test" arch.
arch_include = f"-I../../tests/modules/test_arch"
cmd_args = [gcc] + GCC_PARAMS + [file] + OTHER_PARAMS + defines + includes + [arch_include] + shells
cmds.append(cmd_args)
for file in self.files_arch:
for arch in self.archs:
file_path = f"arch/{arch}/{file}"
arch_include = f"-Iarch/{arch}"
cmd_args = [gcc] + GCC_PARAMS + [file_path] + OTHER_PARAMS + defines + includes + [arch_include] + shells
# TODO: uncomment to build <file_arch/> files.
# cmds.append(cmd_args)
return cmds
def run(self):
cmds = self.make_commands()
diagnostic = []
returncode = 0
for cmd_args in cmds:
p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
if p.stderr != "":
# diagnostic.append(str(cmd_args))
diagnostic.append(p.stderr)
if p.returncode != 0:
returncode = 1
return returncode, self.test_name, diagnostic
@staticmethod
def define_str(define: Tuple[str, Optional[str], Optional[str]]) -> str:
name, value, _type = define
if value is not None:
if _type is not None and _type == "string":
return f'-D{name}="{value}"'
else:
return f"-D{name}={value}"
else:
return f"-D{name}"
@staticmethod
def include_str(name: str) -> str:
return f"-I{name}"
def __repr__(self):
res = ""
res += "Test{\n"
res += "\tdefines: "
for define in self.defines:
res += define[0] + "=" + (define[1] if define[1] is not None else "None") + " "
res += "\n\tconfigures: "
for key, value in self.configures.items():
res += key + "=" + (value if value is not None else "None") + " "
res += "\n\tincludes: " + " ".join(self.includes)
res += "\n\tfiles: "
res += " ".join(self.files)
res += "}"
return res
class Module:
def __init__(self, filename):
m_tree = etree.parse(filename)
mod_elt = m_tree.getroot()
self.name = mod_elt.attrib["name"]
# if dir is not specified, the name of the module is used as default directory name
self.dir = mod_elt.attrib.get("dir", self.name)
self.tests = []
for mkf in mod_elt.findall("makefile"):
files = self.get_files(mkf)
files_arch = self.get_files_arch(mkf)
for i, tst in enumerate(mkf.findall("test")):
test = Test(tst, files, files_arch, f"{self.name}_{i}")
self.tests.append(test)
def get_files(self, mkf):
files = []
for file in mkf.findall("file"):
name = file.attrib["name"]
dir = file.attrib.get("dir")
if dir is None:
file_path = "/".join(["modules", self.dir, name])
else:
file_path = "/".join([dir, name])
if not path.exists(file_path):
for alt_dir in ALT_DIRS:
alt_path = f"{alt_dir}/{file_path}"
if path.exists(alt_path):
file_path = alt_path
break
files.append(file_path)
return files
def get_files_arch(self, mkf):
files = []
for file in mkf.findall("file_arch"):
name = file.attrib["name"]
dir = file.attrib.get("dir")
if dir is None:
file_path = "/".join(["modules", self.dir, name])
else:
file_path = "/".join([dir, name])
files.append(file_path)
return files
def get_modules():
files = os.listdir(PPRZ_HOME+"/conf/modules")
def is_xml(filename):
filename, extension = os.path.splitext(filename)
return extension == ".xml"
files = filter(is_xml, files)
return files
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="test modules")
parser.add_argument('--file', '-f', default=None, help="module to be tested")
parser.add_argument('-w', action='store_false')
args = parser.parse_args()
def is_xml(filename):
filename, extension = os.path.splitext(filename)
return extension == ".xml"
if args.file is None:
files = os.listdir(PPRZ_HOME+"/conf/modules")
files = map(lambda name: PPRZ_HOME+"/conf/modules/"+name, files)
else:
files = [args.file]
files = filter(is_xml, files)
all_tests = []
os.chdir(f"{PPRZ_HOME}/sw/airborne")
for f in files:
mod = Module(f)
all_tests += mod.tests
ok = TAP.Builder.create(len(all_tests)).ok
for test in all_tests:
returncode, comment, diagnotics = test.run()
ok(not returncode, comment)
if args.w or returncode:
for d in diagnotics:
lines = d.split("\n")
for line in lines:
print(f"# {line}")