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