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