generate_test_code.py 42 KB

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