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