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