| #!/usr/bin/env python3 |
| # Test suites code generator. |
| # |
| # Copyright (C) 2018, Arm Limited, All Rights Reserved |
| # 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 file is part of Mbed TLS (https://tls.mbed.org) |
| |
| """ |
| 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. |
| In case of on-target target_test.function tests are not dispatched |
| on target. Target code is kept minimum and only test functions are |
| dispatched. Test case dispatch is done on the host using tools like |
| Greentea. |
| |
| 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: |
| |
| $test_common_helpers <-- All common code from helpers.function |
| is substituted here. |
| $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. |
| $expression_code <-- This script enumerates the |
| expressions in the .data file and |
| generates code to handle enumerated |
| expression Ids and return the values. |
| $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. |
| $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. |
| $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_]*$' |
| 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, object): |
| """ |
| 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 |
| 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(C_IDENTIFIER_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 = [dep for dep in 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.") |
| dep_check = ''' |
| case {id}: |
| {{ |
| #if {_not}defined({macro}) |
| ret = DEPENDENCY_SUPPORTED; |
| #else |
| ret = DEPENDENCY_NOT_SUPPORTED; |
| #endif |
| }} |
| break;'''.format(_not=_not, macro=dep, id=dep_id) |
| 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: |
| """ |
| 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 |
| code = string.Template(line).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))) |