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