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