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