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