Merge pull request #16905 from aabadie/pr/tools/compile_and_test_with_black
tools/compile_and_test_for_board: apply black automatic code formatter + add format checker in tox
This commit is contained in:
commit
1fc45d888b
@ -1,5 +1,6 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
"""
|
"""
|
||||||
This script handles building all applications and tests for one board and also
|
This script handles building all applications and tests for one board and also
|
||||||
execute tests if they are available.
|
execute tests if they are available.
|
||||||
@ -103,9 +104,9 @@ except ImportError:
|
|||||||
LOG_HANDLER = logging.StreamHandler()
|
LOG_HANDLER = logging.StreamHandler()
|
||||||
LOG_HANDLER.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
LOG_HANDLER.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
||||||
|
|
||||||
LOG_LEVELS = ('debug', 'info', 'warning', 'error', 'fatal', 'critical')
|
LOG_LEVELS = ("debug", "info", "warning", "error", "fatal", "critical")
|
||||||
|
|
||||||
MAKE = os.environ.get('MAKE', 'make')
|
MAKE = os.environ.get("MAKE", "make")
|
||||||
|
|
||||||
|
|
||||||
class ErrorInTest(Exception):
|
class ErrorInTest(Exception):
|
||||||
@ -114,6 +115,7 @@ class ErrorInTest(Exception):
|
|||||||
It contains the step that failed in 'message', the 'application' and the
|
It contains the step that failed in 'message', the 'application' and the
|
||||||
'errorfile' path to the execution error.
|
'errorfile' path to the execution error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message, application, errorfile):
|
def __init__(self, message, application, errorfile):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.application = application
|
self.application = application
|
||||||
@ -168,10 +170,10 @@ def apps_directories(riotdir, apps_dirs=None, apps_dirs_skip=None):
|
|||||||
|
|
||||||
def _riot_applications_dirs(riotdir):
|
def _riot_applications_dirs(riotdir):
|
||||||
"""Applications directories in the RIOT repository with relative path."""
|
"""Applications directories in the RIOT repository with relative path."""
|
||||||
cmd = [MAKE, 'info-applications']
|
cmd = [MAKE, "info-applications"]
|
||||||
|
|
||||||
out = subprocess.check_output(cmd, cwd=riotdir)
|
out = subprocess.check_output(cmd, cwd=riotdir)
|
||||||
out = out.decode('utf-8', errors='replace')
|
out = out.decode("utf-8", errors="replace")
|
||||||
return out.split()
|
return out.split()
|
||||||
|
|
||||||
|
|
||||||
@ -181,9 +183,9 @@ def check_is_board(riotdir, board):
|
|||||||
:raises ValueError: on invalid board
|
:raises ValueError: on invalid board
|
||||||
:returns: board name
|
:returns: board name
|
||||||
"""
|
"""
|
||||||
if board == 'common':
|
if board == "common":
|
||||||
raise ValueError(f"'{board}' is not a board")
|
raise ValueError(f"'{board}' is not a board")
|
||||||
board_dir = os.path.join(riotdir, 'boards', board)
|
board_dir = os.path.join(riotdir, "boards", board)
|
||||||
if not os.path.isdir(board_dir):
|
if not os.path.isdir(board_dir):
|
||||||
raise ValueError(f"Cannot find '{board}' in {riotdir}/boards")
|
raise ValueError(f"Cannot find '{board}' in {riotdir}/boards")
|
||||||
return board
|
return board
|
||||||
@ -220,7 +222,7 @@ def is_in_directory(path, directory):
|
|||||||
return path.startswith(directory)
|
return path.startswith(directory)
|
||||||
|
|
||||||
|
|
||||||
class RIOTApplication():
|
class RIOTApplication:
|
||||||
"""RIOT Application representation.
|
"""RIOT Application representation.
|
||||||
|
|
||||||
Allows calling make commands on an application for a board.
|
Allows calling make commands on an application for a board.
|
||||||
@ -232,12 +234,15 @@ class RIOTApplication():
|
|||||||
:param junit: track application in JUnit XML
|
:param junit: track application in JUnit XML
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAKEFLAGS = ('RIOT_CI_BUILD=1', 'CC_NOCOLOR=1', '--no-print-directory')
|
MAKEFLAGS = ("RIOT_CI_BUILD=1", "CC_NOCOLOR=1", "--no-print-directory")
|
||||||
|
|
||||||
COMPILE_TARGETS = ('clean', 'all',)
|
COMPILE_TARGETS = (
|
||||||
FLASH_TARGETS = ('flash-only',)
|
"clean",
|
||||||
TEST_TARGETS = ('test',)
|
"all",
|
||||||
TEST_AVAILABLE_TARGETS = ('test/available',)
|
)
|
||||||
|
FLASH_TARGETS = ("flash-only",)
|
||||||
|
TEST_TARGETS = ("test",)
|
||||||
|
TEST_AVAILABLE_TARGETS = ("test/available",)
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, board, riotdir, appdir, resultdir, junit=False):
|
def __init__(self, board, riotdir, appdir, resultdir, junit=False):
|
||||||
@ -248,24 +253,23 @@ class RIOTApplication():
|
|||||||
if junit:
|
if junit:
|
||||||
if not junit_xml:
|
if not junit_xml:
|
||||||
raise ImportError("`junit-xml` required for --report-xml")
|
raise ImportError("`junit-xml` required for --report-xml")
|
||||||
self.testcase = junit_xml.TestCase(name=self.appdir,
|
self.testcase = junit_xml.TestCase(name=self.appdir, stdout="", stderr="")
|
||||||
stdout='', stderr='')
|
|
||||||
self.log_stream = io.StringIO()
|
self.log_stream = io.StringIO()
|
||||||
logging.basicConfig(stream=self.log_stream)
|
logging.basicConfig(stream=self.log_stream)
|
||||||
else:
|
else:
|
||||||
self.testcase = None
|
self.testcase = None
|
||||||
self.logger = logging.getLogger(f'{board}.{appdir}')
|
self.logger = logging.getLogger(f"{board}.{appdir}")
|
||||||
|
|
||||||
# Currently not handling absolute directories or outside of RIOT
|
# Currently not handling absolute directories or outside of RIOT
|
||||||
assert is_in_directory(self.resultdir, resultdir), \
|
assert is_in_directory(
|
||||||
"Application result directory is outside main result directory"
|
self.resultdir, resultdir
|
||||||
|
), "Application result directory is outside main result directory"
|
||||||
|
|
||||||
# Extract values from make
|
# Extract values from make
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Get application name."""
|
"""Get application name."""
|
||||||
appname = self.make(['info-debug-variable-APPLICATION'],
|
appname = self.make(["info-debug-variable-APPLICATION"], log_error=True).strip()
|
||||||
log_error=True).strip()
|
self.logger.debug("APPLICATION: %s", appname)
|
||||||
self.logger.debug('APPLICATION: %s', appname)
|
|
||||||
return appname
|
return appname
|
||||||
|
|
||||||
def has_test(self):
|
def has_test(self):
|
||||||
@ -279,48 +283,49 @@ class RIOTApplication():
|
|||||||
has_test = False
|
has_test = False
|
||||||
else:
|
else:
|
||||||
has_test = True
|
has_test = True
|
||||||
self.logger.info('Application has test: %s', has_test)
|
self.logger.info("Application has test: %s", has_test)
|
||||||
return has_test
|
return has_test
|
||||||
|
|
||||||
def board_is_supported(self):
|
def board_is_supported(self):
|
||||||
"""Return if current board is supported."""
|
"""Return if current board is supported."""
|
||||||
env = {'BOARDS': self.board}
|
env = {"BOARDS": self.board}
|
||||||
cmd = ['info-boards-supported']
|
cmd = ["info-boards-supported"]
|
||||||
ret = self.make(cmd, env=env, log_error=True).strip()
|
ret = self.make(cmd, env=env, log_error=True).strip()
|
||||||
|
|
||||||
supported = ret == self.board
|
supported = ret == self.board
|
||||||
self.logger.info('Board supported: %s', supported)
|
self.logger.info("Board supported: %s", supported)
|
||||||
return supported
|
return supported
|
||||||
|
|
||||||
def board_has_enough_memory(self):
|
def board_has_enough_memory(self):
|
||||||
"""Return if current board has enough memory."""
|
"""Return if current board has enough memory."""
|
||||||
cmd = ['info-debug-variable-BOARD_INSUFFICIENT_MEMORY']
|
cmd = ["info-debug-variable-BOARD_INSUFFICIENT_MEMORY"]
|
||||||
boards = self.make(cmd, log_error=True).strip().split()
|
boards = self.make(cmd, log_error=True).strip().split()
|
||||||
|
|
||||||
has_enough_memory = self.board not in boards
|
has_enough_memory = self.board not in boards
|
||||||
self.logger.info('Board has enough memory: %s', has_enough_memory)
|
self.logger.info("Board has enough memory: %s", has_enough_memory)
|
||||||
return has_enough_memory
|
return has_enough_memory
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Clean build and packages."""
|
"""Clean build and packages."""
|
||||||
try:
|
try:
|
||||||
cmd = ['clean', 'clean-pkg']
|
cmd = ["clean", "clean-pkg"]
|
||||||
self.make(cmd)
|
self.make(cmd)
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.stderr += err.output + '\n'
|
self.testcase.stderr += err.output + "\n"
|
||||||
self.logger.warning('Got an error during clean, ignore: %r', err)
|
self.logger.warning("Got an error during clean, ignore: %r", err)
|
||||||
|
|
||||||
def clean_intermediates(self):
|
def clean_intermediates(self):
|
||||||
"""Clean intermediates only."""
|
"""Clean intermediates only."""
|
||||||
try:
|
try:
|
||||||
cmd = ['clean-intermediates']
|
cmd = ["clean-intermediates"]
|
||||||
self.make(cmd)
|
self.make(cmd)
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.stderr += err.output + '\n'
|
self.testcase.stderr += err.output + "\n"
|
||||||
self.logger.warning('Got an error during clean-intermediates,'
|
self.logger.warning(
|
||||||
' ignore: %r', err)
|
"Got an error during clean-intermediates," " ignore: %r", err
|
||||||
|
)
|
||||||
|
|
||||||
def run_compilation_and_test(self, **test_kwargs):
|
def run_compilation_and_test(self, **test_kwargs):
|
||||||
"""Same as `compilation_and_test` but handles exception.
|
"""Same as `compilation_and_test` but handles exception.
|
||||||
@ -333,7 +338,7 @@ class RIOTApplication():
|
|||||||
self.compilation_and_test(**test_kwargs)
|
self.compilation_and_test(**test_kwargs)
|
||||||
res = None
|
res = None
|
||||||
except ErrorInTest as err:
|
except ErrorInTest as err:
|
||||||
self.logger.error('Failed during: %s', err)
|
self.logger.error("Failed during: %s", err)
|
||||||
res = (str(err), err.application.appdir, err.errorfile)
|
res = (str(err), err.application.appdir, err.errorfile)
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.elapsed_sec = time.time() - self.testcase.timestamp
|
self.testcase.elapsed_sec = time.time() - self.testcase.timestamp
|
||||||
@ -350,11 +355,16 @@ class RIOTApplication():
|
|||||||
skip_reason_details if skip_reason_details else skip_reason,
|
skip_reason_details if skip_reason_details else skip_reason,
|
||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
self._write_resultfile('skip', skip_reason)
|
self._write_resultfile("skip", skip_reason)
|
||||||
|
|
||||||
def compilation_and_test(self, clean_after=False, runtest=True,
|
def compilation_and_test(
|
||||||
incremental=False, jobs=False,
|
self,
|
||||||
with_test_only=False):
|
clean_after=False,
|
||||||
|
runtest=True,
|
||||||
|
incremental=False,
|
||||||
|
jobs=False,
|
||||||
|
with_test_only=False,
|
||||||
|
):
|
||||||
# pylint:disable=too-many-arguments
|
# pylint:disable=too-many-arguments
|
||||||
"""Compile and execute test if available.
|
"""Compile and execute test if available.
|
||||||
|
|
||||||
@ -375,14 +385,14 @@ class RIOTApplication():
|
|||||||
# Ignore incompatible APPS
|
# Ignore incompatible APPS
|
||||||
if not self.board_is_supported():
|
if not self.board_is_supported():
|
||||||
create_directory(self.resultdir, clean=True)
|
create_directory(self.resultdir, clean=True)
|
||||||
self._skip('not_supported', 'Board not supported')
|
self._skip("not_supported", "Board not supported")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.board_has_enough_memory():
|
if not self.board_has_enough_memory():
|
||||||
create_directory(self.resultdir, clean=True)
|
create_directory(self.resultdir, clean=True)
|
||||||
self._skip(
|
self._skip(
|
||||||
'not_enough_memory',
|
"not_enough_memory",
|
||||||
'Board has not enough memory to carry application',
|
"Board has not enough memory to carry application",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -390,10 +400,7 @@ class RIOTApplication():
|
|||||||
|
|
||||||
if with_test_only and not has_test:
|
if with_test_only and not has_test:
|
||||||
create_directory(self.resultdir, clean=True)
|
create_directory(self.resultdir, clean=True)
|
||||||
self._skip(
|
self._skip("disabled_has_no_tests", f"{self.appdir} has no tests")
|
||||||
'disabled_has_no_tests',
|
|
||||||
f"{self.appdir} has no tests"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Normal case for supported apps
|
# Normal case for supported apps
|
||||||
@ -404,60 +411,55 @@ class RIOTApplication():
|
|||||||
|
|
||||||
compilation_cmd = list(self.COMPILE_TARGETS)
|
compilation_cmd = list(self.COMPILE_TARGETS)
|
||||||
if jobs is not None:
|
if jobs is not None:
|
||||||
compilation_cmd += ['--jobs']
|
compilation_cmd += ["--jobs"]
|
||||||
if jobs:
|
if jobs:
|
||||||
compilation_cmd += [str(jobs)]
|
compilation_cmd += [str(jobs)]
|
||||||
self.make_with_outfile('compilation', compilation_cmd)
|
self.make_with_outfile("compilation", compilation_cmd)
|
||||||
if clean_after:
|
if clean_after:
|
||||||
self.clean_intermediates()
|
self.clean_intermediates()
|
||||||
|
|
||||||
if runtest:
|
if runtest:
|
||||||
if has_test:
|
if has_test:
|
||||||
setuptasks = collections.OrderedDict(
|
setuptasks = collections.OrderedDict([("flash", self.FLASH_TARGETS)])
|
||||||
[('flash', self.FLASH_TARGETS)])
|
self.make_with_outfile(
|
||||||
self.make_with_outfile('test', self.TEST_TARGETS,
|
"test", self.TEST_TARGETS, save_output=True, setuptasks=setuptasks
|
||||||
save_output=True, setuptasks=setuptasks)
|
)
|
||||||
if clean_after:
|
if clean_after:
|
||||||
self.clean()
|
self.clean()
|
||||||
else:
|
else:
|
||||||
self._skip(
|
self._skip("skip.no_test", f"{self.appdir} has no tests")
|
||||||
'skip.no_test',
|
|
||||||
f"{self.appdir} has no tests"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info('Success')
|
self.logger.info("Success")
|
||||||
|
|
||||||
def make(self, args, env=None, log_error=False):
|
def make(self, args, env=None, log_error=False):
|
||||||
"""Run make command in appdir."""
|
"""Run make command in appdir."""
|
||||||
env = env or {}
|
env = env or {}
|
||||||
# HACK: BOARD should be set for make in environment and not command
|
# HACK: BOARD should be set for make in environment and not command
|
||||||
# line either it break the `BOARD=none` for global commands
|
# line either it break the `BOARD=none` for global commands
|
||||||
env['BOARD'] = self.board
|
env["BOARD"] = self.board
|
||||||
|
|
||||||
full_env = os.environ.copy()
|
full_env = os.environ.copy()
|
||||||
full_env.update(env)
|
full_env.update(env)
|
||||||
|
|
||||||
cmd = [MAKE]
|
cmd = [MAKE]
|
||||||
cmd.extend(self.MAKEFLAGS)
|
cmd.extend(self.MAKEFLAGS)
|
||||||
cmd.extend(['-C', os.path.join(self.riotdir, self.appdir)])
|
cmd.extend(["-C", os.path.join(self.riotdir, self.appdir)])
|
||||||
cmd.extend(args)
|
cmd.extend(args)
|
||||||
|
|
||||||
self.logger.debug('%r ENV %s', cmd, env)
|
self.logger.debug("%r ENV %s", cmd, env)
|
||||||
# Call without 'universal_newlines' to have bytes and handle decoding
|
# Call without 'universal_newlines' to have bytes and handle decoding
|
||||||
# (encoding and errors are only supported after python 3.6)
|
# (encoding and errors are only supported after python 3.6)
|
||||||
try:
|
try:
|
||||||
out = subprocess.check_output(cmd, env=full_env,
|
out = subprocess.check_output(cmd, env=full_env, stderr=subprocess.STDOUT)
|
||||||
stderr=subprocess.STDOUT)
|
out = out.decode("utf-8", errors="replace")
|
||||||
out = out.decode('utf-8', errors='replace')
|
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
err.output = err.output.decode('utf-8', errors='replace')
|
err.output = err.output.decode("utf-8", errors="replace")
|
||||||
if log_error:
|
if log_error:
|
||||||
self.logger.error('Error during command: \n%s', err.output)
|
self.logger.error("Error during command: \n%s", err.output)
|
||||||
raise err
|
raise err
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def make_with_outfile(self, name, args, save_output=False,
|
def make_with_outfile(self, name, args, save_output=False, setuptasks=None):
|
||||||
setuptasks=None):
|
|
||||||
"""Run make but save result in an outfile.
|
"""Run make but save result in an outfile.
|
||||||
|
|
||||||
It will be saved in `self.resultdir/name.[success|failure]`.
|
It will be saved in `self.resultdir/name.[success|failure]`.
|
||||||
@ -467,20 +469,20 @@ class RIOTApplication():
|
|||||||
if not, return an empty string.
|
if not, return an empty string.
|
||||||
:param setuptasks: OrderedDict of tasks to run before the main one
|
:param setuptasks: OrderedDict of tasks to run before the main one
|
||||||
"""
|
"""
|
||||||
self.logger.info('Run %s', name)
|
self.logger.info("Run %s", name)
|
||||||
setuptasks = setuptasks or {}
|
setuptasks = setuptasks or {}
|
||||||
|
|
||||||
# Do not re-run if success
|
# Do not re-run if success
|
||||||
output = self._make_get_previous_output(name)
|
output = self._make_get_previous_output(name)
|
||||||
if output is not None:
|
if output is not None:
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.stdout += output + '\n'
|
self.testcase.stdout += output + "\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Run setup-tasks, output is only kept in case of error
|
# Run setup-tasks, output is only kept in case of error
|
||||||
for taskname, taskargs in setuptasks.items():
|
for taskname, taskargs in setuptasks.items():
|
||||||
taskname = f'{name}.{taskname}'
|
taskname = f"{name}.{taskname}"
|
||||||
self.logger.info('Run %s', taskname)
|
self.logger.info("Run %s", taskname)
|
||||||
try:
|
try:
|
||||||
self.make(taskargs)
|
self.make(taskargs)
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
@ -490,10 +492,10 @@ class RIOTApplication():
|
|||||||
try:
|
try:
|
||||||
output = self.make(args)
|
output = self.make(args)
|
||||||
if not save_output:
|
if not save_output:
|
||||||
output = ''
|
output = ""
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.stdout += output + '\n'
|
self.testcase.stdout += output + "\n"
|
||||||
self._write_resultfile(name, 'success', output)
|
self._write_resultfile(name, "success", output)
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
self._make_handle_error(name, err)
|
self._make_handle_error(name, err)
|
||||||
|
|
||||||
@ -503,9 +505,8 @@ class RIOTApplication():
|
|||||||
Returns `output` if it is there, None if not.
|
Returns `output` if it is there, None if not.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(self._outfile(f'{name}.success'),
|
with open(self._outfile(f"{name}.success"), encoding="utf-8") as outputfd:
|
||||||
encoding='utf-8') as outputfd:
|
self.logger.info("Nothing to be done for %s", name)
|
||||||
self.logger.info('Nothing to be done for %s', name)
|
|
||||||
return outputfd.read()
|
return outputfd.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
@ -513,31 +514,29 @@ class RIOTApplication():
|
|||||||
|
|
||||||
def _make_handle_error(self, name, err):
|
def _make_handle_error(self, name, err):
|
||||||
"""Handle exception during make step `name`."""
|
"""Handle exception during make step `name`."""
|
||||||
output = ' '.join(err.cmd) + '\n'
|
output = " ".join(err.cmd) + "\n"
|
||||||
output += err.output + '\n'
|
output += err.output + "\n"
|
||||||
output += f'Return value: {err.returncode}\n'
|
output += f"Return value: {err.returncode}\n"
|
||||||
outfile = self._write_resultfile(name, 'failed', output)
|
outfile = self._write_resultfile(name, "failed", output)
|
||||||
|
|
||||||
self.logger.warning(output)
|
self.logger.warning(output)
|
||||||
self.logger.error('Error during %s, writing to %s', name, outfile)
|
self.logger.error("Error during %s, writing to %s", name, outfile)
|
||||||
if self.testcase:
|
if self.testcase:
|
||||||
self.testcase.stderr += err.output + '\n'
|
self.testcase.stderr += err.output + "\n"
|
||||||
if name == "test":
|
if name == "test":
|
||||||
self.testcase.add_failure_info(f"{err.cmd} failed",
|
self.testcase.add_failure_info(f"{err.cmd} failed", err.output)
|
||||||
err.output)
|
|
||||||
else:
|
else:
|
||||||
self.testcase.add_error_info(f"{err.cmd} had an error",
|
self.testcase.add_error_info(f"{err.cmd} had an error", err.output)
|
||||||
err.output)
|
|
||||||
raise ErrorInTest(name, self, outfile)
|
raise ErrorInTest(name, self, outfile)
|
||||||
|
|
||||||
def _write_resultfile(self, name, status, body=''):
|
def _write_resultfile(self, name, status, body=""):
|
||||||
"""Write `body` to result file `name.status`.
|
"""Write `body` to result file `name.status`.
|
||||||
|
|
||||||
It also deletes other `name.*` files before.
|
It also deletes other `name.*` files before.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Delete previous status files
|
# Delete previous status files
|
||||||
resultfiles = glob.glob(self._outfile(f'{name}.*'))
|
resultfiles = glob.glob(self._outfile(f"{name}.*"))
|
||||||
for resultfile in resultfiles:
|
for resultfile in resultfiles:
|
||||||
try:
|
try:
|
||||||
os.remove(resultfile)
|
os.remove(resultfile)
|
||||||
@ -545,11 +544,10 @@ class RIOTApplication():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Create new file
|
# Create new file
|
||||||
filename = f'{name}.{status}'
|
filename = f"{name}.{status}"
|
||||||
outfile = self._outfile(filename)
|
outfile = self._outfile(filename)
|
||||||
|
|
||||||
with open(outfile, 'w+', encoding='utf-8',
|
with open(outfile, "w+", encoding="utf-8", errors="replace") as outfd:
|
||||||
errors='replace') as outfd:
|
|
||||||
outfd.write(body)
|
outfd.write(body)
|
||||||
outfd.flush()
|
outfd.flush()
|
||||||
return outfile
|
return outfile
|
||||||
@ -559,7 +557,7 @@ class RIOTApplication():
|
|||||||
return os.path.join(self.resultdir, filename)
|
return os.path.join(self.resultdir, filename)
|
||||||
|
|
||||||
|
|
||||||
TOOLCHAIN_SCRIPT = 'dist/tools/ci/print_toolchain_versions.sh'
|
TOOLCHAIN_SCRIPT = "dist/tools/ci/print_toolchain_versions.sh"
|
||||||
|
|
||||||
|
|
||||||
def print_toolchain(riotdir):
|
def print_toolchain(riotdir):
|
||||||
@ -569,23 +567,23 @@ def print_toolchain(riotdir):
|
|||||||
"""
|
"""
|
||||||
toolchain_script = os.path.join(riotdir, TOOLCHAIN_SCRIPT)
|
toolchain_script = os.path.join(riotdir, TOOLCHAIN_SCRIPT)
|
||||||
out = subprocess.check_output([toolchain_script])
|
out = subprocess.check_output([toolchain_script])
|
||||||
return out.decode('utf-8', errors='replace')
|
return out.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def save_toolchain(riotdir, resultdir):
|
def save_toolchain(riotdir, resultdir):
|
||||||
"""Save toolchain in 'resultdir/toolchain'."""
|
"""Save toolchain in 'resultdir/toolchain'."""
|
||||||
outfile = os.path.join(resultdir, 'toolchain')
|
outfile = os.path.join(resultdir, "toolchain")
|
||||||
create_directory(resultdir)
|
create_directory(resultdir)
|
||||||
|
|
||||||
toolchain = print_toolchain(riotdir)
|
toolchain = print_toolchain(riotdir)
|
||||||
with open(outfile, 'w+', encoding='utf-8', errors='replace') as outputfd:
|
with open(outfile, "w+", encoding="utf-8", errors="replace") as outputfd:
|
||||||
outputfd.write(toolchain)
|
outputfd.write(toolchain)
|
||||||
|
|
||||||
|
|
||||||
def _test_failed_summary(errors, relpathstart=None):
|
def _test_failed_summary(errors, relpathstart=None):
|
||||||
"""Generate a test summary for failures."""
|
"""Generate a test summary for failures."""
|
||||||
if not errors:
|
if not errors:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
errors_dict = {}
|
errors_dict = {}
|
||||||
for step, appdir, errorfile in errors:
|
for step, appdir, errorfile in errors:
|
||||||
@ -593,13 +591,13 @@ def _test_failed_summary(errors, relpathstart=None):
|
|||||||
errorfile = os.path.relpath(errorfile, relpathstart)
|
errorfile = os.path.relpath(errorfile, relpathstart)
|
||||||
errors_dict.setdefault(step, []).append((appdir, errorfile))
|
errors_dict.setdefault(step, []).append((appdir, errorfile))
|
||||||
|
|
||||||
summary = ''
|
summary = ""
|
||||||
for step, errs in sorted(errors_dict.items()):
|
for step, errs in sorted(errors_dict.items()):
|
||||||
summary += f'Failures during {step}:\n'
|
summary += f"Failures during {step}:\n"
|
||||||
for appdir, errorfile in errs:
|
for appdir, errorfile in errs:
|
||||||
summary += f'- [{appdir}]({errorfile})\n'
|
summary += f"- [{appdir}]({errorfile})\n"
|
||||||
# Separate sections with a new line
|
# Separate sections with a new line
|
||||||
summary += '\n'
|
summary += "\n"
|
||||||
|
|
||||||
# Remove last new line
|
# Remove last new line
|
||||||
summary = summary[:-1]
|
summary = summary[:-1]
|
||||||
@ -608,9 +606,9 @@ def _test_failed_summary(errors, relpathstart=None):
|
|||||||
|
|
||||||
def save_failure_summary(resultdir, summary):
|
def save_failure_summary(resultdir, summary):
|
||||||
"""Save test summary in 'resultdir/board/failuresummary'."""
|
"""Save test summary in 'resultdir/board/failuresummary'."""
|
||||||
outfile = os.path.join(resultdir, 'failuresummary.md')
|
outfile = os.path.join(resultdir, "failuresummary.md")
|
||||||
|
|
||||||
with open(outfile, 'w+', encoding='utf-8', errors='replace') as outputfd:
|
with open(outfile, "w+", encoding="utf-8", errors="replace") as outputfd:
|
||||||
outputfd.write(summary)
|
outputfd.write(summary)
|
||||||
|
|
||||||
|
|
||||||
@ -633,7 +631,7 @@ def list_from_string(list_str=None):
|
|||||||
>>> list_from_string("a b c")
|
>>> list_from_string("a b c")
|
||||||
['a', 'b', 'c']
|
['a', 'b', 'c']
|
||||||
"""
|
"""
|
||||||
value = (list_str or '').split(' ')
|
value = (list_str or "").split(" ")
|
||||||
return [v for v in value if v]
|
return [v for v in value if v]
|
||||||
|
|
||||||
|
|
||||||
@ -642,57 +640,96 @@ def _strip_board_equal(board):
|
|||||||
|
|
||||||
Increase RIOT compatibility.
|
Increase RIOT compatibility.
|
||||||
"""
|
"""
|
||||||
if board.startswith('BOARD='):
|
if board.startswith("BOARD="):
|
||||||
board = board.replace('BOARD=', '')
|
board = board.replace("BOARD=", "")
|
||||||
return board
|
return board
|
||||||
|
|
||||||
|
|
||||||
PARSER = argparse.ArgumentParser(
|
PARSER = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
PARSER.add_argument("riot_directory", help="RIOT directory to test")
|
||||||
PARSER.add_argument('riot_directory', help='RIOT directory to test')
|
PARSER.add_argument("board", help="Board to test", type=_strip_board_equal)
|
||||||
PARSER.add_argument('board', help='Board to test', type=_strip_board_equal)
|
|
||||||
PARSER.add_argument('result_directory', nargs='?', default='results',
|
|
||||||
help='Result directory')
|
|
||||||
PARSER.add_argument(
|
PARSER.add_argument(
|
||||||
'--applications', type=list_from_string,
|
"result_directory", nargs="?", default="results", help="Result directory"
|
||||||
help=('List of applications to test, overwrites default configuration of'
|
|
||||||
' testing all applications'),
|
|
||||||
)
|
)
|
||||||
PARSER.add_argument(
|
PARSER.add_argument(
|
||||||
'--applications-exclude', type=list_from_string,
|
"--applications",
|
||||||
help=('List of applications to exclude from tested applications.'
|
type=list_from_string,
|
||||||
' Also applied after "--applications".'),
|
help=(
|
||||||
|
"List of applications to test, overwrites default configuration of"
|
||||||
|
" testing all applications"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--applications-exclude",
|
||||||
|
type=list_from_string,
|
||||||
|
help=(
|
||||||
|
"List of applications to exclude from tested applications."
|
||||||
|
' Also applied after "--applications".'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--no-test", action="store_true", default=False, help="Disable executing tests"
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--with-test-only",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Only compile applications that have a test",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--loglevel", choices=LOG_LEVELS, default="info", help="Python logger log level"
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--incremental",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Do not rerun successful compilation and tests",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--clean-after",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Clean after running each test",
|
||||||
)
|
)
|
||||||
PARSER.add_argument('--no-test', action='store_true', default=False,
|
|
||||||
help='Disable executing tests')
|
|
||||||
PARSER.add_argument('--with-test-only', action='store_true', default=False,
|
|
||||||
help='Only compile applications that have a test')
|
|
||||||
PARSER.add_argument('--loglevel', choices=LOG_LEVELS, default='info',
|
|
||||||
help='Python logger log level')
|
|
||||||
PARSER.add_argument('--incremental', action='store_true', default=False,
|
|
||||||
help='Do not rerun successful compilation and tests')
|
|
||||||
PARSER.add_argument('--clean-after', action='store_true', default=False,
|
|
||||||
help='Clean after running each test')
|
|
||||||
|
|
||||||
PARSER.add_argument('--compile-targets', type=list_from_string,
|
|
||||||
default=' '.join(RIOTApplication.COMPILE_TARGETS),
|
|
||||||
help='List of make targets to compile')
|
|
||||||
PARSER.add_argument('--flash-targets', type=list_from_string,
|
|
||||||
default=' '.join(RIOTApplication.FLASH_TARGETS),
|
|
||||||
help='List of make targets to flash')
|
|
||||||
PARSER.add_argument('--test-targets', type=list_from_string,
|
|
||||||
default=' '.join(RIOTApplication.TEST_TARGETS),
|
|
||||||
help='List of make targets to run test')
|
|
||||||
PARSER.add_argument('--test-available-targets', type=list_from_string,
|
|
||||||
default=' '.join(RIOTApplication.TEST_AVAILABLE_TARGETS),
|
|
||||||
help='List of make targets to know if a test is present')
|
|
||||||
PARSER.add_argument('--report-xml', action='store_true', default=False,
|
|
||||||
help='Output results to report.xml in the '
|
|
||||||
'result_directory')
|
|
||||||
|
|
||||||
PARSER.add_argument(
|
PARSER.add_argument(
|
||||||
'--jobs', '-j', type=int, default=None,
|
"--compile-targets",
|
||||||
help="Parallel building (0 means not limit, like '--jobs')")
|
type=list_from_string,
|
||||||
|
default=" ".join(RIOTApplication.COMPILE_TARGETS),
|
||||||
|
help="List of make targets to compile",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--flash-targets",
|
||||||
|
type=list_from_string,
|
||||||
|
default=" ".join(RIOTApplication.FLASH_TARGETS),
|
||||||
|
help="List of make targets to flash",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--test-targets",
|
||||||
|
type=list_from_string,
|
||||||
|
default=" ".join(RIOTApplication.TEST_TARGETS),
|
||||||
|
help="List of make targets to run test",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--test-available-targets",
|
||||||
|
type=list_from_string,
|
||||||
|
default=" ".join(RIOTApplication.TEST_AVAILABLE_TARGETS),
|
||||||
|
help="List of make targets to know if a test is present",
|
||||||
|
)
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--report-xml",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Output results to report.xml in the " "result_directory",
|
||||||
|
)
|
||||||
|
|
||||||
|
PARSER.add_argument(
|
||||||
|
"--jobs",
|
||||||
|
"-j",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Parallel building (0 means not limit, like '--jobs')",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
def main(args):
|
||||||
@ -704,23 +741,24 @@ def main(args):
|
|||||||
|
|
||||||
logger.addHandler(LOG_HANDLER)
|
logger.addHandler(LOG_HANDLER)
|
||||||
|
|
||||||
logger.info('Saving toolchain')
|
logger.info("Saving toolchain")
|
||||||
save_toolchain(args.riot_directory, args.result_directory)
|
save_toolchain(args.riot_directory, args.result_directory)
|
||||||
|
|
||||||
board = check_is_board(args.riot_directory, args.board)
|
board = check_is_board(args.riot_directory, args.board)
|
||||||
logger.debug('board: %s', board)
|
logger.debug("board: %s", board)
|
||||||
|
|
||||||
# Expand application directories: allows use of glob in application names
|
# Expand application directories: allows use of glob in application names
|
||||||
apps_dirs = _expand_apps_directories(args.applications,
|
apps_dirs = _expand_apps_directories(args.applications, args.riot_directory)
|
||||||
args.riot_directory)
|
apps_dirs_skip = _expand_apps_directories(
|
||||||
apps_dirs_skip = _expand_apps_directories(args.applications_exclude,
|
args.applications_exclude, args.riot_directory, skip=True
|
||||||
args.riot_directory, skip=True)
|
)
|
||||||
|
|
||||||
app_dirs = apps_directories(args.riot_directory, apps_dirs=apps_dirs,
|
app_dirs = apps_directories(
|
||||||
apps_dirs_skip=apps_dirs_skip)
|
args.riot_directory, apps_dirs=apps_dirs, apps_dirs_skip=apps_dirs_skip
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug('app_dirs: %s', app_dirs)
|
logger.debug("app_dirs: %s", app_dirs)
|
||||||
logger.debug('resultdir: %s', args.result_directory)
|
logger.debug("resultdir: %s", args.result_directory)
|
||||||
board_result_directory = os.path.join(args.result_directory, args.board)
|
board_result_directory = os.path.join(args.result_directory, args.board)
|
||||||
|
|
||||||
# Overwrite the compile/test targets from command line arguments
|
# Overwrite the compile/test targets from command line arguments
|
||||||
@ -730,18 +768,28 @@ def main(args):
|
|||||||
RIOTApplication.TEST_AVAILABLE_TARGETS = args.test_available_targets
|
RIOTApplication.TEST_AVAILABLE_TARGETS = args.test_available_targets
|
||||||
|
|
||||||
# List of applications for board
|
# List of applications for board
|
||||||
applications = [RIOTApplication(board, args.riot_directory, app_dir,
|
applications = [
|
||||||
board_result_directory,
|
RIOTApplication(
|
||||||
junit=args.report_xml)
|
board,
|
||||||
for app_dir in app_dirs]
|
args.riot_directory,
|
||||||
|
app_dir,
|
||||||
|
board_result_directory,
|
||||||
|
junit=args.report_xml,
|
||||||
|
)
|
||||||
|
for app_dir in app_dirs
|
||||||
|
]
|
||||||
|
|
||||||
# Execute tests
|
# Execute tests
|
||||||
errors = [app.run_compilation_and_test(clean_after=args.clean_after,
|
errors = [
|
||||||
runtest=not args.no_test,
|
app.run_compilation_and_test(
|
||||||
incremental=args.incremental,
|
clean_after=args.clean_after,
|
||||||
jobs=args.jobs,
|
runtest=not args.no_test,
|
||||||
with_test_only=args.with_test_only)
|
incremental=args.incremental,
|
||||||
for app in applications]
|
jobs=args.jobs,
|
||||||
|
with_test_only=args.with_test_only,
|
||||||
|
)
|
||||||
|
for app in applications
|
||||||
|
]
|
||||||
errors = [e for e in errors if e is not None]
|
errors = [e for e in errors if e is not None]
|
||||||
num_errors = len(errors)
|
num_errors = len(errors)
|
||||||
|
|
||||||
@ -755,16 +803,20 @@ def main(args):
|
|||||||
with open(report_file, "w+", encoding="utf-8") as report:
|
with open(report_file, "w+", encoding="utf-8") as report:
|
||||||
junit_xml.TestSuite.to_file(
|
junit_xml.TestSuite.to_file(
|
||||||
report,
|
report,
|
||||||
[junit_xml.TestSuite(f'compile_and_test_for_{board}',
|
[
|
||||||
[app.testcase for app in applications])]
|
junit_xml.TestSuite(
|
||||||
|
f"compile_and_test_for_{board}",
|
||||||
|
[app.testcase for app in applications],
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
if num_errors:
|
if num_errors:
|
||||||
logger.error('Tests failed: %d', num_errors)
|
logger.error("Tests failed: %d", num_errors)
|
||||||
print(summary, end='')
|
print(summary, end="")
|
||||||
else:
|
else:
|
||||||
logger.info('Tests successful')
|
logger.info("Tests successful")
|
||||||
sys.exit(num_errors)
|
sys.exit(num_errors)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main(PARSER.parse_args())
|
main(PARSER.parse_args())
|
||||||
|
|||||||
12
dist/tools/compile_and_test_for_board/setup.cfg
vendored
Normal file
12
dist/tools/compile_and_test_for_board/setup.cfg
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Use black compatible configuration for flake8 and pylint
|
||||||
|
# flake8: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
|
||||||
|
# pylint: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#pylint
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 88
|
||||||
|
extend-ignore = E203
|
||||||
|
|
||||||
|
[pylint]
|
||||||
|
max-line-length = 88
|
||||||
|
|
||||||
|
[pylint.messages_control]
|
||||||
|
disable = C0330, C0326
|
||||||
10
dist/tools/compile_and_test_for_board/tox.ini
vendored
10
dist/tools/compile_and_test_for_board/tox.ini
vendored
@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = test,lint,flake8
|
envlist = test,lint,flake8,black
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
@ -10,6 +10,7 @@ commands =
|
|||||||
test: {[testenv:test]commands}
|
test: {[testenv:test]commands}
|
||||||
lint: {[testenv:lint]commands}
|
lint: {[testenv:lint]commands}
|
||||||
flake8: {[testenv:flake8]commands}
|
flake8: {[testenv:flake8]commands}
|
||||||
|
black: {[testenv:black]commands}
|
||||||
|
|
||||||
[testenv:test]
|
[testenv:test]
|
||||||
deps = pytest
|
deps = pytest
|
||||||
@ -19,9 +20,14 @@ commands =
|
|||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
deps = pylint
|
deps = pylint
|
||||||
commands =
|
commands =
|
||||||
pylint {env:script} tests
|
pylint --rcfile=setup.cfg {env:script} tests
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
deps = flake8
|
deps = flake8
|
||||||
commands =
|
commands =
|
||||||
flake8 {env:script} tests
|
flake8 {env:script} tests
|
||||||
|
|
||||||
|
[testenv:black]
|
||||||
|
deps = black
|
||||||
|
commands =
|
||||||
|
black --check --diff {env:script} tests
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user