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