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