12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145 |
- #!/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:
- $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_]*'
- 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:
- """
- 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)))
|