• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Test suites code generator.
3#
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
6
7"""
8This script is a key part of Mbed TLS test suites framework. For
9understanding the script it is important to understand the
10framework. This doc string contains a summary of the framework
11and explains the function of this script.
12
13Mbed TLS test suites:
14=====================
15Scope:
16------
17The test suites focus on unit testing the crypto primitives and also
18include x509 parser tests. Tests can be added to test any Mbed TLS
19module. However, the framework is not capable of testing SSL
20protocol, since that requires full stack execution and that is best
21tested as part of the system test.
22
23Test case definition:
24---------------------
25Tests are defined in a test_suite_<module>[.<optional sub module>].data
26file. A test definition contains:
27 test name
28 optional build macro dependencies
29 test function
30 test parameters
31
32Test dependencies are build macros that can be specified to indicate
33the build config in which the test is valid. For example if a test
34depends on a feature that is only enabled by defining a macro. Then
35that macro should be specified as a dependency of the test.
36
37Test function is the function that implements the test steps. This
38function is specified for different tests that perform same steps
39with different parameters.
40
41Test parameters are specified in string form separated by ':'.
42Parameters can be of type string, binary data specified as hex
43string and integer constants specified as integer, macro or
44as an expression. Following is an example test definition:
45
46 AES 128 GCM Encrypt and decrypt 8 bytes
47 depends_on:MBEDTLS_AES_C:MBEDTLS_GCM_C
48 enc_dec_buf:MBEDTLS_CIPHER_AES_128_GCM:"AES-128-GCM":128:8:-1
49
50Test functions:
51---------------
52Test functions are coded in C in test_suite_<module>.function files.
53Functions file is itself not compilable and contains special
54format patterns to specify test suite dependencies, start and end
55of functions and function dependencies. Check any existing functions
56file for example.
57
58Execution:
59----------
60Tests are executed in 3 steps:
61- Generating test_suite_<module>[.<optional sub module>].c file
62  for each corresponding .data file.
63- Building each source file into executables.
64- Running each executable and printing report.
65
66Generating C test source requires more than just the test functions.
67Following extras are required:
68- Process main()
69- Reading .data file and dispatching test cases.
70- Platform specific test case execution
71- Dependency checking
72- Integer expression evaluation
73- Test function dispatch
74
75Build dependencies and integer expressions (in the test parameters)
76are specified as strings in the .data file. Their run time value is
77not known at the generation stage. Hence, they need to be translated
78into run time evaluations. This script generates the run time checks
79for dependencies and integer expressions.
80
81Similarly, function names have to be translated into function calls.
82This script also generates code for function dispatch.
83
84The extra code mentioned here is either generated by this script
85or it comes from the input files: helpers file, platform file and
86the template file.
87
88Helper file:
89------------
90Helpers file contains common helper/utility functions and data.
91
92Platform file:
93--------------
94Platform file contains platform specific setup code and test case
95dispatch code. For example, host_test.function reads test data
96file from host's file system and dispatches tests.
97
98Template file:
99---------
100Template file for example main_test.function is a template C file in
101which generated code and code from input files is substituted to
102generate a compilable C file. It also contains skeleton functions for
103dependency checks, expression evaluation and function dispatch. These
104functions are populated with checks and return codes by this script.
105
106Template file contains "replacement" fields that are formatted
107strings processed by Python string.Template.substitute() method.
108
109This script:
110============
111Core function of this script is to fill the template file with
112code that is generated or read from helpers and platform files.
113
114This script replaces following fields in the template and generates
115the test source file:
116
117__MBEDTLS_TEST_TEMPLATE__TEST_COMMON_HELPERS
118            All common code from helpers.function
119            is substituted here.
120__MBEDTLS_TEST_TEMPLATE__FUNCTIONS_CODE
121            Test functions are substituted here
122            from the input test_suit_xyz.function
123            file. C preprocessor checks are generated
124            for the build dependencies specified
125            in the input file. This script also
126            generates wrappers for the test
127            functions with code to expand the
128            string parameters read from the data
129            file.
130__MBEDTLS_TEST_TEMPLATE__EXPRESSION_CODE
131            This script enumerates the
132            expressions in the .data file and
133            generates code to handle enumerated
134            expression Ids and return the values.
135__MBEDTLS_TEST_TEMPLATE__DEP_CHECK_CODE
136            This script enumerates all
137            build dependencies and generate
138            code to handle enumerated build
139            dependency Id and return status: if
140            the dependency is defined or not.
141__MBEDTLS_TEST_TEMPLATE__DISPATCH_CODE
142            This script enumerates the functions
143            specified in the input test data file
144            and generates the initializer for the
145            function table in the template
146            file.
147__MBEDTLS_TEST_TEMPLATE__PLATFORM_CODE
148            Platform specific setup and test
149            dispatch code.
150
151"""
152
153
154import io
155import os
156import re
157import sys
158import string
159import argparse
160
161
162# Types recognized as signed integer arguments in test functions.
163SIGNED_INTEGER_TYPES = frozenset([
164    'char',
165    'short',
166    'short int',
167    'int',
168    'int8_t',
169    'int16_t',
170    'int32_t',
171    'int64_t',
172    'intmax_t',
173    'long',
174    'long int',
175    'long long int',
176    'mbedtls_mpi_sint',
177    'psa_status_t',
178])
179# Types recognized as string arguments in test functions.
180STRING_TYPES = frozenset(['char*', 'const char*', 'char const*'])
181# Types recognized as hex data arguments in test functions.
182DATA_TYPES = frozenset(['data_t*', 'const data_t*', 'data_t const*'])
183
184BEGIN_HEADER_REGEX = r'/\*\s*BEGIN_HEADER\s*\*/'
185END_HEADER_REGEX = r'/\*\s*END_HEADER\s*\*/'
186
187BEGIN_SUITE_HELPERS_REGEX = r'/\*\s*BEGIN_SUITE_HELPERS\s*\*/'
188END_SUITE_HELPERS_REGEX = r'/\*\s*END_SUITE_HELPERS\s*\*/'
189
190BEGIN_DEP_REGEX = r'BEGIN_DEPENDENCIES'
191END_DEP_REGEX = r'END_DEPENDENCIES'
192
193BEGIN_CASE_REGEX = r'/\*\s*BEGIN_CASE\s*(?P<depends_on>.*?)\s*\*/'
194END_CASE_REGEX = r'/\*\s*END_CASE\s*\*/'
195
196DEPENDENCY_REGEX = r'depends_on:(?P<dependencies>.*)'
197C_IDENTIFIER_REGEX = r'!?[a-z_][a-z0-9_]*'
198CONDITION_OPERATOR_REGEX = r'[!=]=|[<>]=?'
199# forbid 0ddd which might be accidentally octal or accidentally decimal
200CONDITION_VALUE_REGEX = r'[-+]?(0x[0-9a-f]+|0|[1-9][0-9]*)'
201CONDITION_REGEX = r'({})(?:\s*({})\s*({}))?$'.format(C_IDENTIFIER_REGEX,
202                                                     CONDITION_OPERATOR_REGEX,
203                                                     CONDITION_VALUE_REGEX)
204TEST_FUNCTION_VALIDATION_REGEX = r'\s*void\s+(?P<func_name>\w+)\s*\('
205FUNCTION_ARG_LIST_END_REGEX = r'.*\)'
206EXIT_LABEL_REGEX = r'^exit:'
207
208
209class GeneratorInputError(Exception):
210    """
211    Exception to indicate error in the input files to this script.
212    This includes missing patterns, test function names and other
213    parsing errors.
214    """
215    pass
216
217
218class FileWrapper(io.FileIO):
219    """
220    This class extends built-in io.FileIO class with attribute line_no,
221    that indicates line number for the line that is read.
222    """
223
224    def __init__(self, file_name):
225        """
226        Instantiate the base class and initialize the line number to 0.
227
228        :param file_name: File path to open.
229        """
230        super().__init__(file_name, 'r')
231        self._line_no = 0
232
233    def __next__(self):
234        """
235        This method overrides base class's __next__ method and extends it
236        method to count the line numbers as each line is read.
237
238        :return: Line read from file.
239        """
240        line = super().__next__()
241        if line is not None:
242            self._line_no += 1
243            # Convert byte array to string with correct encoding and
244            # strip any whitespaces added in the decoding process.
245            return line.decode(sys.getdefaultencoding()).rstrip() + '\n'
246        return None
247
248    def get_line_no(self):
249        """
250        Gives current line number.
251        """
252        return self._line_no
253
254    line_no = property(get_line_no)
255
256
257def split_dep(dep):
258    """
259    Split NOT character '!' from dependency. Used by gen_dependencies()
260
261    :param dep: Dependency list
262    :return: string tuple. Ex: ('!', MACRO) for !MACRO and ('', MACRO) for
263             MACRO.
264    """
265    return ('!', dep[1:]) if dep[0] == '!' else ('', dep)
266
267
268def gen_dependencies(dependencies):
269    """
270    Test suite data and functions specifies compile time dependencies.
271    This function generates C preprocessor code from the input
272    dependency list. Caller uses the generated preprocessor code to
273    wrap dependent code.
274    A dependency in the input list can have a leading '!' character
275    to negate a condition. '!' is separated from the dependency using
276    function split_dep() and proper preprocessor check is generated
277    accordingly.
278
279    :param dependencies: List of dependencies.
280    :return: if defined and endif code with macro annotations for
281             readability.
282    """
283    dep_start = ''.join(['#if %sdefined(%s)\n' % (x, y) for x, y in
284                         map(split_dep, dependencies)])
285    dep_end = ''.join(['#endif /* %s */\n' %
286                       x for x in reversed(dependencies)])
287
288    return dep_start, dep_end
289
290
291def gen_dependencies_one_line(dependencies):
292    """
293    Similar to gen_dependencies() but generates dependency checks in one line.
294    Useful for generating code with #else block.
295
296    :param dependencies: List of dependencies.
297    :return: Preprocessor check code
298    """
299    defines = '#if ' if dependencies else ''
300    defines += ' && '.join(['%sdefined(%s)' % (x, y) for x, y in map(
301        split_dep, dependencies)])
302    return defines
303
304
305def gen_function_wrapper(name, local_vars, args_dispatch):
306    """
307    Creates test function wrapper code. A wrapper has the code to
308    unpack parameters from parameters[] array.
309
310    :param name: Test function name
311    :param local_vars: Local variables declaration code
312    :param args_dispatch: List of dispatch arguments.
313           Ex: ['(char *) params[0]', '*((int *) params[1])']
314    :return: Test function wrapper.
315    """
316    # Then create the wrapper
317    wrapper = '''
318void {name}_wrapper( void ** params )
319{{
320{unused_params}{locals}
321    {name}( {args} );
322}}
323'''.format(name=name,
324           unused_params='' if args_dispatch else '    (void)params;\n',
325           args=', '.join(args_dispatch),
326           locals=local_vars)
327    return wrapper
328
329
330def gen_dispatch(name, dependencies):
331    """
332    Test suite code template main_test.function defines a C function
333    array to contain test case functions. This function generates an
334    initializer entry for a function in that array. The entry is
335    composed of a compile time check for the test function
336    dependencies. At compile time the test function is assigned when
337    dependencies are met, else NULL is assigned.
338
339    :param name: Test function name
340    :param dependencies: List of dependencies
341    :return: Dispatch code.
342    """
343    if dependencies:
344        preprocessor_check = gen_dependencies_one_line(dependencies)
345        dispatch_code = '''
346{preprocessor_check}
347    {name}_wrapper,
348#else
349    NULL,
350#endif
351'''.format(preprocessor_check=preprocessor_check, name=name)
352    else:
353        dispatch_code = '''
354    {name}_wrapper,
355'''.format(name=name)
356
357    return dispatch_code
358
359
360def parse_until_pattern(funcs_f, end_regex):
361    """
362    Matches pattern end_regex to the lines read from the file object.
363    Returns the lines read until end pattern is matched.
364
365    :param funcs_f: file object for .function file
366    :param end_regex: Pattern to stop parsing
367    :return: Lines read before the end pattern
368    """
369    headers = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
370    for line in funcs_f:
371        if re.search(end_regex, line):
372            break
373        headers += line
374    else:
375        raise GeneratorInputError("file: %s - end pattern [%s] not found!" %
376                                  (funcs_f.name, end_regex))
377
378    return headers
379
380
381def validate_dependency(dependency):
382    """
383    Validates a C macro and raises GeneratorInputError on invalid input.
384    :param dependency: Input macro dependency
385    :return: input dependency stripped of leading & trailing white spaces.
386    """
387    dependency = dependency.strip()
388    if not re.match(CONDITION_REGEX, dependency, re.I):
389        raise GeneratorInputError('Invalid dependency %s' % dependency)
390    return dependency
391
392
393def parse_dependencies(inp_str):
394    """
395    Parses dependencies out of inp_str, validates them and returns a
396    list of macros.
397
398    :param inp_str: Input string with macros delimited by ':'.
399    :return: list of dependencies
400    """
401    dependencies = list(map(validate_dependency, inp_str.split(':')))
402    return dependencies
403
404
405def parse_suite_dependencies(funcs_f):
406    """
407    Parses test suite dependencies specified at the top of a
408    .function file, that starts with pattern BEGIN_DEPENDENCIES
409    and end with END_DEPENDENCIES. Dependencies are specified
410    after pattern 'depends_on:' and are delimited by ':'.
411
412    :param funcs_f: file object for .function file
413    :return: List of test suite dependencies.
414    """
415    dependencies = []
416    for line in funcs_f:
417        match = re.search(DEPENDENCY_REGEX, line.strip())
418        if match:
419            try:
420                dependencies = parse_dependencies(match.group('dependencies'))
421            except GeneratorInputError as error:
422                raise GeneratorInputError(
423                    str(error) + " - %s:%d" % (funcs_f.name, funcs_f.line_no))
424        if re.search(END_DEP_REGEX, line):
425            break
426    else:
427        raise GeneratorInputError("file: %s - end dependency pattern [%s]"
428                                  " not found!" % (funcs_f.name,
429                                                   END_DEP_REGEX))
430
431    return dependencies
432
433
434def parse_function_dependencies(line):
435    """
436    Parses function dependencies, that are in the same line as
437    comment BEGIN_CASE. Dependencies are specified after pattern
438    'depends_on:' and are delimited by ':'.
439
440    :param line: Line from .function file that has dependencies.
441    :return: List of dependencies.
442    """
443    dependencies = []
444    match = re.search(BEGIN_CASE_REGEX, line)
445    dep_str = match.group('depends_on')
446    if dep_str:
447        match = re.search(DEPENDENCY_REGEX, dep_str)
448        if match:
449            dependencies += parse_dependencies(match.group('dependencies'))
450
451    return dependencies
452
453
454ARGUMENT_DECLARATION_REGEX = re.compile(r'(.+?) ?(?:\bconst\b)? ?(\w+)\Z', re.S)
455def parse_function_argument(arg, arg_idx, args, local_vars, args_dispatch):
456    """
457    Parses one test function's argument declaration.
458
459    :param arg: argument declaration.
460    :param arg_idx: current wrapper argument index.
461    :param args: accumulator of arguments' internal types.
462    :param local_vars: accumulator of internal variable declarations.
463    :param args_dispatch: accumulator of argument usage expressions.
464    :return: the number of new wrapper arguments,
465             or None if the argument declaration is invalid.
466    """
467    # Normalize whitespace
468    arg = arg.strip()
469    arg = re.sub(r'\s*\*\s*', r'*', arg)
470    arg = re.sub(r'\s+', r' ', arg)
471    # Extract name and type
472    m = ARGUMENT_DECLARATION_REGEX.search(arg)
473    if not m:
474        # E.g. "int x[42]"
475        return None
476    typ, _ = m.groups()
477    if typ in SIGNED_INTEGER_TYPES:
478        args.append('int')
479        args_dispatch.append('((mbedtls_test_argument_t *) params[%d])->sint' % arg_idx)
480        return 1
481    if typ in STRING_TYPES:
482        args.append('char*')
483        args_dispatch.append('(char *) params[%d]' % arg_idx)
484        return 1
485    if typ in DATA_TYPES:
486        args.append('hex')
487        # create a structure
488        pointer_initializer = '(uint8_t *) params[%d]' % arg_idx
489        len_initializer = '((mbedtls_test_argument_t *) params[%d])->len' % (arg_idx+1)
490        local_vars.append('    data_t data%d = {%s, %s};\n' %
491                          (arg_idx, pointer_initializer, len_initializer))
492        args_dispatch.append('&data%d' % arg_idx)
493        return 2
494    return None
495
496ARGUMENT_LIST_REGEX = re.compile(r'\((.*?)\)', re.S)
497def parse_function_arguments(line):
498    """
499    Parses test function signature for validation and generates
500    a dispatch wrapper function that translates input test vectors
501    read from the data file into test function arguments.
502
503    :param line: Line from .function file that has a function
504                 signature.
505    :return: argument list, local variables for
506             wrapper function and argument dispatch code.
507    """
508    # Process arguments, ex: <type> arg1, <type> arg2 )
509    # This script assumes that the argument list is terminated by ')'
510    # i.e. the test functions will not have a function pointer
511    # argument.
512    m = ARGUMENT_LIST_REGEX.search(line)
513    arg_list = m.group(1).strip()
514    if arg_list in ['', 'void']:
515        return [], '', []
516    args = []
517    local_vars = []
518    args_dispatch = []
519    arg_idx = 0
520    for arg in arg_list.split(','):
521        indexes = parse_function_argument(arg, arg_idx,
522                                          args, local_vars, args_dispatch)
523        if indexes is None:
524            raise ValueError("Test function arguments can only be 'int', "
525                             "'char *' or 'data_t'\n%s" % line)
526        arg_idx += indexes
527
528    return args, ''.join(local_vars), args_dispatch
529
530
531def generate_function_code(name, code, local_vars, args_dispatch,
532                           dependencies):
533    """
534    Generate function code with preprocessor checks and parameter dispatch
535    wrapper.
536
537    :param name: Function name
538    :param code: Function code
539    :param local_vars: Local variables for function wrapper
540    :param args_dispatch: Argument dispatch code
541    :param dependencies: Preprocessor dependencies list
542    :return: Final function code
543    """
544    # Add exit label if not present
545    if code.find('exit:') == -1:
546        split_code = code.rsplit('}', 1)
547        if len(split_code) == 2:
548            code = """exit:
549    ;
550}""".join(split_code)
551
552    code += gen_function_wrapper(name, local_vars, args_dispatch)
553    preprocessor_check_start, preprocessor_check_end = \
554        gen_dependencies(dependencies)
555    return preprocessor_check_start + code + preprocessor_check_end
556
557COMMENT_START_REGEX = re.compile(r'/[*/]')
558
559def skip_comments(line, stream):
560    """Remove comments in line.
561
562    If the line contains an unfinished comment, read more lines from stream
563    until the line that contains the comment.
564
565    :return: The original line with inner comments replaced by spaces.
566             Trailing comments and whitespace may be removed completely.
567    """
568    pos = 0
569    while True:
570        opening = COMMENT_START_REGEX.search(line, pos)
571        if not opening:
572            break
573        if line[opening.start(0) + 1] == '/': # //...
574            continuation = line
575            # Count the number of line breaks, to keep line numbers aligned
576            # in the output.
577            line_count = 1
578            while continuation.endswith('\\\n'):
579                # This errors out if the file ends with an unfinished line
580                # comment. That's acceptable to not complicate the code further.
581                continuation = next(stream)
582                line_count += 1
583            return line[:opening.start(0)].rstrip() + '\n' * line_count
584        # Parsing /*...*/, looking for the end
585        closing = line.find('*/', opening.end(0))
586        while closing == -1:
587            # This errors out if the file ends with an unfinished block
588            # comment. That's acceptable to not complicate the code further.
589            line += next(stream)
590            closing = line.find('*/', opening.end(0))
591        pos = closing + 2
592        # Replace inner comment by spaces. There needs to be at least one space
593        # for things like 'int/*ihatespaces*/foo'. Go further and preserve the
594        # width of the comment and line breaks, this way positions in error
595        # messages remain correct.
596        line = (line[:opening.start(0)] +
597                re.sub(r'.', r' ', line[opening.start(0):pos]) +
598                line[pos:])
599    # Strip whitespace at the end of lines (it's irrelevant to error messages).
600    return re.sub(r' +(\n|\Z)', r'\1', line)
601
602def parse_function_code(funcs_f, dependencies, suite_dependencies):
603    """
604    Parses out a function from function file object and generates
605    function and dispatch code.
606
607    :param funcs_f: file object of the functions file.
608    :param dependencies: List of dependencies
609    :param suite_dependencies: List of test suite dependencies
610    :return: Function name, arguments, function code and dispatch code.
611    """
612    line_directive = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name)
613    code = ''
614    has_exit_label = False
615    for line in funcs_f:
616        # Check function signature. Function signature may be split
617        # across multiple lines. Here we try to find the start of
618        # arguments list, then remove '\n's and apply the regex to
619        # detect function start.
620        line = skip_comments(line, funcs_f)
621        up_to_arg_list_start = code + line[:line.find('(') + 1]
622        match = re.match(TEST_FUNCTION_VALIDATION_REGEX,
623                         up_to_arg_list_start.replace('\n', ' '), re.I)
624        if match:
625            # check if we have full signature i.e. split in more lines
626            name = match.group('func_name')
627            if not re.match(FUNCTION_ARG_LIST_END_REGEX, line):
628                for lin in funcs_f:
629                    line += skip_comments(lin, funcs_f)
630                    if re.search(FUNCTION_ARG_LIST_END_REGEX, line):
631                        break
632            args, local_vars, args_dispatch = parse_function_arguments(
633                line)
634            code += line
635            break
636        code += line
637    else:
638        raise GeneratorInputError("file: %s - Test functions not found!" %
639                                  funcs_f.name)
640
641    # Prefix test function name with 'test_'
642    code = code.replace(name, 'test_' + name, 1)
643    name = 'test_' + name
644
645    # If a test function has no arguments then add 'void' argument to
646    # avoid "-Wstrict-prototypes" warnings from clang
647    if len(args) == 0:
648        code = code.replace('()', '(void)', 1)
649
650    for line in funcs_f:
651        if re.search(END_CASE_REGEX, line):
652            break
653        if not has_exit_label:
654            has_exit_label = \
655                re.search(EXIT_LABEL_REGEX, line.strip()) is not None
656        code += line
657    else:
658        raise GeneratorInputError("file: %s - end case pattern [%s] not "
659                                  "found!" % (funcs_f.name, END_CASE_REGEX))
660
661    code = line_directive + code
662    code = generate_function_code(name, code, local_vars, args_dispatch,
663                                  dependencies)
664    dispatch_code = gen_dispatch(name, suite_dependencies + dependencies)
665    return (name, args, code, dispatch_code)
666
667
668def parse_functions(funcs_f):
669    """
670    Parses a test_suite_xxx.function file and returns information
671    for generating a C source file for the test suite.
672
673    :param funcs_f: file object of the functions file.
674    :return: List of test suite dependencies, test function dispatch
675             code, function code and a dict with function identifiers
676             and arguments info.
677    """
678    suite_helpers = ''
679    suite_dependencies = []
680    suite_functions = ''
681    func_info = {}
682    function_idx = 0
683    dispatch_code = ''
684    for line in funcs_f:
685        if re.search(BEGIN_HEADER_REGEX, line):
686            suite_helpers += parse_until_pattern(funcs_f, END_HEADER_REGEX)
687        elif re.search(BEGIN_SUITE_HELPERS_REGEX, line):
688            suite_helpers += parse_until_pattern(funcs_f,
689                                                 END_SUITE_HELPERS_REGEX)
690        elif re.search(BEGIN_DEP_REGEX, line):
691            suite_dependencies += parse_suite_dependencies(funcs_f)
692        elif re.search(BEGIN_CASE_REGEX, line):
693            try:
694                dependencies = parse_function_dependencies(line)
695            except GeneratorInputError as error:
696                raise GeneratorInputError(
697                    "%s:%d: %s" % (funcs_f.name, funcs_f.line_no,
698                                   str(error)))
699            func_name, args, func_code, func_dispatch =\
700                parse_function_code(funcs_f, dependencies, suite_dependencies)
701            suite_functions += func_code
702            # Generate dispatch code and enumeration info
703            if func_name in func_info:
704                raise GeneratorInputError(
705                    "file: %s - function %s re-declared at line %d" %
706                    (funcs_f.name, func_name, funcs_f.line_no))
707            func_info[func_name] = (function_idx, args)
708            dispatch_code += '/* Function Id: %d */\n' % function_idx
709            dispatch_code += func_dispatch
710            function_idx += 1
711
712    func_code = (suite_helpers +
713                 suite_functions).join(gen_dependencies(suite_dependencies))
714    return suite_dependencies, dispatch_code, func_code, func_info
715
716
717def escaped_split(inp_str, split_char):
718    """
719    Split inp_str on character split_char but ignore if escaped.
720    Since, return value is used to write back to the intermediate
721    data file, any escape characters in the input are retained in the
722    output.
723
724    :param inp_str: String to split
725    :param split_char: Split character
726    :return: List of splits
727    """
728    if len(split_char) > 1:
729        raise ValueError('Expected split character. Found string!')
730    out = re.sub(r'(\\.)|' + split_char,
731                 lambda m: m.group(1) or '\n', inp_str,
732                 len(inp_str)).split('\n')
733    out = [x for x in out if x]
734    return out
735
736
737def parse_test_data(data_f):
738    """
739    Parses .data file for each test case name, test function name,
740    test dependencies and test arguments. This information is
741    correlated with the test functions file for generating an
742    intermediate data file replacing the strings for test function
743    names, dependencies and integer constant expressions with
744    identifiers. Mainly for optimising space for on-target
745    execution.
746
747    :param data_f: file object of the data file.
748    :return: Generator that yields line number, test name, function name,
749             dependency list and function argument list.
750    """
751    __state_read_name = 0
752    __state_read_args = 1
753    state = __state_read_name
754    dependencies = []
755    name = ''
756    for line in data_f:
757        line = line.strip()
758        # Skip comments
759        if line.startswith('#'):
760            continue
761
762        # Blank line indicates end of test
763        if not line:
764            if state == __state_read_args:
765                raise GeneratorInputError("[%s:%d] Newline before arguments. "
766                                          "Test function and arguments "
767                                          "missing for %s" %
768                                          (data_f.name, data_f.line_no, name))
769            continue
770
771        if state == __state_read_name:
772            # Read test name
773            name = line
774            state = __state_read_args
775        elif state == __state_read_args:
776            # Check dependencies
777            match = re.search(DEPENDENCY_REGEX, line)
778            if match:
779                try:
780                    dependencies = parse_dependencies(
781                        match.group('dependencies'))
782                except GeneratorInputError as error:
783                    raise GeneratorInputError(
784                        str(error) + " - %s:%d" %
785                        (data_f.name, data_f.line_no))
786            else:
787                # Read test vectors
788                parts = escaped_split(line, ':')
789                test_function = parts[0]
790                args = parts[1:]
791                yield data_f.line_no, name, test_function, dependencies, args
792                dependencies = []
793                state = __state_read_name
794    if state == __state_read_args:
795        raise GeneratorInputError("[%s:%d] Newline before arguments. "
796                                  "Test function and arguments missing for "
797                                  "%s" % (data_f.name, data_f.line_no, name))
798
799
800def gen_dep_check(dep_id, dep):
801    """
802    Generate code for checking dependency with the associated
803    identifier.
804
805    :param dep_id: Dependency identifier
806    :param dep: Dependency macro
807    :return: Dependency check code
808    """
809    if dep_id < 0:
810        raise GeneratorInputError("Dependency Id should be a positive "
811                                  "integer.")
812    _not, dep = ('!', dep[1:]) if dep[0] == '!' else ('', dep)
813    if not dep:
814        raise GeneratorInputError("Dependency should not be an empty string.")
815
816    dependency = re.match(CONDITION_REGEX, dep, re.I)
817    if not dependency:
818        raise GeneratorInputError('Invalid dependency %s' % dep)
819
820    _defined = '' if dependency.group(2) else 'defined'
821    _cond = dependency.group(2) if dependency.group(2) else ''
822    _value = dependency.group(3) if dependency.group(3) else ''
823
824    dep_check = '''
825        case {id}:
826            {{
827#if {_not}{_defined}({macro}{_cond}{_value})
828                ret = DEPENDENCY_SUPPORTED;
829#else
830                ret = DEPENDENCY_NOT_SUPPORTED;
831#endif
832            }}
833            break;'''.format(_not=_not, _defined=_defined,
834                             macro=dependency.group(1), id=dep_id,
835                             _cond=_cond, _value=_value)
836    return dep_check
837
838
839def gen_expression_check(exp_id, exp):
840    """
841    Generates code for evaluating an integer expression using
842    associated expression Id.
843
844    :param exp_id: Expression Identifier
845    :param exp: Expression/Macro
846    :return: Expression check code
847    """
848    if exp_id < 0:
849        raise GeneratorInputError("Expression Id should be a positive "
850                                  "integer.")
851    if not exp:
852        raise GeneratorInputError("Expression should not be an empty string.")
853    exp_code = '''
854        case {exp_id}:
855            {{
856                *out_value = {expression};
857            }}
858            break;'''.format(exp_id=exp_id, expression=exp)
859    return exp_code
860
861
862def write_dependencies(out_data_f, test_dependencies, unique_dependencies):
863    """
864    Write dependencies to intermediate test data file, replacing
865    the string form with identifiers. Also, generates dependency
866    check code.
867
868    :param out_data_f: Output intermediate data file
869    :param test_dependencies: Dependencies
870    :param unique_dependencies: Mutable list to track unique dependencies
871           that are global to this re-entrant function.
872    :return: returns dependency check code.
873    """
874    dep_check_code = ''
875    if test_dependencies:
876        out_data_f.write('depends_on')
877        for dep in test_dependencies:
878            if dep not in unique_dependencies:
879                unique_dependencies.append(dep)
880                dep_id = unique_dependencies.index(dep)
881                dep_check_code += gen_dep_check(dep_id, dep)
882            else:
883                dep_id = unique_dependencies.index(dep)
884            out_data_f.write(':' + str(dep_id))
885        out_data_f.write('\n')
886    return dep_check_code
887
888
889INT_VAL_REGEX = re.compile(r'-?(\d+|0x[0-9a-f]+)$', re.I)
890def val_is_int(val: str) -> bool:
891    """Whether val is suitable as an 'int' parameter in the .datax file."""
892    if not INT_VAL_REGEX.match(val):
893        return False
894    # Limit the range to what is guaranteed to get through strtol()
895    return abs(int(val, 0)) <= 0x7fffffff
896
897def write_parameters(out_data_f, test_args, func_args, unique_expressions):
898    """
899    Writes test parameters to the intermediate data file, replacing
900    the string form with identifiers. Also, generates expression
901    check code.
902
903    :param out_data_f: Output intermediate data file
904    :param test_args: Test parameters
905    :param func_args: Function arguments
906    :param unique_expressions: Mutable list to track unique
907           expressions that are global to this re-entrant function.
908    :return: Returns expression check code.
909    """
910    expression_code = ''
911    for i, _ in enumerate(test_args):
912        typ = func_args[i]
913        val = test_args[i]
914
915        # Pass small integer constants literally. This reduces the size of
916        # the C code. Register anything else as an expression.
917        if typ == 'int' and not val_is_int(val):
918            typ = 'exp'
919            if val not in unique_expressions:
920                unique_expressions.append(val)
921                # exp_id can be derived from len(). But for
922                # readability and consistency with case of existing
923                # let's use index().
924                exp_id = unique_expressions.index(val)
925                expression_code += gen_expression_check(exp_id, val)
926                val = exp_id
927            else:
928                val = unique_expressions.index(val)
929        out_data_f.write(':' + typ + ':' + str(val))
930    out_data_f.write('\n')
931    return expression_code
932
933
934def gen_suite_dep_checks(suite_dependencies, dep_check_code, expression_code):
935    """
936    Generates preprocessor checks for test suite dependencies.
937
938    :param suite_dependencies: Test suite dependencies read from the
939            .function file.
940    :param dep_check_code: Dependency check code
941    :param expression_code: Expression check code
942    :return: Dependency and expression code guarded by test suite
943             dependencies.
944    """
945    if suite_dependencies:
946        preprocessor_check = gen_dependencies_one_line(suite_dependencies)
947        dep_check_code = '''
948{preprocessor_check}
949{code}
950#endif
951'''.format(preprocessor_check=preprocessor_check, code=dep_check_code)
952        expression_code = '''
953{preprocessor_check}
954{code}
955#endif
956'''.format(preprocessor_check=preprocessor_check, code=expression_code)
957    return dep_check_code, expression_code
958
959
960def get_function_info(func_info, function_name, line_no):
961    """Look up information about a test function by name.
962
963    Raise an informative expression if function_name is not found.
964
965    :param func_info: dictionary mapping function names to their information.
966    :param function_name: the function name as written in the .function and
967                          .data files.
968    :param line_no: line number for error messages.
969    :return Function information (id, args).
970    """
971    test_function_name = 'test_' + function_name
972    if test_function_name not in func_info:
973        raise GeneratorInputError("%d: Function %s not found!" %
974                                  (line_no, test_function_name))
975    return func_info[test_function_name]
976
977
978def gen_from_test_data(data_f, out_data_f, func_info, suite_dependencies):
979    """
980    This function reads test case name, dependencies and test vectors
981    from the .data file. This information is correlated with the test
982    functions file for generating an intermediate data file replacing
983    the strings for test function names, dependencies and integer
984    constant expressions with identifiers. Mainly for optimising
985    space for on-target execution.
986    It also generates test case dependency check code and expression
987    evaluation code.
988
989    :param data_f: Data file object
990    :param out_data_f: Output intermediate data file
991    :param func_info: Dict keyed by function and with function id
992           and arguments info
993    :param suite_dependencies: Test suite dependencies
994    :return: Returns dependency and expression check code
995    """
996    unique_dependencies = []
997    unique_expressions = []
998    dep_check_code = ''
999    expression_code = ''
1000    for line_no, test_name, function_name, test_dependencies, test_args in \
1001            parse_test_data(data_f):
1002        out_data_f.write(test_name + '\n')
1003
1004        # Write dependencies
1005        dep_check_code += write_dependencies(out_data_f, test_dependencies,
1006                                             unique_dependencies)
1007
1008        # Write test function name
1009        func_id, func_args = \
1010            get_function_info(func_info, function_name, line_no)
1011        out_data_f.write(str(func_id))
1012
1013        # Write parameters
1014        if len(test_args) != len(func_args):
1015            raise GeneratorInputError("%d: Invalid number of arguments in test "
1016                                      "%s. See function %s signature." %
1017                                      (line_no, test_name, function_name))
1018        expression_code += write_parameters(out_data_f, test_args, func_args,
1019                                            unique_expressions)
1020
1021        # Write a newline as test case separator
1022        out_data_f.write('\n')
1023
1024    dep_check_code, expression_code = gen_suite_dep_checks(
1025        suite_dependencies, dep_check_code, expression_code)
1026    return dep_check_code, expression_code
1027
1028
1029def add_input_info(funcs_file, data_file, template_file,
1030                   c_file, snippets):
1031    """
1032    Add generator input info in snippets.
1033
1034    :param funcs_file: Functions file object
1035    :param data_file: Data file object
1036    :param template_file: Template file object
1037    :param c_file: Output C file object
1038    :param snippets: Dictionary to contain code pieces to be
1039                     substituted in the template.
1040    :return:
1041    """
1042    snippets['test_file'] = c_file
1043    snippets['test_main_file'] = template_file
1044    snippets['test_case_file'] = funcs_file
1045    snippets['test_case_data_file'] = data_file
1046
1047
1048def read_code_from_input_files(platform_file, helpers_file,
1049                               out_data_file, snippets):
1050    """
1051    Read code from input files and create substitutions for replacement
1052    strings in the template file.
1053
1054    :param platform_file: Platform file object
1055    :param helpers_file: Helper functions file object
1056    :param out_data_file: Output intermediate data file object
1057    :param snippets: Dictionary to contain code pieces to be
1058                     substituted in the template.
1059    :return:
1060    """
1061    # Read helpers
1062    with open(helpers_file, 'r') as help_f, open(platform_file, 'r') as \
1063            platform_f:
1064        snippets['test_common_helper_file'] = helpers_file
1065        snippets['test_common_helpers'] = help_f.read()
1066        snippets['test_platform_file'] = platform_file
1067        snippets['platform_code'] = platform_f.read().replace(
1068            'DATA_FILE', out_data_file.replace('\\', '\\\\'))  # escape '\'
1069
1070
1071def write_test_source_file(template_file, c_file, snippets):
1072    """
1073    Write output source file with generated source code.
1074
1075    :param template_file: Template file name
1076    :param c_file: Output source file
1077    :param snippets: Generated and code snippets
1078    :return:
1079    """
1080
1081    # Create a placeholder pattern with the correct named capture groups
1082    # to override the default provided with Template.
1083    # Match nothing (no way of escaping placeholders).
1084    escaped = "(?P<escaped>(?!))"
1085    # Match the "__MBEDTLS_TEST_TEMPLATE__PLACEHOLDER_NAME" pattern.
1086    named = "__MBEDTLS_TEST_TEMPLATE__(?P<named>[A-Z][_A-Z0-9]*)"
1087    # Match nothing (no braced placeholder syntax).
1088    braced = "(?P<braced>(?!))"
1089    # If not already matched, a "__MBEDTLS_TEST_TEMPLATE__" prefix is invalid.
1090    invalid = "(?P<invalid>__MBEDTLS_TEST_TEMPLATE__)"
1091    placeholder_pattern = re.compile("|".join([escaped, named, braced, invalid]))
1092
1093    with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
1094        for line_no, line in enumerate(template_f.readlines(), 1):
1095            # Update line number. +1 as #line directive sets next line number
1096            snippets['line_no'] = line_no + 1
1097            template = string.Template(line)
1098            template.pattern = placeholder_pattern
1099            snippets = {k.upper():v for (k, v) in snippets.items()}
1100            code = template.substitute(**snippets)
1101            c_f.write(code)
1102
1103
1104def parse_function_file(funcs_file, snippets):
1105    """
1106    Parse function file and generate function dispatch code.
1107
1108    :param funcs_file: Functions file name
1109    :param snippets: Dictionary to contain code pieces to be
1110                     substituted in the template.
1111    :return:
1112    """
1113    with FileWrapper(funcs_file) as funcs_f:
1114        suite_dependencies, dispatch_code, func_code, func_info = \
1115            parse_functions(funcs_f)
1116        snippets['functions_code'] = func_code
1117        snippets['dispatch_code'] = dispatch_code
1118        return suite_dependencies, func_info
1119
1120
1121def generate_intermediate_data_file(data_file, out_data_file,
1122                                    suite_dependencies, func_info, snippets):
1123    """
1124    Generates intermediate data file from input data file and
1125    information read from functions file.
1126
1127    :param data_file: Data file name
1128    :param out_data_file: Output/Intermediate data file
1129    :param suite_dependencies: List of suite dependencies.
1130    :param func_info: Function info parsed from functions file.
1131    :param snippets: Dictionary to contain code pieces to be
1132                     substituted in the template.
1133    :return:
1134    """
1135    with FileWrapper(data_file) as data_f, \
1136            open(out_data_file, 'w') as out_data_f:
1137        dep_check_code, expression_code = gen_from_test_data(
1138            data_f, out_data_f, func_info, suite_dependencies)
1139        snippets['dep_check_code'] = dep_check_code
1140        snippets['expression_code'] = expression_code
1141
1142
1143def generate_code(**input_info):
1144    """
1145    Generates C source code from test suite file, data file, common
1146    helpers file and platform file.
1147
1148    input_info expands to following parameters:
1149    funcs_file: Functions file object
1150    data_file: Data file object
1151    template_file: Template file object
1152    platform_file: Platform file object
1153    helpers_file: Helper functions file object
1154    suites_dir: Test suites dir
1155    c_file: Output C file object
1156    out_data_file: Output intermediate data file object
1157    :return:
1158    """
1159    funcs_file = input_info['funcs_file']
1160    data_file = input_info['data_file']
1161    template_file = input_info['template_file']
1162    platform_file = input_info['platform_file']
1163    helpers_file = input_info['helpers_file']
1164    suites_dir = input_info['suites_dir']
1165    c_file = input_info['c_file']
1166    out_data_file = input_info['out_data_file']
1167    for name, path in [('Functions file', funcs_file),
1168                       ('Data file', data_file),
1169                       ('Template file', template_file),
1170                       ('Platform file', platform_file),
1171                       ('Helpers code file', helpers_file),
1172                       ('Suites dir', suites_dir)]:
1173        if not os.path.exists(path):
1174            raise IOError("ERROR: %s [%s] not found!" % (name, path))
1175
1176    snippets = {'generator_script': os.path.basename(__file__)}
1177    read_code_from_input_files(platform_file, helpers_file,
1178                               out_data_file, snippets)
1179    add_input_info(funcs_file, data_file, template_file,
1180                   c_file, snippets)
1181    suite_dependencies, func_info = parse_function_file(funcs_file, snippets)
1182    generate_intermediate_data_file(data_file, out_data_file,
1183                                    suite_dependencies, func_info, snippets)
1184    write_test_source_file(template_file, c_file, snippets)
1185
1186
1187def main():
1188    """
1189    Command line parser.
1190
1191    :return:
1192    """
1193    parser = argparse.ArgumentParser(
1194        description='Dynamically generate test suite code.')
1195
1196    parser.add_argument("-f", "--functions-file",
1197                        dest="funcs_file",
1198                        help="Functions file",
1199                        metavar="FUNCTIONS_FILE",
1200                        required=True)
1201
1202    parser.add_argument("-d", "--data-file",
1203                        dest="data_file",
1204                        help="Data file",
1205                        metavar="DATA_FILE",
1206                        required=True)
1207
1208    parser.add_argument("-t", "--template-file",
1209                        dest="template_file",
1210                        help="Template file",
1211                        metavar="TEMPLATE_FILE",
1212                        required=True)
1213
1214    parser.add_argument("-s", "--suites-dir",
1215                        dest="suites_dir",
1216                        help="Suites dir",
1217                        metavar="SUITES_DIR",
1218                        required=True)
1219
1220    parser.add_argument("--helpers-file",
1221                        dest="helpers_file",
1222                        help="Helpers file",
1223                        metavar="HELPERS_FILE",
1224                        required=True)
1225
1226    parser.add_argument("-p", "--platform-file",
1227                        dest="platform_file",
1228                        help="Platform code file",
1229                        metavar="PLATFORM_FILE",
1230                        required=True)
1231
1232    parser.add_argument("-o", "--out-dir",
1233                        dest="out_dir",
1234                        help="Dir where generated code and scripts are copied",
1235                        metavar="OUT_DIR",
1236                        required=True)
1237
1238    args = parser.parse_args()
1239
1240    data_file_name = os.path.basename(args.data_file)
1241    data_name = os.path.splitext(data_file_name)[0]
1242
1243    out_c_file = os.path.join(args.out_dir, data_name + '.c')
1244    out_data_file = os.path.join(args.out_dir, data_name + '.datax')
1245
1246    out_c_file_dir = os.path.dirname(out_c_file)
1247    out_data_file_dir = os.path.dirname(out_data_file)
1248    for directory in [out_c_file_dir, out_data_file_dir]:
1249        if not os.path.exists(directory):
1250            os.makedirs(directory)
1251
1252    generate_code(funcs_file=args.funcs_file, data_file=args.data_file,
1253                  template_file=args.template_file,
1254                  platform_file=args.platform_file,
1255                  helpers_file=args.helpers_file, suites_dir=args.suites_dir,
1256                  c_file=out_c_file, out_data_file=out_data_file)
1257
1258
1259if __name__ == "__main__":
1260    try:
1261        main()
1262    except GeneratorInputError as err:
1263        sys.exit("%s: input error: %s" %
1264                 (os.path.basename(sys.argv[0]), str(err)))
1265