ChibiOS/tools/workflows/make.py

279 lines
8.0 KiB
Python
Executable File

#!/usr/bin/env python
"""
This script is intended to be used to run make under supervision for
the purpose of capturing its stdout/stderr and retcode to generate
a JUnit XML report. This report can be used in Jenkins or any other CI.
This report contains one test suite and as many cases as many targets
are specified.
To get help on usage, possible options and their descriptions, use
the following command:
make.py --help
An example of running this script for building a demo project:
make.py -C demos/STM32/RT-STM32WB55RG-NUCLEO68 -f Makefile all
This script requires the following packages to be installed:
pip install junit-xml
pip install pyyaml
pip install requests
"""
import argparse
import collections
import os
import re
import subprocess
import sys
import time
from xml.etree import ElementTree
import junit_xml
import requests
import yaml
def get_jobserver_auth_fds():
"""Get "jobserver" file descriptors from MAKEFLAGS."""
make_flags = os.environ.get('MAKEFLAGS', '')
jobserver_auth_flag = '--jobserver-auth='
for make_flag in make_flags.split():
if make_flag.startswith(jobserver_auth_flag):
fdr, _, fdw = make_flag[len(jobserver_auth_flag):].partition(',')
return (int(fdr), int(fdw))
return ()
class BaseRules:
def __init__(self, filename):
self.filename = filename
self._rules = None
def load(self):
with open(self.filename) as fd:
self._rules = yaml.safe_load(fd)
@property
def rules(self):
if self._rules is None:
self.load()
return self._rules
class SkipRules(BaseRules):
def match(self, path, stderr):
for rule in self.rules:
if (re.match(rule['path'], path)
and re.search(rule['stderr'], stderr)):
return True, rule['reason']
return False, None
class TargetRules(BaseRules):
def match(self, path):
for rule, targets in self.rules.items():
if re.match(rule, path):
return targets
return None
def get_params(args):
directory = args.directory or ''
makefile = args.makefile or ''
path = os.path.join(directory, makefile)
if args.prefix:
assert path.startswith(args.prefix)
path = path[len(args.prefix):].lstrip(os.path.sep)
return Params(path, directory, makefile)
Params = collections.namedtuple('Params', [
'path',
'directory',
'makefile',
])
def get_targets(args, path):
if args.target_rules:
target_rules = TargetRules(args.target_rules)
targets = target_rules.match(path)
if targets is not None:
return targets
return args.targets
def make(args):
if args.skip_rules:
skip_rules = SkipRules(args.skip_rules)
params = get_params(args)
package = params.path.partition(os.path.sep)[0]
suite = junit_xml.TestSuite(
params.path,
timestamp=time.time(),
file=params.path,
package=package,
)
cmd = ['/usr/bin/env', 'make']
if params.directory:
cmd.extend(['-C', params.directory])
if params.makefile:
cmd.extend(['-f', params.makefile])
if args.jobs > 1:
cmd.extend(['-j', str(args.jobs)])
jobserver_auth_fds = get_jobserver_auth_fds()
targets = get_targets(args, params.path)
for target in targets:
if not target:
continue
cmd_target = cmd + [target]
timestamp=time.time()
start = time.monotonic()
# To better control the use of resources by a subprocess make, pass
# jobserver auth file descriptors in the subprocess, so it will
# participate in the jobserver protocol and schedule its jobs
# accordingly.
ret = subprocess.run(cmd_target, pass_fds=jobserver_auth_fds,
capture_output=True, text=True)
end = time.monotonic()
case_name = 'make ' + target
case_log = ' '.join(cmd_target)
case_classname = params.path
case = junit_xml.TestCase(
case_name,
classname=case_classname,
file=params.path,
log=case_log,
timestamp=timestamp,
elapsed_sec=(end - start),
stdout=ret.stdout,
stderr=ret.stderr,
)
suite.test_cases.append(case)
if ret.returncode != 0:
skipped, msg = skip_rules.match(params.path, ret.stderr)
if skipped:
case.add_skipped_info(msg)
continue
msg = 'Ended with non-zero exit code: {}'.format(ret.returncode)
case.add_failure_info(msg)
if args.result == '-':
suite.to_file(sys.stdout, [suite])
else:
test_result_path = os.path.join(args.result, params.path) + '.xml'
with open(test_result_path, 'w') as f:
suite.to_file(f, [suite])
def check_skip_rules(args):
def get_signature(content):
root = ElementTree.fromstring(content)
return (
root.findtext('className'),
root.findtext('stderr'),
)
if not args.skip_rules:
sys.stderr.write('--skip-rules is required\n')
sys.exit(2)
skip_rules = SkipRules(args.skip_rules)
url = args.check_skip_rules
if not url.endswith('/api/xml'):
url += '/api/xml'
resp = requests.get(url)
if resp.status_code != 200:
sys.stderr.write('Got unexpected response {}\n'
.format(resp.status_code))
sys.exit(3)
path, stderr = get_signature(resp.content)
if not (path and stderr):
sys.stderr.write('Cannot find path and stderr\n')
sys.exit(4)
matched, msg = skip_rules.match(path, stderr)
if matched:
sys.stdout.write('Matched rule: {}\n'.format(msg))
else:
sys.stdout.write('Not matched!\n')
sys.exit(1)
def check_target_rules(args):
if not args.target_rules:
sys.stderr.write('--target-rules is required\n')
sys.exit(1)
params = get_params(args)
targets = get_targets(args, params.path)
sys.stdout.write(' '.join(targets))
def main():
parser = argparse.ArgumentParser(description=(
'Execute make targets and generate JUnit results'
))
# Make specific arguments
parser.add_argument('-C', '--directory',
help='Change directory to dir (equivalent to '
'`make -C dir`)')
parser.add_argument('-f', '--makefile',
help='Use file as a makefile (equivalent to '
'`make -f file`)')
parser.add_argument('-j', '--jobs',
type=int, default=1,
help='Number of simultaneous jobs (equivalent to '
'`make -j jobs`)')
# Test specific arguments
parser.add_argument('-r', '--result',
default='-',
help='Directory to store JUnit XML test result')
parser.add_argument('-p', '--prefix',
help=('Prefix path which should be removed for test '
'result'))
parser.add_argument('-s', '--skip-rules',
help='YAML-file with skip rules')
parser.add_argument('-S', '--check-skip-rules',
help='URL to a test result in Jenkins job')
parser.add_argument('-t', '--target-rules',
help='YAML-file with target rules')
parser.add_argument('-T', '--check-target-rules', action='store_true',
help='Print target rules for a given makefile')
parser.add_argument('targets', metavar='target', nargs='*',
default=['all', 'clean'],
help='Names of targets to run')
args = parser.parse_args()
if args.check_skip_rules:
check_skip_rules(args)
elif args.check_target_rules:
check_target_rules(args)
else:
make(args)
if __name__ == '__main__':
main()