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