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