2 # Test suites code generator.
4 # Copyright (C) 2018, Arm Limited, All Rights Reserved
5 # SPDX-License-Identifier: Apache-2.0
7 # Licensed under the Apache License, Version 2.0 (the "License"); you may
8 # not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # This file is part of Mbed TLS (https://tls.mbed.org)
22 This script is a key part of Mbed TLS test suites framework. For
23 understanding the script it is important to understand the
24 framework. This doc string contains a summary of the framework
25 and explains the function of this script.
31 The test suites focus on unit testing the crypto primitives and also
32 include x509 parser tests. Tests can be added to test any Mbed TLS
33 module. However, the framework is not capable of testing SSL
34 protocol, since that requires full stack execution and that is best
35 tested as part of the system test.
39 Tests are defined in a test_suite_<module>[.<optional sub module>].data
40 file. A test definition contains:
42 optional build macro dependencies
46 Test dependencies are build macros that can be specified to indicate
47 the build config in which the test is valid. For example if a test
48 depends on a feature that is only enabled by defining a macro. Then
49 that macro should be specified as a dependency of the test.
51 Test function is the function that implements the test steps. This
52 function is specified for different tests that perform same steps
53 with different parameters.
55 Test parameters are specified in string form separated by ':'.
56 Parameters can be of type string, binary data specified as hex
57 string and integer constants specified as integer, macro or
58 as an expression. Following is an example test definition:
60 AES 128 GCM Encrypt and decrypt 8 bytes
61 depends_on:MBEDTLS_AES_C:MBEDTLS_GCM_C
62 enc_dec_buf:MBEDTLS_CIPHER_AES_128_GCM:"AES-128-GCM":128:8:-1
66 Test functions are coded in C in test_suite_<module>.function files.
67 Functions file is itself not compilable and contains special
68 format patterns to specify test suite dependencies, start and end
69 of functions and function dependencies. Check any existing functions
74 Tests are executed in 3 steps:
75 - Generating test_suite_<module>[.<optional sub module>].c file
76 for each corresponding .data file.
77 - Building each source file into executables.
78 - Running each executable and printing report.
80 Generating C test source requires more than just the test functions.
81 Following extras are required:
83 - Reading .data file and dispatching test cases.
84 - Platform specific test case execution
86 - Integer expression evaluation
87 - Test function dispatch
89 Build dependencies and integer expressions (in the test parameters)
90 are specified as strings in the .data file. Their run time value is
91 not known at the generation stage. Hence, they need to be translated
92 into run time evaluations. This script generates the run time checks
93 for dependencies and integer expressions.
95 Similarly, function names have to be translated into function calls.
96 This script also generates code for function dispatch.
98 The extra code mentioned here is either generated by this script
99 or it comes from the input files: helpers file, platform file and
104 Helpers file contains common helper/utility functions and data.
108 Platform file contains platform specific setup code and test case
109 dispatch code. For example, host_test.function reads test data
110 file from host's file system and dispatches tests.
111 In case of on-target target_test.function tests are not dispatched
112 on target. Target code is kept minimum and only test functions are
113 dispatched. Test case dispatch is done on the host using tools like
118 Template file for example main_test.function is a template C file in
119 which generated code and code from input files is substituted to
120 generate a compilable C file. It also contains skeleton functions for
121 dependency checks, expression evaluation and function dispatch. These
122 functions are populated with checks and return codes by this script.
124 Template file contains "replacement" fields that are formatted
125 strings processed by Python string.Template.substitute() method.
129 Core function of this script is to fill the template file with
130 code that is generated or read from helpers and platform files.
132 This script replaces following fields in the template and generates
133 the test source file:
135 $test_common_helpers <-- All common code from helpers.function
137 $functions_code <-- Test functions are substituted here
138 from the input test_suit_xyz.function
139 file. C preprocessor checks are generated
140 for the build dependencies specified
141 in the input file. This script also
142 generates wrappers for the test
143 functions with code to expand the
144 string parameters read from the data
146 $expression_code <-- This script enumerates the
147 expressions in the .data file and
148 generates code to handle enumerated
149 expression Ids and return the values.
150 $dep_check_code <-- This script enumerates all
151 build dependencies and generate
152 code to handle enumerated build
153 dependency Id and return status: if
154 the dependency is defined or not.
155 $dispatch_code <-- This script enumerates the functions
156 specified in the input test data file
157 and generates the initializer for the
158 function table in the template
160 $platform_code <-- Platform specific setup and test
174 BEGIN_HEADER_REGEX = r'/\*\s*BEGIN_HEADER\s*\*/'
175 END_HEADER_REGEX = r'/\*\s*END_HEADER\s*\*/'
177 BEGIN_SUITE_HELPERS_REGEX = r'/\*\s*BEGIN_SUITE_HELPERS\s*\*/'
178 END_SUITE_HELPERS_REGEX = r'/\*\s*END_SUITE_HELPERS\s*\*/'
180 BEGIN_DEP_REGEX = r'BEGIN_DEPENDENCIES'
181 END_DEP_REGEX = r'END_DEPENDENCIES'
183 BEGIN_CASE_REGEX = r'/\*\s*BEGIN_CASE\s*(?P<depends_on>.*?)\s*\*/'
184 END_CASE_REGEX = r'/\*\s*END_CASE\s*\*/'
186 DEPENDENCY_REGEX = r'depends_on:(?P<dependencies>.*)'
187 C_IDENTIFIER_REGEX = r'!?[a-z_][a-z0-9_]*'
188 CONDITION_OPERATOR_REGEX = r'[!=]=|[<>]=?'
189 # forbid 0ddd which might be accidentally octal or accidentally decimal
190 CONDITION_VALUE_REGEX = r'[-+]?(0x[0-9a-f]+|0|[1-9][0-9]*)'
191 CONDITION_REGEX = r'({})(?:\s*({})\s*({}))?$'.format(C_IDENTIFIER_REGEX,
192 CONDITION_OPERATOR_REGEX,
193 CONDITION_VALUE_REGEX)
194 TEST_FUNCTION_VALIDATION_REGEX = r'\s*void\s+(?P<func_name>\w+)\s*\('
195 INT_CHECK_REGEX = r'int\s+.*'
196 CHAR_CHECK_REGEX = r'char\s*\*\s*.*'
197 DATA_T_CHECK_REGEX = r'data_t\s*\*\s*.*'
198 FUNCTION_ARG_LIST_END_REGEX = r'.*\)'
199 EXIT_LABEL_REGEX = r'^exit:'
202 class GeneratorInputError(Exception):
204 Exception to indicate error in the input files to this script.
205 This includes missing patterns, test function names and other
211 class FileWrapper(io.FileIO, object):
213 This class extends built-in io.FileIO class with attribute line_no,
214 that indicates line number for the line that is read.
217 def __init__(self, file_name):
219 Instantiate the base class and initialize the line number to 0.
221 :param file_name: File path to open.
223 super(FileWrapper, self).__init__(file_name, 'r')
228 Python 2 iterator method. This method overrides base class's
229 next method and extends the next method to count the line
230 numbers as each line is read.
232 It works for both Python 2 and Python 3 by checking iterator
233 method name in the base iterator object.
235 :return: Line read from file.
237 parent = super(FileWrapper, self)
238 if hasattr(parent, '__next__'):
239 line = parent.__next__() # Python 3
241 line = next(parent) # Python 2 # pylint: disable=no-member
244 # Convert byte array to string with correct encoding and
245 # strip any whitespaces added in the decoding process.
246 return line.decode(sys.getdefaultencoding()).rstrip() + '\n'
249 # Python 3 iterator method
252 def get_line_no(self):
254 Gives current line number.
258 line_no = property(get_line_no)
263 Split NOT character '!' from dependency. Used by gen_dependencies()
265 :param dep: Dependency list
266 :return: string tuple. Ex: ('!', MACRO) for !MACRO and ('', MACRO) for
269 return ('!', dep[1:]) if dep[0] == '!' else ('', dep)
272 def gen_dependencies(dependencies):
274 Test suite data and functions specifies compile time dependencies.
275 This function generates C preprocessor code from the input
276 dependency list. Caller uses the generated preprocessor code to
278 A dependency in the input list can have a leading '!' character
279 to negate a condition. '!' is separated from the dependency using
280 function split_dep() and proper preprocessor check is generated
283 :param dependencies: List of dependencies.
284 :return: if defined and endif code with macro annotations for
287 dep_start = ''.join(['#if %sdefined(%s)\n' % (x, y) for x, y in
288 map(split_dep, dependencies)])
289 dep_end = ''.join(['#endif /* %s */\n' %
290 x for x in reversed(dependencies)])
292 return dep_start, dep_end
295 def gen_dependencies_one_line(dependencies):
297 Similar to gen_dependencies() but generates dependency checks in one line.
298 Useful for generating code with #else block.
300 :param dependencies: List of dependencies.
301 :return: Preprocessor check code
303 defines = '#if ' if dependencies else ''
304 defines += ' && '.join(['%sdefined(%s)' % (x, y) for x, y in map(
305 split_dep, dependencies)])
309 def gen_function_wrapper(name, local_vars, args_dispatch):
311 Creates test function wrapper code. A wrapper has the code to
312 unpack parameters from parameters[] array.
314 :param name: Test function name
315 :param local_vars: Local variables declaration code
316 :param args_dispatch: List of dispatch arguments.
317 Ex: ['(char *)params[0]', '*((int *)params[1])']
318 :return: Test function wrapper.
320 # Then create the wrapper
322 void {name}_wrapper( void ** params )
324 {unused_params}{locals}
327 '''.format(name=name,
328 unused_params='' if args_dispatch else ' (void)params;\n',
329 args=', '.join(args_dispatch),
334 def gen_dispatch(name, dependencies):
336 Test suite code template main_test.function defines a C function
337 array to contain test case functions. This function generates an
338 initializer entry for a function in that array. The entry is
339 composed of a compile time check for the test function
340 dependencies. At compile time the test function is assigned when
341 dependencies are met, else NULL is assigned.
343 :param name: Test function name
344 :param dependencies: List of dependencies
345 :return: Dispatch code.
348 preprocessor_check = gen_dependencies_one_line(dependencies)
355 '''.format(preprocessor_check=preprocessor_check, name=name)
359 '''.format(name=name)
364 def parse_until_pattern(funcs_f, end_regex):
366 Matches pattern end_regex to the lines read from the file object.
367 Returns the lines read until end pattern is matched.
369 :param funcs_f: file object for .function file
370 :param end_regex: Pattern to stop parsing
371 :return: Lines read before the end pattern
373 headers = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
375 if re.search(end_regex, line):
379 raise GeneratorInputError("file: %s - end pattern [%s] not found!" %
380 (funcs_f.name, end_regex))
385 def validate_dependency(dependency):
387 Validates a C macro and raises GeneratorInputError on invalid input.
388 :param dependency: Input macro dependency
389 :return: input dependency stripped of leading & trailing white spaces.
391 dependency = dependency.strip()
392 if not re.match(CONDITION_REGEX, dependency, re.I):
393 raise GeneratorInputError('Invalid dependency %s' % dependency)
397 def parse_dependencies(inp_str):
399 Parses dependencies out of inp_str, validates them and returns a
402 :param inp_str: Input string with macros delimited by ':'.
403 :return: list of dependencies
405 dependencies = [dep for dep in map(validate_dependency,
410 def parse_suite_dependencies(funcs_f):
412 Parses test suite dependencies specified at the top of a
413 .function file, that starts with pattern BEGIN_DEPENDENCIES
414 and end with END_DEPENDENCIES. Dependencies are specified
415 after pattern 'depends_on:' and are delimited by ':'.
417 :param funcs_f: file object for .function file
418 :return: List of test suite dependencies.
422 match = re.search(DEPENDENCY_REGEX, line.strip())
425 dependencies = parse_dependencies(match.group('dependencies'))
426 except GeneratorInputError as error:
427 raise GeneratorInputError(
428 str(error) + " - %s:%d" % (funcs_f.name, funcs_f.line_no))
429 if re.search(END_DEP_REGEX, line):
432 raise GeneratorInputError("file: %s - end dependency pattern [%s]"
433 " not found!" % (funcs_f.name,
439 def parse_function_dependencies(line):
441 Parses function dependencies, that are in the same line as
442 comment BEGIN_CASE. Dependencies are specified after pattern
443 'depends_on:' and are delimited by ':'.
445 :param line: Line from .function file that has dependencies.
446 :return: List of dependencies.
449 match = re.search(BEGIN_CASE_REGEX, line)
450 dep_str = match.group('depends_on')
452 match = re.search(DEPENDENCY_REGEX, dep_str)
454 dependencies += parse_dependencies(match.group('dependencies'))
459 def parse_function_arguments(line):
461 Parses test function signature for validation and generates
462 a dispatch wrapper function that translates input test vectors
463 read from the data file into test function arguments.
465 :param line: Line from .function file that has a function
467 :return: argument list, local variables for
468 wrapper function and argument dispatch code.
474 # Remove characters before arguments
475 line = line[line.find('(') + 1:]
476 # Process arguments, ex: <type> arg1, <type> arg2 )
477 # This script assumes that the argument list is terminated by ')'
478 # i.e. the test functions will not have a function pointer
480 for arg in line[:line.find(')')].split(','):
484 if re.search(INT_CHECK_REGEX, arg.strip()):
486 args_dispatch.append('*( (int *) params[%d] )' % arg_idx)
487 elif re.search(CHAR_CHECK_REGEX, arg.strip()):
489 args_dispatch.append('(char *) params[%d]' % arg_idx)
490 elif re.search(DATA_T_CHECK_REGEX, arg.strip()):
493 pointer_initializer = '(uint8_t *) params[%d]' % arg_idx
494 len_initializer = '*( (uint32_t *) params[%d] )' % (arg_idx+1)
495 local_vars += """ data_t data%d = {%s, %s};
496 """ % (arg_idx, pointer_initializer, len_initializer)
498 args_dispatch.append('&data%d' % arg_idx)
501 raise ValueError("Test function arguments can only be 'int', "
502 "'char *' or 'data_t'\n%s" % line)
505 return args, local_vars, args_dispatch
508 def generate_function_code(name, code, local_vars, args_dispatch,
511 Generate function code with preprocessor checks and parameter dispatch
514 :param name: Function name
515 :param code: Function code
516 :param local_vars: Local variables for function wrapper
517 :param args_dispatch: Argument dispatch code
518 :param dependencies: Preprocessor dependencies list
519 :return: Final function code
521 # Add exit label if not present
522 if code.find('exit:') == -1:
523 split_code = code.rsplit('}', 1)
524 if len(split_code) == 2:
527 }""".join(split_code)
529 code += gen_function_wrapper(name, local_vars, args_dispatch)
530 preprocessor_check_start, preprocessor_check_end = \
531 gen_dependencies(dependencies)
532 return preprocessor_check_start + code + preprocessor_check_end
535 def parse_function_code(funcs_f, dependencies, suite_dependencies):
537 Parses out a function from function file object and generates
538 function and dispatch code.
540 :param funcs_f: file object of the functions file.
541 :param dependencies: List of dependencies
542 :param suite_dependencies: List of test suite dependencies
543 :return: Function name, arguments, function code and dispatch code.
545 line_directive = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
547 has_exit_label = False
549 # Check function signature. Function signature may be split
550 # across multiple lines. Here we try to find the start of
551 # arguments list, then remove '\n's and apply the regex to
552 # detect function start.
553 up_to_arg_list_start = code + line[:line.find('(') + 1]
554 match = re.match(TEST_FUNCTION_VALIDATION_REGEX,
555 up_to_arg_list_start.replace('\n', ' '), re.I)
557 # check if we have full signature i.e. split in more lines
558 name = match.group('func_name')
559 if not re.match(FUNCTION_ARG_LIST_END_REGEX, line):
562 if re.search(FUNCTION_ARG_LIST_END_REGEX, line):
564 args, local_vars, args_dispatch = parse_function_arguments(
570 raise GeneratorInputError("file: %s - Test functions not found!" %
573 # Prefix test function name with 'test_'
574 code = code.replace(name, 'test_' + name, 1)
575 name = 'test_' + name
578 if re.search(END_CASE_REGEX, line):
580 if not has_exit_label:
582 re.search(EXIT_LABEL_REGEX, line.strip()) is not None
585 raise GeneratorInputError("file: %s - end case pattern [%s] not "
586 "found!" % (funcs_f.name, END_CASE_REGEX))
588 code = line_directive + code
589 code = generate_function_code(name, code, local_vars, args_dispatch,
591 dispatch_code = gen_dispatch(name, suite_dependencies + dependencies)
592 return (name, args, code, dispatch_code)
595 def parse_functions(funcs_f):
597 Parses a test_suite_xxx.function file and returns information
598 for generating a C source file for the test suite.
600 :param funcs_f: file object of the functions file.
601 :return: List of test suite dependencies, test function dispatch
602 code, function code and a dict with function identifiers
606 suite_dependencies = []
612 if re.search(BEGIN_HEADER_REGEX, line):
613 suite_helpers += parse_until_pattern(funcs_f, END_HEADER_REGEX)
614 elif re.search(BEGIN_SUITE_HELPERS_REGEX, line):
615 suite_helpers += parse_until_pattern(funcs_f,
616 END_SUITE_HELPERS_REGEX)
617 elif re.search(BEGIN_DEP_REGEX, line):
618 suite_dependencies += parse_suite_dependencies(funcs_f)
619 elif re.search(BEGIN_CASE_REGEX, line):
621 dependencies = parse_function_dependencies(line)
622 except GeneratorInputError as error:
623 raise GeneratorInputError(
624 "%s:%d: %s" % (funcs_f.name, funcs_f.line_no,
626 func_name, args, func_code, func_dispatch =\
627 parse_function_code(funcs_f, dependencies, suite_dependencies)
628 suite_functions += func_code
629 # Generate dispatch code and enumeration info
630 if func_name in func_info:
631 raise GeneratorInputError(
632 "file: %s - function %s re-declared at line %d" %
633 (funcs_f.name, func_name, funcs_f.line_no))
634 func_info[func_name] = (function_idx, args)
635 dispatch_code += '/* Function Id: %d */\n' % function_idx
636 dispatch_code += func_dispatch
639 func_code = (suite_helpers +
640 suite_functions).join(gen_dependencies(suite_dependencies))
641 return suite_dependencies, dispatch_code, func_code, func_info
644 def escaped_split(inp_str, split_char):
646 Split inp_str on character split_char but ignore if escaped.
647 Since, return value is used to write back to the intermediate
648 data file, any escape characters in the input are retained in the
651 :param inp_str: String to split
652 :param split_char: Split character
653 :return: List of splits
655 if len(split_char) > 1:
656 raise ValueError('Expected split character. Found string!')
657 out = re.sub(r'(\\.)|' + split_char,
658 lambda m: m.group(1) or '\n', inp_str,
659 len(inp_str)).split('\n')
660 out = [x for x in out if x]
664 def parse_test_data(data_f):
666 Parses .data file for each test case name, test function name,
667 test dependencies and test arguments. This information is
668 correlated with the test functions file for generating an
669 intermediate data file replacing the strings for test function
670 names, dependencies and integer constant expressions with
671 identifiers. Mainly for optimising space for on-target
674 :param data_f: file object of the data file.
675 :return: Generator that yields test name, function name,
676 dependency list and function argument list.
678 __state_read_name = 0
679 __state_read_args = 1
680 state = __state_read_name
686 if line.startswith('#'):
689 # Blank line indicates end of test
691 if state == __state_read_args:
692 raise GeneratorInputError("[%s:%d] Newline before arguments. "
693 "Test function and arguments "
695 (data_f.name, data_f.line_no, name))
698 if state == __state_read_name:
701 state = __state_read_args
702 elif state == __state_read_args:
704 match = re.search(DEPENDENCY_REGEX, line)
707 dependencies = parse_dependencies(
708 match.group('dependencies'))
709 except GeneratorInputError as error:
710 raise GeneratorInputError(
711 str(error) + " - %s:%d" %
712 (data_f.name, data_f.line_no))
715 parts = escaped_split(line, ':')
716 test_function = parts[0]
718 yield name, test_function, dependencies, args
720 state = __state_read_name
721 if state == __state_read_args:
722 raise GeneratorInputError("[%s:%d] Newline before arguments. "
723 "Test function and arguments missing for "
724 "%s" % (data_f.name, data_f.line_no, name))
727 def gen_dep_check(dep_id, dep):
729 Generate code for checking dependency with the associated
732 :param dep_id: Dependency identifier
733 :param dep: Dependency macro
734 :return: Dependency check code
737 raise GeneratorInputError("Dependency Id should be a positive "
739 _not, dep = ('!', dep[1:]) if dep[0] == '!' else ('', dep)
741 raise GeneratorInputError("Dependency should not be an empty string.")
743 dependency = re.match(CONDITION_REGEX, dep, re.I)
745 raise GeneratorInputError('Invalid dependency %s' % dep)
747 _defined = '' if dependency.group(2) else 'defined'
748 _cond = dependency.group(2) if dependency.group(2) else ''
749 _value = dependency.group(3) if dependency.group(3) else ''
754 #if {_not}{_defined}({macro}{_cond}{_value})
755 ret = DEPENDENCY_SUPPORTED;
757 ret = DEPENDENCY_NOT_SUPPORTED;
760 break;'''.format(_not=_not, _defined=_defined,
761 macro=dependency.group(1), id=dep_id,
762 _cond=_cond, _value=_value)
766 def gen_expression_check(exp_id, exp):
768 Generates code for evaluating an integer expression using
769 associated expression Id.
771 :param exp_id: Expression Identifier
772 :param exp: Expression/Macro
773 :return: Expression check code
776 raise GeneratorInputError("Expression Id should be a positive "
779 raise GeneratorInputError("Expression should not be an empty string.")
783 *out_value = {expression};
785 break;'''.format(exp_id=exp_id, expression=exp)
789 def write_dependencies(out_data_f, test_dependencies, unique_dependencies):
791 Write dependencies to intermediate test data file, replacing
792 the string form with identifiers. Also, generates dependency
795 :param out_data_f: Output intermediate data file
796 :param test_dependencies: Dependencies
797 :param unique_dependencies: Mutable list to track unique dependencies
798 that are global to this re-entrant function.
799 :return: returns dependency check code.
802 if test_dependencies:
803 out_data_f.write('depends_on')
804 for dep in test_dependencies:
805 if dep not in unique_dependencies:
806 unique_dependencies.append(dep)
807 dep_id = unique_dependencies.index(dep)
808 dep_check_code += gen_dep_check(dep_id, dep)
810 dep_id = unique_dependencies.index(dep)
811 out_data_f.write(':' + str(dep_id))
812 out_data_f.write('\n')
813 return dep_check_code
816 def write_parameters(out_data_f, test_args, func_args, unique_expressions):
818 Writes test parameters to the intermediate data file, replacing
819 the string form with identifiers. Also, generates expression
822 :param out_data_f: Output intermediate data file
823 :param test_args: Test parameters
824 :param func_args: Function arguments
825 :param unique_expressions: Mutable list to track unique
826 expressions that are global to this re-entrant function.
827 :return: Returns expression check code.
830 for i, _ in enumerate(test_args):
834 # check if val is a non literal int val (i.e. an expression)
835 if typ == 'int' and not re.match(r'(\d+|0x[0-9a-f]+)$',
838 if val not in unique_expressions:
839 unique_expressions.append(val)
840 # exp_id can be derived from len(). But for
841 # readability and consistency with case of existing
843 exp_id = unique_expressions.index(val)
844 expression_code += gen_expression_check(exp_id, val)
847 val = unique_expressions.index(val)
848 out_data_f.write(':' + typ + ':' + str(val))
849 out_data_f.write('\n')
850 return expression_code
853 def gen_suite_dep_checks(suite_dependencies, dep_check_code, expression_code):
855 Generates preprocessor checks for test suite dependencies.
857 :param suite_dependencies: Test suite dependencies read from the
859 :param dep_check_code: Dependency check code
860 :param expression_code: Expression check code
861 :return: Dependency and expression code guarded by test suite
864 if suite_dependencies:
865 preprocessor_check = gen_dependencies_one_line(suite_dependencies)
870 '''.format(preprocessor_check=preprocessor_check, code=dep_check_code)
871 expression_code = '''
875 '''.format(preprocessor_check=preprocessor_check, code=expression_code)
876 return dep_check_code, expression_code
879 def gen_from_test_data(data_f, out_data_f, func_info, suite_dependencies):
881 This function reads test case name, dependencies and test vectors
882 from the .data file. This information is correlated with the test
883 functions file for generating an intermediate data file replacing
884 the strings for test function names, dependencies and integer
885 constant expressions with identifiers. Mainly for optimising
886 space for on-target execution.
887 It also generates test case dependency check code and expression
890 :param data_f: Data file object
891 :param out_data_f: Output intermediate data file
892 :param func_info: Dict keyed by function and with function id
894 :param suite_dependencies: Test suite dependencies
895 :return: Returns dependency and expression check code
897 unique_dependencies = []
898 unique_expressions = []
901 for test_name, function_name, test_dependencies, test_args in \
902 parse_test_data(data_f):
903 out_data_f.write(test_name + '\n')
906 dep_check_code += write_dependencies(out_data_f, test_dependencies,
909 # Write test function name
910 test_function_name = 'test_' + function_name
911 if test_function_name not in func_info:
912 raise GeneratorInputError("Function %s not found!" %
914 func_id, func_args = func_info[test_function_name]
915 out_data_f.write(str(func_id))
918 if len(test_args) != len(func_args):
919 raise GeneratorInputError("Invalid number of arguments in test "
920 "%s. See function %s signature." %
921 (test_name, function_name))
922 expression_code += write_parameters(out_data_f, test_args, func_args,
925 # Write a newline as test case separator
926 out_data_f.write('\n')
928 dep_check_code, expression_code = gen_suite_dep_checks(
929 suite_dependencies, dep_check_code, expression_code)
930 return dep_check_code, expression_code
933 def add_input_info(funcs_file, data_file, template_file,
936 Add generator input info in snippets.
938 :param funcs_file: Functions file object
939 :param data_file: Data file object
940 :param template_file: Template file object
941 :param c_file: Output C file object
942 :param snippets: Dictionary to contain code pieces to be
943 substituted in the template.
946 snippets['test_file'] = c_file
947 snippets['test_main_file'] = template_file
948 snippets['test_case_file'] = funcs_file
949 snippets['test_case_data_file'] = data_file
952 def read_code_from_input_files(platform_file, helpers_file,
953 out_data_file, snippets):
955 Read code from input files and create substitutions for replacement
956 strings in the template file.
958 :param platform_file: Platform file object
959 :param helpers_file: Helper functions file object
960 :param out_data_file: Output intermediate data file object
961 :param snippets: Dictionary to contain code pieces to be
962 substituted in the template.
966 with open(helpers_file, 'r') as help_f, open(platform_file, 'r') as \
968 snippets['test_common_helper_file'] = helpers_file
969 snippets['test_common_helpers'] = help_f.read()
970 snippets['test_platform_file'] = platform_file
971 snippets['platform_code'] = platform_f.read().replace(
972 'DATA_FILE', out_data_file.replace('\\', '\\\\')) # escape '\'
975 def write_test_source_file(template_file, c_file, snippets):
977 Write output source file with generated source code.
979 :param template_file: Template file name
980 :param c_file: Output source file
981 :param snippets: Generated and code snippets
984 with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
985 for line_no, line in enumerate(template_f.readlines(), 1):
986 # Update line number. +1 as #line directive sets next line number
987 snippets['line_no'] = line_no + 1
988 code = string.Template(line).substitute(**snippets)
992 def parse_function_file(funcs_file, snippets):
994 Parse function file and generate function dispatch code.
996 :param funcs_file: Functions file name
997 :param snippets: Dictionary to contain code pieces to be
998 substituted in the template.
1001 with FileWrapper(funcs_file) as funcs_f:
1002 suite_dependencies, dispatch_code, func_code, func_info = \
1003 parse_functions(funcs_f)
1004 snippets['functions_code'] = func_code
1005 snippets['dispatch_code'] = dispatch_code
1006 return suite_dependencies, func_info
1009 def generate_intermediate_data_file(data_file, out_data_file,
1010 suite_dependencies, func_info, snippets):
1012 Generates intermediate data file from input data file and
1013 information read from functions file.
1015 :param data_file: Data file name
1016 :param out_data_file: Output/Intermediate data file
1017 :param suite_dependencies: List of suite dependencies.
1018 :param func_info: Function info parsed from functions file.
1019 :param snippets: Dictionary to contain code pieces to be
1020 substituted in the template.
1023 with FileWrapper(data_file) as data_f, \
1024 open(out_data_file, 'w') as out_data_f:
1025 dep_check_code, expression_code = gen_from_test_data(
1026 data_f, out_data_f, func_info, suite_dependencies)
1027 snippets['dep_check_code'] = dep_check_code
1028 snippets['expression_code'] = expression_code
1031 def generate_code(**input_info):
1033 Generates C source code from test suite file, data file, common
1034 helpers file and platform file.
1036 input_info expands to following parameters:
1037 funcs_file: Functions file object
1038 data_file: Data file object
1039 template_file: Template file object
1040 platform_file: Platform file object
1041 helpers_file: Helper functions file object
1042 suites_dir: Test suites dir
1043 c_file: Output C file object
1044 out_data_file: Output intermediate data file object
1047 funcs_file = input_info['funcs_file']
1048 data_file = input_info['data_file']
1049 template_file = input_info['template_file']
1050 platform_file = input_info['platform_file']
1051 helpers_file = input_info['helpers_file']
1052 suites_dir = input_info['suites_dir']
1053 c_file = input_info['c_file']
1054 out_data_file = input_info['out_data_file']
1055 for name, path in [('Functions file', funcs_file),
1056 ('Data file', data_file),
1057 ('Template file', template_file),
1058 ('Platform file', platform_file),
1059 ('Helpers code file', helpers_file),
1060 ('Suites dir', suites_dir)]:
1061 if not os.path.exists(path):
1062 raise IOError("ERROR: %s [%s] not found!" % (name, path))
1064 snippets = {'generator_script': os.path.basename(__file__)}
1065 read_code_from_input_files(platform_file, helpers_file,
1066 out_data_file, snippets)
1067 add_input_info(funcs_file, data_file, template_file,
1069 suite_dependencies, func_info = parse_function_file(funcs_file, snippets)
1070 generate_intermediate_data_file(data_file, out_data_file,
1071 suite_dependencies, func_info, snippets)
1072 write_test_source_file(template_file, c_file, snippets)
1077 Command line parser.
1081 parser = argparse.ArgumentParser(
1082 description='Dynamically generate test suite code.')
1084 parser.add_argument("-f", "--functions-file",
1086 help="Functions file",
1087 metavar="FUNCTIONS_FILE",
1090 parser.add_argument("-d", "--data-file",
1093 metavar="DATA_FILE",
1096 parser.add_argument("-t", "--template-file",
1097 dest="template_file",
1098 help="Template file",
1099 metavar="TEMPLATE_FILE",
1102 parser.add_argument("-s", "--suites-dir",
1105 metavar="SUITES_DIR",
1108 parser.add_argument("--helpers-file",
1109 dest="helpers_file",
1110 help="Helpers file",
1111 metavar="HELPERS_FILE",
1114 parser.add_argument("-p", "--platform-file",
1115 dest="platform_file",
1116 help="Platform code file",
1117 metavar="PLATFORM_FILE",
1120 parser.add_argument("-o", "--out-dir",
1122 help="Dir where generated code and scripts are copied",
1126 args = parser.parse_args()
1128 data_file_name = os.path.basename(args.data_file)
1129 data_name = os.path.splitext(data_file_name)[0]
1131 out_c_file = os.path.join(args.out_dir, data_name + '.c')
1132 out_data_file = os.path.join(args.out_dir, data_name + '.datax')
1134 out_c_file_dir = os.path.dirname(out_c_file)
1135 out_data_file_dir = os.path.dirname(out_data_file)
1136 for directory in [out_c_file_dir, out_data_file_dir]:
1137 if not os.path.exists(directory):
1138 os.makedirs(directory)
1140 generate_code(funcs_file=args.funcs_file, data_file=args.data_file,
1141 template_file=args.template_file,
1142 platform_file=args.platform_file,
1143 helpers_file=args.helpers_file, suites_dir=args.suites_dir,
1144 c_file=out_c_file, out_data_file=out_data_file)
1147 if __name__ == "__main__":
1150 except GeneratorInputError as err:
1151 sys.exit("%s: input error: %s" %
1152 (os.path.basename(sys.argv[0]), str(err)))