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