aboutsummaryrefslogtreecommitdiff
path: root/tests/scripts/generate_test_code.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/scripts/generate_test_code.py')
-rwxr-xr-xtests/scripts/generate_test_code.py1167
1 files changed, 1167 insertions, 0 deletions
diff --git a/tests/scripts/generate_test_code.py b/tests/scripts/generate_test_code.py
new file mode 100755
index 000000000..938f24cf4
--- /dev/null
+++ b/tests/scripts/generate_test_code.py
@@ -0,0 +1,1167 @@
+#!/usr/bin/env python3
+# Test suites code generator.
+#
+# Copyright The Mbed TLS Contributors
+# SPDX-License-Identifier: Apache-2.0
+#
+# 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.
+
+"""
+This script is a key part of Mbed TLS test suites framework. For
+understanding the script it is important to understand the
+framework. This doc string contains a summary of the framework
+and explains the function of this script.
+
+Mbed TLS test suites:
+=====================
+Scope:
+------
+The test suites focus on unit testing the crypto primitives and also
+include x509 parser tests. Tests can be added to test any Mbed TLS
+module. However, the framework is not capable of testing SSL
+protocol, since that requires full stack execution and that is best
+tested as part of the system test.
+
+Test case definition:
+---------------------
+Tests are defined in a test_suite_<module>[.<optional sub module>].data
+file. A test definition contains:
+ test name
+ optional build macro dependencies
+ test function
+ test parameters
+
+Test dependencies are build macros that can be specified to indicate
+the build config in which the test is valid. For example if a test
+depends on a feature that is only enabled by defining a macro. Then
+that macro should be specified as a dependency of the test.
+
+Test function is the function that implements the test steps. This
+function is specified for different tests that perform same steps
+with different parameters.
+
+Test parameters are specified in string form separated by ':'.
+Parameters can be of type string, binary data specified as hex
+string and integer constants specified as integer, macro or
+as an expression. Following is an example test definition:
+
+ AES 128 GCM Encrypt and decrypt 8 bytes
+ depends_on:MBEDTLS_AES_C:MBEDTLS_GCM_C
+ enc_dec_buf:MBEDTLS_CIPHER_AES_128_GCM:"AES-128-GCM":128:8:-1
+
+Test functions:
+---------------
+Test functions are coded in C in test_suite_<module>.function files.
+Functions file is itself not compilable and contains special
+format patterns to specify test suite dependencies, start and end
+of functions and function dependencies. Check any existing functions
+file for example.
+
+Execution:
+----------
+Tests are executed in 3 steps:
+- Generating test_suite_<module>[.<optional sub module>].c file
+ for each corresponding .data file.
+- Building each source file into executables.
+- Running each executable and printing report.
+
+Generating C test source requires more than just the test functions.
+Following extras are required:
+- Process main()
+- Reading .data file and dispatching test cases.
+- Platform specific test case execution
+- Dependency checking
+- Integer expression evaluation
+- Test function dispatch
+
+Build dependencies and integer expressions (in the test parameters)
+are specified as strings in the .data file. Their run time value is
+not known at the generation stage. Hence, they need to be translated
+into run time evaluations. This script generates the run time checks
+for dependencies and integer expressions.
+
+Similarly, function names have to be translated into function calls.
+This script also generates code for function dispatch.
+
+The extra code mentioned here is either generated by this script
+or it comes from the input files: helpers file, platform file and
+the template file.
+
+Helper file:
+------------
+Helpers file contains common helper/utility functions and data.
+
+Platform file:
+--------------
+Platform file contains platform specific setup code and test case
+dispatch code. For example, host_test.function reads test data
+file from host's file system and dispatches tests.
+
+Template file:
+---------
+Template file for example main_test.function is a template C file in
+which generated code and code from input files is substituted to
+generate a compilable C file. It also contains skeleton functions for
+dependency checks, expression evaluation and function dispatch. These
+functions are populated with checks and return codes by this script.
+
+Template file contains "replacement" fields that are formatted
+strings processed by Python string.Template.substitute() method.
+
+This script:
+============
+Core function of this script is to fill the template file with
+code that is generated or read from helpers and platform files.
+
+This script replaces following fields in the template and generates
+the test source file:
+
+__MBEDTLS_TEST_TEMPLATE__TEST_COMMON_HELPERS
+ All common code from helpers.function
+ is substituted here.
+__MBEDTLS_TEST_TEMPLATE__FUNCTIONS_CODE
+ Test functions are substituted here
+ from the input test_suit_xyz.function
+ file. C preprocessor checks are generated
+ for the build dependencies specified
+ in the input file. This script also
+ generates wrappers for the test
+ functions with code to expand the
+ string parameters read from the data
+ file.
+__MBEDTLS_TEST_TEMPLATE__EXPRESSION_CODE
+ This script enumerates the
+ expressions in the .data file and
+ generates code to handle enumerated
+ expression Ids and return the values.
+__MBEDTLS_TEST_TEMPLATE__DEP_CHECK_CODE
+ This script enumerates all
+ build dependencies and generate
+ code to handle enumerated build
+ dependency Id and return status: if
+ the dependency is defined or not.
+__MBEDTLS_TEST_TEMPLATE__DISPATCH_CODE
+ This script enumerates the functions
+ specified in the input test data file
+ and generates the initializer for the
+ function table in the template
+ file.
+__MBEDTLS_TEST_TEMPLATE__PLATFORM_CODE
+ Platform specific setup and test
+ dispatch code.
+
+"""
+
+
+import io
+import os
+import re
+import sys
+import string
+import argparse
+
+
+BEGIN_HEADER_REGEX = r'/\*\s*BEGIN_HEADER\s*\*/'
+END_HEADER_REGEX = r'/\*\s*END_HEADER\s*\*/'
+
+BEGIN_SUITE_HELPERS_REGEX = r'/\*\s*BEGIN_SUITE_HELPERS\s*\*/'
+END_SUITE_HELPERS_REGEX = r'/\*\s*END_SUITE_HELPERS\s*\*/'
+
+BEGIN_DEP_REGEX = r'BEGIN_DEPENDENCIES'
+END_DEP_REGEX = r'END_DEPENDENCIES'
+
+BEGIN_CASE_REGEX = r'/\*\s*BEGIN_CASE\s*(?P<depends_on>.*?)\s*\*/'
+END_CASE_REGEX = r'/\*\s*END_CASE\s*\*/'
+
+DEPENDENCY_REGEX = r'depends_on:(?P<dependencies>.*)'
+C_IDENTIFIER_REGEX = r'!?[a-z_][a-z0-9_]*'
+CONDITION_OPERATOR_REGEX = r'[!=]=|[<>]=?'
+# forbid 0ddd which might be accidentally octal or accidentally decimal
+CONDITION_VALUE_REGEX = r'[-+]?(0x[0-9a-f]+|0|[1-9][0-9]*)'
+CONDITION_REGEX = r'({})(?:\s*({})\s*({}))?$'.format(C_IDENTIFIER_REGEX,
+ CONDITION_OPERATOR_REGEX,
+ CONDITION_VALUE_REGEX)
+TEST_FUNCTION_VALIDATION_REGEX = r'\s*void\s+(?P<func_name>\w+)\s*\('
+INT_CHECK_REGEX = r'int\s+.*'
+CHAR_CHECK_REGEX = r'char\s*\*\s*.*'
+DATA_T_CHECK_REGEX = r'data_t\s*\*\s*.*'
+FUNCTION_ARG_LIST_END_REGEX = r'.*\)'
+EXIT_LABEL_REGEX = r'^exit:'
+
+
+class GeneratorInputError(Exception):
+ """
+ Exception to indicate error in the input files to this script.
+ This includes missing patterns, test function names and other
+ parsing errors.
+ """
+ pass
+
+
+class FileWrapper(io.FileIO):
+ """
+ This class extends built-in io.FileIO class with attribute line_no,
+ that indicates line number for the line that is read.
+ """
+
+ def __init__(self, file_name):
+ """
+ Instantiate the base class and initialize the line number to 0.
+
+ :param file_name: File path to open.
+ """
+ super(FileWrapper, self).__init__(file_name, 'r')
+ self._line_no = 0
+
+ def next(self):
+ """
+ Python 2 iterator method. This method overrides base class's
+ next method and extends the next method to count the line
+ numbers as each line is read.
+
+ It works for both Python 2 and Python 3 by checking iterator
+ method name in the base iterator object.
+
+ :return: Line read from file.
+ """
+ parent = super(FileWrapper, self)
+ if hasattr(parent, '__next__'):
+ line = parent.__next__() # Python 3
+ else:
+ line = parent.next() # Python 2 # pylint: disable=no-member
+ if line is not None:
+ self._line_no += 1
+ # Convert byte array to string with correct encoding and
+ # strip any whitespaces added in the decoding process.
+ return line.decode(sys.getdefaultencoding()).rstrip() + '\n'
+ return None
+
+ # Python 3 iterator method
+ __next__ = next
+
+ def get_line_no(self):
+ """
+ Gives current line number.
+ """
+ return self._line_no
+
+ line_no = property(get_line_no)
+
+
+def split_dep(dep):
+ """
+ Split NOT character '!' from dependency. Used by gen_dependencies()
+
+ :param dep: Dependency list
+ :return: string tuple. Ex: ('!', MACRO) for !MACRO and ('', MACRO) for
+ MACRO.
+ """
+ return ('!', dep[1:]) if dep[0] == '!' else ('', dep)
+
+
+def gen_dependencies(dependencies):
+ """
+ Test suite data and functions specifies compile time dependencies.
+ This function generates C preprocessor code from the input
+ dependency list. Caller uses the generated preprocessor code to
+ wrap dependent code.
+ A dependency in the input list can have a leading '!' character
+ to negate a condition. '!' is separated from the dependency using
+ function split_dep() and proper preprocessor check is generated
+ accordingly.
+
+ :param dependencies: List of dependencies.
+ :return: if defined and endif code with macro annotations for
+ readability.
+ """
+ dep_start = ''.join(['#if %sdefined(%s)\n' % (x, y) for x, y in
+ map(split_dep, dependencies)])
+ dep_end = ''.join(['#endif /* %s */\n' %
+ x for x in reversed(dependencies)])
+
+ return dep_start, dep_end
+
+
+def gen_dependencies_one_line(dependencies):
+ """
+ Similar to gen_dependencies() but generates dependency checks in one line.
+ Useful for generating code with #else block.
+
+ :param dependencies: List of dependencies.
+ :return: Preprocessor check code
+ """
+ defines = '#if ' if dependencies else ''
+ defines += ' && '.join(['%sdefined(%s)' % (x, y) for x, y in map(
+ split_dep, dependencies)])
+ return defines
+
+
+def gen_function_wrapper(name, local_vars, args_dispatch):
+ """
+ Creates test function wrapper code. A wrapper has the code to
+ unpack parameters from parameters[] array.
+
+ :param name: Test function name
+ :param local_vars: Local variables declaration code
+ :param args_dispatch: List of dispatch arguments.
+ Ex: ['(char *)params[0]', '*((int *)params[1])']
+ :return: Test function wrapper.
+ """
+ # Then create the wrapper
+ wrapper = '''
+void {name}_wrapper( void ** params )
+{{
+{unused_params}{locals}
+ {name}( {args} );
+}}
+'''.format(name=name,
+ unused_params='' if args_dispatch else ' (void)params;\n',
+ args=', '.join(args_dispatch),
+ locals=local_vars)
+ return wrapper
+
+
+def gen_dispatch(name, dependencies):
+ """
+ Test suite code template main_test.function defines a C function
+ array to contain test case functions. This function generates an
+ initializer entry for a function in that array. The entry is
+ composed of a compile time check for the test function
+ dependencies. At compile time the test function is assigned when
+ dependencies are met, else NULL is assigned.
+
+ :param name: Test function name
+ :param dependencies: List of dependencies
+ :return: Dispatch code.
+ """
+ if dependencies:
+ preprocessor_check = gen_dependencies_one_line(dependencies)
+ dispatch_code = '''
+{preprocessor_check}
+ {name}_wrapper,
+#else
+ NULL,
+#endif
+'''.format(preprocessor_check=preprocessor_check, name=name)
+ else:
+ dispatch_code = '''
+ {name}_wrapper,
+'''.format(name=name)
+
+ return dispatch_code
+
+
+def parse_until_pattern(funcs_f, end_regex):
+ """
+ Matches pattern end_regex to the lines read from the file object.
+ Returns the lines read until end pattern is matched.
+
+ :param funcs_f: file object for .function file
+ :param end_regex: Pattern to stop parsing
+ :return: Lines read before the end pattern
+ """
+ headers = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
+ for line in funcs_f:
+ if re.search(end_regex, line):
+ break
+ headers += line
+ else:
+ raise GeneratorInputError("file: %s - end pattern [%s] not found!" %
+ (funcs_f.name, end_regex))
+
+ return headers
+
+
+def validate_dependency(dependency):
+ """
+ Validates a C macro and raises GeneratorInputError on invalid input.
+ :param dependency: Input macro dependency
+ :return: input dependency stripped of leading & trailing white spaces.
+ """
+ dependency = dependency.strip()
+ if not re.match(CONDITION_REGEX, dependency, re.I):
+ raise GeneratorInputError('Invalid dependency %s' % dependency)
+ return dependency
+
+
+def parse_dependencies(inp_str):
+ """
+ Parses dependencies out of inp_str, validates them and returns a
+ list of macros.
+
+ :param inp_str: Input string with macros delimited by ':'.
+ :return: list of dependencies
+ """
+ dependencies = list(map(validate_dependency, inp_str.split(':')))
+ return dependencies
+
+
+def parse_suite_dependencies(funcs_f):
+ """
+ Parses test suite dependencies specified at the top of a
+ .function file, that starts with pattern BEGIN_DEPENDENCIES
+ and end with END_DEPENDENCIES. Dependencies are specified
+ after pattern 'depends_on:' and are delimited by ':'.
+
+ :param funcs_f: file object for .function file
+ :return: List of test suite dependencies.
+ """
+ dependencies = []
+ for line in funcs_f:
+ match = re.search(DEPENDENCY_REGEX, line.strip())
+ if match:
+ try:
+ dependencies = parse_dependencies(match.group('dependencies'))
+ except GeneratorInputError as error:
+ raise GeneratorInputError(
+ str(error) + " - %s:%d" % (funcs_f.name, funcs_f.line_no))
+ if re.search(END_DEP_REGEX, line):
+ break
+ else:
+ raise GeneratorInputError("file: %s - end dependency pattern [%s]"
+ " not found!" % (funcs_f.name,
+ END_DEP_REGEX))
+
+ return dependencies
+
+
+def parse_function_dependencies(line):
+ """
+ Parses function dependencies, that are in the same line as
+ comment BEGIN_CASE. Dependencies are specified after pattern
+ 'depends_on:' and are delimited by ':'.
+
+ :param line: Line from .function file that has dependencies.
+ :return: List of dependencies.
+ """
+ dependencies = []
+ match = re.search(BEGIN_CASE_REGEX, line)
+ dep_str = match.group('depends_on')
+ if dep_str:
+ match = re.search(DEPENDENCY_REGEX, dep_str)
+ if match:
+ dependencies += parse_dependencies(match.group('dependencies'))
+
+ return dependencies
+
+
+def parse_function_arguments(line):
+ """
+ Parses test function signature for validation and generates
+ a dispatch wrapper function that translates input test vectors
+ read from the data file into test function arguments.
+
+ :param line: Line from .function file that has a function
+ signature.
+ :return: argument list, local variables for
+ wrapper function and argument dispatch code.
+ """
+ args = []
+ local_vars = ''
+ args_dispatch = []
+ arg_idx = 0
+ # Remove characters before arguments
+ line = line[line.find('(') + 1:]
+ # Process arguments, ex: <type> arg1, <type> arg2 )
+ # This script assumes that the argument list is terminated by ')'
+ # i.e. the test functions will not have a function pointer
+ # argument.
+ for arg in line[:line.find(')')].split(','):
+ arg = arg.strip()
+ if arg == '':
+ continue
+ if re.search(INT_CHECK_REGEX, arg.strip()):
+ args.append('int')
+ args_dispatch.append('*( (int *) params[%d] )' % arg_idx)
+ elif re.search(CHAR_CHECK_REGEX, arg.strip()):
+ args.append('char*')
+ args_dispatch.append('(char *) params[%d]' % arg_idx)
+ elif re.search(DATA_T_CHECK_REGEX, arg.strip()):
+ args.append('hex')
+ # create a structure
+ pointer_initializer = '(uint8_t *) params[%d]' % arg_idx
+ len_initializer = '*( (uint32_t *) params[%d] )' % (arg_idx+1)
+ local_vars += """ data_t data%d = {%s, %s};
+""" % (arg_idx, pointer_initializer, len_initializer)
+
+ args_dispatch.append('&data%d' % arg_idx)
+ arg_idx += 1
+ else:
+ raise ValueError("Test function arguments can only be 'int', "
+ "'char *' or 'data_t'\n%s" % line)
+ arg_idx += 1
+
+ return args, local_vars, args_dispatch
+
+
+def generate_function_code(name, code, local_vars, args_dispatch,
+ dependencies):
+ """
+ Generate function code with preprocessor checks and parameter dispatch
+ wrapper.
+
+ :param name: Function name
+ :param code: Function code
+ :param local_vars: Local variables for function wrapper
+ :param args_dispatch: Argument dispatch code
+ :param dependencies: Preprocessor dependencies list
+ :return: Final function code
+ """
+ # Add exit label if not present
+ if code.find('exit:') == -1:
+ split_code = code.rsplit('}', 1)
+ if len(split_code) == 2:
+ code = """exit:
+ ;
+}""".join(split_code)
+
+ code += gen_function_wrapper(name, local_vars, args_dispatch)
+ preprocessor_check_start, preprocessor_check_end = \
+ gen_dependencies(dependencies)
+ return preprocessor_check_start + code + preprocessor_check_end
+
+
+def parse_function_code(funcs_f, dependencies, suite_dependencies):
+ """
+ Parses out a function from function file object and generates
+ function and dispatch code.
+
+ :param funcs_f: file object of the functions file.
+ :param dependencies: List of dependencies
+ :param suite_dependencies: List of test suite dependencies
+ :return: Function name, arguments, function code and dispatch code.
+ """
+ line_directive = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
+ code = ''
+ has_exit_label = False
+ for line in funcs_f:
+ # Check function signature. Function signature may be split
+ # across multiple lines. Here we try to find the start of
+ # arguments list, then remove '\n's and apply the regex to
+ # detect function start.
+ up_to_arg_list_start = code + line[:line.find('(') + 1]
+ match = re.match(TEST_FUNCTION_VALIDATION_REGEX,
+ up_to_arg_list_start.replace('\n', ' '), re.I)
+ if match:
+ # check if we have full signature i.e. split in more lines
+ name = match.group('func_name')
+ if not re.match(FUNCTION_ARG_LIST_END_REGEX, line):
+ for lin in funcs_f:
+ line += lin
+ if re.search(FUNCTION_ARG_LIST_END_REGEX, line):
+ break
+ args, local_vars, args_dispatch = parse_function_arguments(
+ line)
+ code += line
+ break
+ code += line
+ else:
+ raise GeneratorInputError("file: %s - Test functions not found!" %
+ funcs_f.name)
+
+ # Prefix test function name with 'test_'
+ code = code.replace(name, 'test_' + name, 1)
+ name = 'test_' + name
+
+ for line in funcs_f:
+ if re.search(END_CASE_REGEX, line):
+ break
+ if not has_exit_label:
+ has_exit_label = \
+ re.search(EXIT_LABEL_REGEX, line.strip()) is not None
+ code += line
+ else:
+ raise GeneratorInputError("file: %s - end case pattern [%s] not "
+ "found!" % (funcs_f.name, END_CASE_REGEX))
+
+ code = line_directive + code
+ code = generate_function_code(name, code, local_vars, args_dispatch,
+ dependencies)
+ dispatch_code = gen_dispatch(name, suite_dependencies + dependencies)
+ return (name, args, code, dispatch_code)
+
+
+def parse_functions(funcs_f):
+ """
+ Parses a test_suite_xxx.function file and returns information
+ for generating a C source file for the test suite.
+
+ :param funcs_f: file object of the functions file.
+ :return: List of test suite dependencies, test function dispatch
+ code, function code and a dict with function identifiers
+ and arguments info.
+ """
+ suite_helpers = ''
+ suite_dependencies = []
+ suite_functions = ''
+ func_info = {}
+ function_idx = 0
+ dispatch_code = ''
+ for line in funcs_f:
+ if re.search(BEGIN_HEADER_REGEX, line):
+ suite_helpers += parse_until_pattern(funcs_f, END_HEADER_REGEX)
+ elif re.search(BEGIN_SUITE_HELPERS_REGEX, line):
+ suite_helpers += parse_until_pattern(funcs_f,
+ END_SUITE_HELPERS_REGEX)
+ elif re.search(BEGIN_DEP_REGEX, line):
+ suite_dependencies += parse_suite_dependencies(funcs_f)
+ elif re.search(BEGIN_CASE_REGEX, line):
+ try:
+ dependencies = parse_function_dependencies(line)
+ except GeneratorInputError as error:
+ raise GeneratorInputError(
+ "%s:%d: %s" % (funcs_f.name, funcs_f.line_no,
+ str(error)))
+ func_name, args, func_code, func_dispatch =\
+ parse_function_code(funcs_f, dependencies, suite_dependencies)
+ suite_functions += func_code
+ # Generate dispatch code and enumeration info
+ if func_name in func_info:
+ raise GeneratorInputError(
+ "file: %s - function %s re-declared at line %d" %
+ (funcs_f.name, func_name, funcs_f.line_no))
+ func_info[func_name] = (function_idx, args)
+ dispatch_code += '/* Function Id: %d */\n' % function_idx
+ dispatch_code += func_dispatch
+ function_idx += 1
+
+ func_code = (suite_helpers +
+ suite_functions).join(gen_dependencies(suite_dependencies))
+ return suite_dependencies, dispatch_code, func_code, func_info
+
+
+def escaped_split(inp_str, split_char):
+ """
+ Split inp_str on character split_char but ignore if escaped.
+ Since, return value is used to write back to the intermediate
+ data file, any escape characters in the input are retained in the
+ output.
+
+ :param inp_str: String to split
+ :param split_char: Split character
+ :return: List of splits
+ """
+ if len(split_char) > 1:
+ raise ValueError('Expected split character. Found string!')
+ out = re.sub(r'(\\.)|' + split_char,
+ lambda m: m.group(1) or '\n', inp_str,
+ len(inp_str)).split('\n')
+ out = [x for x in out if x]
+ return out
+
+
+def parse_test_data(data_f):
+ """
+ Parses .data file for each test case name, test function name,
+ test dependencies and test arguments. This information is
+ correlated with the test functions file for generating an
+ intermediate data file replacing the strings for test function
+ names, dependencies and integer constant expressions with
+ identifiers. Mainly for optimising space for on-target
+ execution.
+
+ :param data_f: file object of the data file.
+ :return: Generator that yields test name, function name,
+ dependency list and function argument list.
+ """
+ __state_read_name = 0
+ __state_read_args = 1
+ state = __state_read_name
+ dependencies = []
+ name = ''
+ for line in data_f:
+ line = line.strip()
+ # Skip comments
+ if line.startswith('#'):
+ continue
+
+ # Blank line indicates end of test
+ if not line:
+ if state == __state_read_args:
+ raise GeneratorInputError("[%s:%d] Newline before arguments. "
+ "Test function and arguments "
+ "missing for %s" %
+ (data_f.name, data_f.line_no, name))
+ continue
+
+ if state == __state_read_name:
+ # Read test name
+ name = line
+ state = __state_read_args
+ elif state == __state_read_args:
+ # Check dependencies
+ match = re.search(DEPENDENCY_REGEX, line)
+ if match:
+ try:
+ dependencies = parse_dependencies(
+ match.group('dependencies'))
+ except GeneratorInputError as error:
+ raise GeneratorInputError(
+ str(error) + " - %s:%d" %
+ (data_f.name, data_f.line_no))
+ else:
+ # Read test vectors
+ parts = escaped_split(line, ':')
+ test_function = parts[0]
+ args = parts[1:]
+ yield name, test_function, dependencies, args
+ dependencies = []
+ state = __state_read_name
+ if state == __state_read_args:
+ raise GeneratorInputError("[%s:%d] Newline before arguments. "
+ "Test function and arguments missing for "
+ "%s" % (data_f.name, data_f.line_no, name))
+
+
+def gen_dep_check(dep_id, dep):
+ """
+ Generate code for checking dependency with the associated
+ identifier.
+
+ :param dep_id: Dependency identifier
+ :param dep: Dependency macro
+ :return: Dependency check code
+ """
+ if dep_id < 0:
+ raise GeneratorInputError("Dependency Id should be a positive "
+ "integer.")
+ _not, dep = ('!', dep[1:]) if dep[0] == '!' else ('', dep)
+ if not dep:
+ raise GeneratorInputError("Dependency should not be an empty string.")
+
+ dependency = re.match(CONDITION_REGEX, dep, re.I)
+ if not dependency:
+ raise GeneratorInputError('Invalid dependency %s' % dep)
+
+ _defined = '' if dependency.group(2) else 'defined'
+ _cond = dependency.group(2) if dependency.group(2) else ''
+ _value = dependency.group(3) if dependency.group(3) else ''
+
+ dep_check = '''
+ case {id}:
+ {{
+#if {_not}{_defined}({macro}{_cond}{_value})
+ ret = DEPENDENCY_SUPPORTED;
+#else
+ ret = DEPENDENCY_NOT_SUPPORTED;
+#endif
+ }}
+ break;'''.format(_not=_not, _defined=_defined,
+ macro=dependency.group(1), id=dep_id,
+ _cond=_cond, _value=_value)
+ return dep_check
+
+
+def gen_expression_check(exp_id, exp):
+ """
+ Generates code for evaluating an integer expression using
+ associated expression Id.
+
+ :param exp_id: Expression Identifier
+ :param exp: Expression/Macro
+ :return: Expression check code
+ """
+ if exp_id < 0:
+ raise GeneratorInputError("Expression Id should be a positive "
+ "integer.")
+ if not exp:
+ raise GeneratorInputError("Expression should not be an empty string.")
+ exp_code = '''
+ case {exp_id}:
+ {{
+ *out_value = {expression};
+ }}
+ break;'''.format(exp_id=exp_id, expression=exp)
+ return exp_code
+
+
+def write_dependencies(out_data_f, test_dependencies, unique_dependencies):
+ """
+ Write dependencies to intermediate test data file, replacing
+ the string form with identifiers. Also, generates dependency
+ check code.
+
+ :param out_data_f: Output intermediate data file
+ :param test_dependencies: Dependencies
+ :param unique_dependencies: Mutable list to track unique dependencies
+ that are global to this re-entrant function.
+ :return: returns dependency check code.
+ """
+ dep_check_code = ''
+ if test_dependencies:
+ out_data_f.write('depends_on')
+ for dep in test_dependencies:
+ if dep not in unique_dependencies:
+ unique_dependencies.append(dep)
+ dep_id = unique_dependencies.index(dep)
+ dep_check_code += gen_dep_check(dep_id, dep)
+ else:
+ dep_id = unique_dependencies.index(dep)
+ out_data_f.write(':' + str(dep_id))
+ out_data_f.write('\n')
+ return dep_check_code
+
+
+def write_parameters(out_data_f, test_args, func_args, unique_expressions):
+ """
+ Writes test parameters to the intermediate data file, replacing
+ the string form with identifiers. Also, generates expression
+ check code.
+
+ :param out_data_f: Output intermediate data file
+ :param test_args: Test parameters
+ :param func_args: Function arguments
+ :param unique_expressions: Mutable list to track unique
+ expressions that are global to this re-entrant function.
+ :return: Returns expression check code.
+ """
+ expression_code = ''
+ for i, _ in enumerate(test_args):
+ typ = func_args[i]
+ val = test_args[i]
+
+ # check if val is a non literal int val (i.e. an expression)
+ if typ == 'int' and not re.match(r'(\d+|0x[0-9a-f]+)$',
+ val, re.I):
+ 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')
+ return expression_code
+
+
+def gen_suite_dep_checks(suite_dependencies, dep_check_code, expression_code):
+ """
+ Generates preprocessor checks for test suite dependencies.
+
+ :param suite_dependencies: Test suite dependencies read from the
+ .function file.
+ :param dep_check_code: Dependency check code
+ :param expression_code: Expression check code
+ :return: Dependency and expression code guarded by test suite
+ dependencies.
+ """
+ if suite_dependencies:
+ preprocessor_check = gen_dependencies_one_line(suite_dependencies)
+ dep_check_code = '''
+{preprocessor_check}
+{code}
+#endif
+'''.format(preprocessor_check=preprocessor_check, code=dep_check_code)
+ expression_code = '''
+{preprocessor_check}
+{code}
+#endif
+'''.format(preprocessor_check=preprocessor_check, code=expression_code)
+ return dep_check_code, expression_code
+
+
+def gen_from_test_data(data_f, out_data_f, func_info, suite_dependencies):
+ """
+ This function reads test case name, dependencies and test vectors
+ from the .data file. This information is correlated with the test
+ functions file for generating an intermediate data file replacing
+ the strings for test function names, dependencies and integer
+ constant expressions with identifiers. Mainly for optimising
+ space for on-target execution.
+ It also generates test case dependency check code and expression
+ evaluation code.
+
+ :param data_f: Data file object
+ :param out_data_f: Output intermediate data file
+ :param func_info: Dict keyed by function and with function id
+ and arguments info
+ :param suite_dependencies: Test suite dependencies
+ :return: Returns dependency and expression check code
+ """
+ unique_dependencies = []
+ unique_expressions = []
+ dep_check_code = ''
+ expression_code = ''
+ for test_name, function_name, test_dependencies, test_args in \
+ parse_test_data(data_f):
+ out_data_f.write(test_name + '\n')
+
+ # Write dependencies
+ dep_check_code += write_dependencies(out_data_f, test_dependencies,
+ unique_dependencies)
+
+ # Write test function name
+ test_function_name = 'test_' + function_name
+ if test_function_name not in func_info:
+ raise GeneratorInputError("Function %s not found!" %
+ test_function_name)
+ func_id, func_args = func_info[test_function_name]
+ out_data_f.write(str(func_id))
+
+ # Write parameters
+ if len(test_args) != len(func_args):
+ raise GeneratorInputError("Invalid number of arguments in test "
+ "%s. See function %s signature." %
+ (test_name, function_name))
+ expression_code += write_parameters(out_data_f, test_args, func_args,
+ unique_expressions)
+
+ # Write a newline as test case separator
+ out_data_f.write('\n')
+
+ dep_check_code, expression_code = gen_suite_dep_checks(
+ suite_dependencies, dep_check_code, expression_code)
+ return dep_check_code, expression_code
+
+
+def add_input_info(funcs_file, data_file, template_file,
+ c_file, snippets):
+ """
+ Add generator input info in snippets.
+
+ :param funcs_file: Functions file object
+ :param data_file: Data file object
+ :param template_file: Template file object
+ :param c_file: Output C file object
+ :param snippets: Dictionary to contain code pieces to be
+ substituted in the template.
+ :return:
+ """
+ snippets['test_file'] = c_file
+ snippets['test_main_file'] = template_file
+ snippets['test_case_file'] = funcs_file
+ snippets['test_case_data_file'] = data_file
+
+
+def read_code_from_input_files(platform_file, helpers_file,
+ out_data_file, snippets):
+ """
+ Read code from input files and create substitutions for replacement
+ strings in the template file.
+
+ :param platform_file: Platform file object
+ :param helpers_file: Helper functions file object
+ :param out_data_file: Output intermediate data file object
+ :param snippets: Dictionary to contain code pieces to be
+ substituted in the template.
+ :return:
+ """
+ # Read helpers
+ with open(helpers_file, 'r') as help_f, open(platform_file, 'r') as \
+ platform_f:
+ snippets['test_common_helper_file'] = helpers_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 '\'
+
+
+def write_test_source_file(template_file, c_file, snippets):
+ """
+ Write output source file with generated source code.
+
+ :param template_file: Template file name
+ :param c_file: Output source file
+ :param snippets: Generated and code snippets
+ :return:
+ """
+
+ # Create a placeholder pattern with the correct named capture groups
+ # to override the default provided with Template.
+ # Match nothing (no way of escaping placeholders).
+ escaped = "(?P<escaped>(?!))"
+ # Match the "__MBEDTLS_TEST_TEMPLATE__PLACEHOLDER_NAME" pattern.
+ named = "__MBEDTLS_TEST_TEMPLATE__(?P<named>[A-Z][_A-Z0-9]*)"
+ # Match nothing (no braced placeholder syntax).
+ braced = "(?P<braced>(?!))"
+ # If not already matched, a "__MBEDTLS_TEST_TEMPLATE__" prefix is invalid.
+ invalid = "(?P<invalid>__MBEDTLS_TEST_TEMPLATE__)"
+ placeholder_pattern = re.compile("|".join([escaped, named, braced, invalid]))
+
+ with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
+ for line_no, line in enumerate(template_f.readlines(), 1):
+ # Update line number. +1 as #line directive sets next line number
+ snippets['line_no'] = line_no + 1
+ template = string.Template(line)
+ template.pattern = placeholder_pattern
+ snippets = {k.upper():v for (k, v) in snippets.items()}
+ code = template.substitute(**snippets)
+ c_f.write(code)
+
+
+def parse_function_file(funcs_file, snippets):
+ """
+ Parse function file and generate function dispatch code.
+
+ :param funcs_file: Functions file name
+ :param snippets: Dictionary to contain code pieces to be
+ substituted in the template.
+ :return:
+ """
+ with FileWrapper(funcs_file) as funcs_f:
+ suite_dependencies, dispatch_code, func_code, func_info = \
+ parse_functions(funcs_f)
+ snippets['functions_code'] = func_code
+ snippets['dispatch_code'] = dispatch_code
+ return suite_dependencies, func_info
+
+
+def generate_intermediate_data_file(data_file, out_data_file,
+ suite_dependencies, func_info, snippets):
+ """
+ Generates intermediate data file from input data file and
+ information read from functions file.
+
+ :param data_file: Data file name
+ :param out_data_file: Output/Intermediate data file
+ :param suite_dependencies: List of suite dependencies.
+ :param func_info: Function info parsed from functions file.
+ :param snippets: Dictionary to contain code pieces to be
+ substituted in the template.
+ :return:
+ """
+ with FileWrapper(data_file) as data_f, \
+ open(out_data_file, 'w') as out_data_f:
+ dep_check_code, expression_code = gen_from_test_data(
+ data_f, out_data_f, func_info, suite_dependencies)
+ snippets['dep_check_code'] = dep_check_code
+ snippets['expression_code'] = expression_code
+
+
+def generate_code(**input_info):
+ """
+ Generates C source code from test suite file, data file, common
+ helpers file and platform file.
+
+ input_info expands to following parameters:
+ funcs_file: Functions file object
+ data_file: Data file object
+ template_file: Template file object
+ platform_file: Platform file object
+ helpers_file: Helper functions file object
+ suites_dir: Test suites dir
+ c_file: Output C file object
+ out_data_file: Output intermediate data file object
+ :return:
+ """
+ funcs_file = input_info['funcs_file']
+ data_file = input_info['data_file']
+ template_file = input_info['template_file']
+ platform_file = input_info['platform_file']
+ helpers_file = input_info['helpers_file']
+ suites_dir = input_info['suites_dir']
+ c_file = input_info['c_file']
+ out_data_file = input_info['out_data_file']
+ for name, path in [('Functions file', funcs_file),
+ ('Data file', data_file),
+ ('Template file', template_file),
+ ('Platform file', platform_file),
+ ('Helpers code file', helpers_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_code_from_input_files(platform_file, helpers_file,
+ out_data_file, snippets)
+ add_input_info(funcs_file, data_file, template_file,
+ c_file, snippets)
+ suite_dependencies, func_info = parse_function_file(funcs_file, snippets)
+ generate_intermediate_data_file(data_file, out_data_file,
+ suite_dependencies, func_info, snippets)
+ write_test_source_file(template_file, c_file, snippets)
+
+
+def main():
+ """
+ Command line parser.
+
+ :return:
+ """
+ parser = argparse.ArgumentParser(
+ description='Dynamically generate test suite code.')
+
+ parser.add_argument("-f", "--functions-file",
+ dest="funcs_file",
+ help="Functions file",
+ metavar="FUNCTIONS_FILE",
+ required=True)
+
+ parser.add_argument("-d", "--data-file",
+ dest="data_file",
+ help="Data file",
+ metavar="DATA_FILE",
+ required=True)
+
+ parser.add_argument("-t", "--template-file",
+ dest="template_file",
+ help="Template file",
+ metavar="TEMPLATE_FILE",
+ required=True)
+
+ parser.add_argument("-s", "--suites-dir",
+ dest="suites_dir",
+ help="Suites dir",
+ metavar="SUITES_DIR",
+ required=True)
+
+ parser.add_argument("--helpers-file",
+ dest="helpers_file",
+ help="Helpers file",
+ metavar="HELPERS_FILE",
+ 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_name + '.datax')
+
+ out_c_file_dir = os.path.dirname(out_c_file)
+ out_data_file_dir = os.path.dirname(out_data_file)
+ for directory in [out_c_file_dir, out_data_file_dir]:
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ generate_code(funcs_file=args.funcs_file, data_file=args.data_file,
+ template_file=args.template_file,
+ platform_file=args.platform_file,
+ helpers_file=args.helpers_file, suites_dir=args.suites_dir,
+ c_file=out_c_file, out_data_file=out_data_file)
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except GeneratorInputError as err:
+ sys.exit("%s: input error: %s" %
+ (os.path.basename(sys.argv[0]), str(err)))