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