blob: 9fd9a00454ed616520653adeb66a3b6a1a8d200d [file] [log] [blame] [raw]
"""
mbed SDK
Copyright (c) 2017-2018 ARM Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import re
import argparse
import shutil
"""
Generates code in following structure.
<output dir>/
|-- host_tests/
| |-- mbedtls_test.py
| |-- suites/
| | |-- *.data files
| |-- mbedtls/
| | |-- <test suite #1>/
| | | |-- main.c
| | ...
| | |-- <test suite #n>/
| | | |-- main.c
| | |
"""
BEGIN_HEADER_REGEX = '/\*\s*BEGIN_HEADER\s*\*/'
END_HEADER_REGEX = '/\*\s*END_HEADER\s*\*/'
BEGIN_DEP_REGEX = 'BEGIN_DEPENDENCIES'
END_DEP_REGEX = 'END_DEPENDENCIES'
BEGIN_CASE_REGEX = '/\*\s*BEGIN_CASE\s*(.*?)\s*\*/'
END_CASE_REGEX = '/\*\s*END_CASE\s*\*/'
class InvalidFileFormat(Exception):
"""
Exception to indicate invalid file format.
"""
pass
def gen_deps(deps):
"""
Generates dependency i.e. if def and endif code
:param deps:
:return:
"""
dep_start = ''
dep_end = ''
for dep in deps:
if dep[0] == '!':
noT = '!'
dep = dep[1:]
else:
noT = ''
dep_start += '#if %sdefined(%s)\n' % (noT, dep)
dep_end = '#endif /* %s%s */\n' % (noT, dep) + dep_end
return dep_start, dep_end
def gen_deps_one_line(deps):
"""
Generates dependency checks in one line. Useful for writing code in #else case.
:param deps:
:return:
"""
defines = []
for dep in deps:
if dep[0] == '!':
noT = '!'
dep = dep[1:]
else:
noT = ''
defines.append('%sdefined(%s)' % (noT, dep))
return '#if ' + ' && '.join(defines)
def gen_function_wrapper(name, args_dispatch):
"""
Creates test function code
:param name:
:param args_dispatch:
:return:
"""
# Then create the wrapper
wrapper = '''
void {name}_wrapper( void ** params )
{{
{unused_params}
{name}( {args} );
}}
'''.format(name=name, unused_params='(void)params;' if len(args_dispatch) == 0 else '', args=', '.join(args_dispatch))
return wrapper
def gen_dispatch(name, deps):
"""
Generates dispatch condition for the functions.
:param name:
:param deps:
:return:
"""
if len(deps):
ifdef = gen_deps_one_line(deps)
dispatch_code = '''
{ifdef}
{name}_wrapper,
#else
NULL,
#endif
'''.format(ifdef=ifdef, name=name)
else:
dispatch_code = '''
{name}_wrapper,
'''.format(name=name)
return dispatch_code
def parse_suite_headers(line_no, funcs_f):
"""
Parses function headers.
:param line_no:
:param funcs_f:
:return:
"""
headers = '#line %d "%s"\n' % (line_no + 1, funcs_f.name)
for line in funcs_f:
line_no += 1
if re.search(END_HEADER_REGEX, line):
break
headers += line
else:
raise InvalidFileFormat("file: %s - end header pattern [%s] not found!" % (funcs_f.name, END_HEADER_REGEX))
return line_no, headers
def parse_suite_deps(line_no, funcs_f):
"""
Parses function dependencies.
:param line_no:
:param funcs_f:
:return:
"""
deps = []
for line in funcs_f:
line_no += 1
m = re.search('depends_on\:(.*)', line.strip())
if m:
deps += [x.strip() for x in m.group(1).split(':')]
if re.search(END_DEP_REGEX, line):
break
else:
raise InvalidFileFormat("file: %s - end dependency pattern [%s] not found!" % (funcs_f.name, END_DEP_REGEX))
return line_no, deps
def parse_function_deps(line):
"""
:param line:
:return:
"""
deps = []
m = re.search(BEGIN_CASE_REGEX, line)
dep_str = m.group(1)
if len(dep_str):
m = re.search('depends_on:(.*)', dep_str)
if m:
deps = m.group(1).strip().split(':')
return deps
def parse_function_signature(line):
"""
Parsing function signature
:param line:
:return:
"""
args = []
args_dispatch = []
m = re.search('\s*void\s+(\w+)\s*\(', line, re.I)
if not m:
raise ValueError("Test function should return 'void'\n%s" % line)
name = m.group(1)
line = line[len(m.group(0)):]
arg_idx = 0
for arg in line[:line.find(')')].split(','):
arg = arg.strip()
if arg == '':
continue
if re.search('int\s+.*', arg.strip()):
args.append('int')
args_dispatch.append('*( (int *) params[%d] )' % arg_idx)
elif re.search('char\s*\*\s*.*', arg.strip()):
args.append('char*')
args_dispatch.append('(char *) params[%d]' % arg_idx)
else:
raise ValueError("Test function arguments can only be 'int' or 'char *'\n%s" % line)
arg_idx += 1
return name, args, args_dispatch
def parse_function_code(line_no, funcs_f, deps, suite_deps):
"""
:param line_no:
:param funcs_f:
:param deps:
:param suite_deps:
:return:
"""
code = '#line %d "%s"\n' % (line_no + 1, funcs_f.name)
for line in funcs_f:
line_no += 1
# Check function signature
m = re.match('.*?\s+(\w+)\s*\(', line, re.I)
if m:
# check if we have full signature i.e. split in more lines
if not re.match('.*\)', line):
for lin in funcs_f:
line += lin
line_no += 1
if re.search('.*?\)', line):
break
name, args, args_dispatch = parse_function_signature(line)
code += line.replace(name, 'test_' + name)
name = 'test_' + name
break
else:
raise InvalidFileFormat("file: %s - Test functions not found!" % funcs_f.name)
for line in funcs_f:
line_no += 1
if re.search(END_CASE_REGEX, line):
break
code += line
else:
raise InvalidFileFormat("file: %s - end case pattern [%s] not found!" % (funcs_f.name, END_CASE_REGEX))
# Add exit label if not present
if code.find('exit:') == -1:
s = code.rsplit('}', 1)
if len(s) == 2:
code = """
exit:
;;
}
""".join(s)
code += gen_function_wrapper(name, args_dispatch)
ifdef, endif = gen_deps(deps)
dispatch_code = gen_dispatch(name, suite_deps + deps)
return line_no, name, args, ifdef + code + endif, dispatch_code
def parse_functions(funcs_f):
"""
Returns functions code pieces
:param funcs_f:
:return:
"""
line_no = 0
suite_headers = ''
suite_deps = []
suite_functions = ''
func_info = {}
function_idx = 0
dispatch_code = ''
for line in funcs_f:
line_no += 1
if re.search(BEGIN_HEADER_REGEX, line):
line_no, headers = parse_suite_headers(line_no, funcs_f)
suite_headers += headers
elif re.search(BEGIN_DEP_REGEX, line):
line_no, deps = parse_suite_deps(line_no, funcs_f)
suite_deps += deps
elif re.search(BEGIN_CASE_REGEX, line):
deps = parse_function_deps(line)
line_no, func_name, args, func_code, func_dispatch = parse_function_code(line_no, funcs_f, deps, suite_deps)
suite_functions += func_code
# Generate dispatch code and enumeration info
assert func_name not in func_info, "file: %s - function %s re-declared at line %d" % \
(funcs_f.name, func_name, line_no)
func_info[func_name] = (function_idx, args)
dispatch_code += '/* Function Id: %d */\n' % function_idx
dispatch_code += func_dispatch
function_idx += 1
ifdef, endif = gen_deps(suite_deps)
func_code = ifdef + suite_functions + endif
return dispatch_code, suite_headers, func_code, func_info
def escaped_split(str, ch):
"""
Split str on character ch but ignore escaped \{ch}
:param str:
:param ch:
:return:
"""
if len(ch) > 1:
raise ValueError('Expected split character. Found string!')
out = []
part = ''
escape = False
for i in range(len(str)):
if not escape and str[i] == ch:
out.append(part)
part = ''
else:
part += str[i]
escape = not escape and str[i] == '\\'
if len(part):
out.append(part)
return out
def parse_test_data(data_f):
"""
Parses .data file
:param data_f:
:return:
"""
STATE_READ_NAME = 0
STATE_READ_ARGS = 1
state = STATE_READ_NAME
deps = []
for line in data_f:
line = line.strip()
if len(line) and line[0] == '#': # Skip comments
continue
# skip blank lines
if len(line) == 0:
continue
if state == STATE_READ_NAME:
# Read test name
name = line
state = STATE_READ_ARGS
elif state == STATE_READ_ARGS:
# Check dependencies
m = re.search('depends_on\:(.*)', line)
if m:
deps = m.group(1).split(':')
else:
# Read test vectors
parts = escaped_split(line, ':')
function = parts[0]
args = parts[1:]
yield name, function, deps, args
deps = []
state = STATE_READ_NAME
def gen_dep_check(dep_id, dep):
"""
Generate code for the dependency.
:param dep_id:
:param dep:
:return:
"""
if dep[0] == '!':
noT = '!'
dep = dep[1:]
else:
noT = ''
dep_check = '''
if ( dep_id == {id} )
{{
#if {noT}defined({macro})
return( DEPENDENCY_SUPPORTED );
#else
return( DEPENDENCY_NOT_SUPPORTED );
#endif
}}
else
'''.format(noT=noT, macro=dep, id=dep_id)
return dep_check
def gen_expression_check(exp_id, exp):
"""
Generates code for expression check
:param exp_id:
:param exp:
:return:
"""
exp_code = '''
if ( exp_id == {exp_id} )
{{
*out_value = {expression};
}}
else
'''.format(exp_id=exp_id, expression=exp)
return exp_code
def gen_from_test_data(data_f, out_data_f, func_info):
"""
Generates dependency checks, expression code and intermediate data file from test data file.
:param data_f:
:param out_data_f:
:param func_info:
:return:
"""
unique_deps = []
unique_expressions = []
dep_check_code = ''
expression_code = ''
for test_name, function_name, test_deps, test_args in parse_test_data(data_f):
out_data_f.write(test_name + '\n')
func_id, func_args = func_info['test_' + function_name]
if len(test_deps):
out_data_f.write('depends_on')
for dep in test_deps:
if dep not in unique_deps:
unique_deps.append(dep)
dep_id = unique_deps.index(dep)
dep_check_code += gen_dep_check(dep_id, dep)
else:
dep_id = unique_deps.index(dep)
out_data_f.write(':' + str(dep_id))
out_data_f.write('\n')
assert len(test_args) == len(func_args), \
"Invalid number of arguments in test %s. See function %s signature." % (test_name, function_name)
out_data_f.write(str(func_id))
for i in xrange(len(test_args)):
typ = func_args[i]
val = test_args[i]
# check if val is a non literal int val
if typ == 'int' and not re.match('\d+', val): # its an expression # FIXME: Handle hex format. Tip: instead try converting int(str, 10) and int(str, 16)
typ = 'exp'
if val not in unique_expressions:
unique_expressions.append(val)
# exp_id can be derived from len(). But for readability and consistency with case of existing let's
# use index().
exp_id = unique_expressions.index(val)
expression_code += gen_expression_check(exp_id, val)
val = exp_id
else:
val = unique_expressions.index(val)
out_data_f.write(':' + typ + ':' + str(val))
out_data_f.write('\n\n')
# void unused params
if len(dep_check_code) == 0:
dep_check_code = '(void) dep_id;\n'
if len(expression_code) == 0:
expression_code = '(void) exp_id;\n'
expression_code += '(void) out_value;\n'
return dep_check_code, expression_code
def gen_mbed_code(funcs_file, data_file, template_file, platform_file, help_file, suites_dir, c_file, out_data_file):
"""
Generate mbed-os test code.
:param funcs_file:
:param dat a_file:
:param template_file:
:param platform_file:
:param help_file:
:param suites_dir:
:param c_file:
:param out_data_file:
:return:
"""
for name, path in [('Functions file', funcs_file),
('Data file', data_file),
('Template file', template_file),
('Platform file', platform_file),
('Help code file', help_file),
('Suites dir', suites_dir)]:
if not os.path.exists(path):
raise IOError("ERROR: %s [%s] not found!" % (name, path))
snippets = {'generator_script' : os.path.basename(__file__)}
# Read helpers
with open(help_file, 'r') as help_f, open(platform_file, 'r') as platform_f:
snippets['test_common_helper_file'] = help_file
snippets['test_common_helpers'] = help_f.read()
snippets['test_platform_file'] = platform_file
snippets['platform_code'] = platform_f.read().replace('DATA_FILE',
out_data_file.replace('\\', '\\\\')) # escape '\'
# Function code
with open(funcs_file, 'r') as funcs_f, open(data_file, 'r') as data_f, open(out_data_file, 'w') as out_data_f:
dispatch_code, func_headers, func_code, func_info = parse_functions(funcs_f)
snippets['function_headers'] = func_headers
snippets['functions_code'] = func_code
snippets['dispatch_code'] = dispatch_code
dep_check_code, expression_code = gen_from_test_data(data_f, out_data_f, func_info)
snippets['dep_check_code'] = dep_check_code
snippets['expression_code'] = expression_code
snippets['test_file'] = c_file
snippets['test_main_file'] = template_file
snippets['test_case_file'] = funcs_file
snippets['test_case_data_file'] = data_file
# Read Template
# Add functions
#
with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
line_no = 1
for line in template_f.readlines():
snippets['line_no'] = line_no + 1 # Increment as it sets next line number
code = line.format(**snippets)
c_f.write(code)
line_no += 1
def check_cmd():
"""
Command line parser.
:return:
"""
parser = argparse.ArgumentParser(description='Generate code for mbed-os tests.')
parser.add_argument("-f", "--functions-file",
dest="funcs_file",
help="Functions file",
metavar="FUNCTIONS",
required=True)
parser.add_argument("-d", "--data-file",
dest="data_file",
help="Data file",
metavar="DATA",
required=True)
parser.add_argument("-t", "--template-file",
dest="template_file",
help="Template file",
metavar="TEMPLATE",
required=True)
parser.add_argument("-s", "--suites-dir",
dest="suites_dir",
help="Suites dir",
metavar="SUITES",
required=True)
parser.add_argument("--help-file",
dest="help_file",
help="Help file",
metavar="HELPER",
required=True)
parser.add_argument("-p", "--platform-file",
dest="platform_file",
help="Platform code file",
metavar="PLATFORM_FILE",
required=True)
parser.add_argument("-o", "--out-dir",
dest="out_dir",
help="Dir where generated code and scripts are copied",
metavar="OUT_DIR",
required=True)
args = parser.parse_args()
data_file_name = os.path.basename(args.data_file)
data_name = os.path.splitext(data_file_name)[0]
out_c_file = os.path.join(args.out_dir, data_name + '.c')
out_data_file = os.path.join(args.out_dir, data_file_name)
out_c_file_dir = os.path.dirname(out_c_file)
out_data_file_dir = os.path.dirname(out_data_file)
for d in [out_c_file_dir, out_data_file_dir]:
if not os.path.exists(d):
os.makedirs(d)
gen_mbed_code(args.funcs_file, args.data_file, args.template_file, args.platform_file,
args.help_file, args.suites_dir, out_c_file, out_data_file)
if __name__ == "__main__":
check_cmd()