blob: 9fd9a00454ed616520653adeb66a3b6a1a8d200d [file] [log] [blame]
Mohammad Azim Khanfff49042017-03-28 01:48:31 +01001"""
2mbed SDK
3Copyright (c) 2017-2018 ARM Limited
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16"""
17
18import os
19import re
20import argparse
21import shutil
22
23
24"""
25Generates code in following structure.
26
27<output dir>/
28|-- host_tests/
29| |-- mbedtls_test.py
30| |-- suites/
31| | |-- *.data files
32| |-- mbedtls/
33| | |-- <test suite #1>/
34| | | |-- main.c
35| | ...
36| | |-- <test suite #n>/
37| | | |-- main.c
38| | |
39"""
40
41
42BEGIN_HEADER_REGEX = '/\*\s*BEGIN_HEADER\s*\*/'
43END_HEADER_REGEX = '/\*\s*END_HEADER\s*\*/'
44
45BEGIN_DEP_REGEX = 'BEGIN_DEPENDENCIES'
46END_DEP_REGEX = 'END_DEPENDENCIES'
47
48BEGIN_CASE_REGEX = '/\*\s*BEGIN_CASE\s*(.*?)\s*\*/'
49END_CASE_REGEX = '/\*\s*END_CASE\s*\*/'
50
51
52class InvalidFileFormat(Exception):
53 """
54 Exception to indicate invalid file format.
55 """
56 pass
57
58
59def gen_deps(deps):
60 """
61 Generates dependency i.e. if def and endif code
62
63 :param deps:
64 :return:
65 """
66 dep_start = ''
67 dep_end = ''
68 for dep in deps:
69 if dep[0] == '!':
70 noT = '!'
71 dep = dep[1:]
72 else:
73 noT = ''
74 dep_start += '#if %sdefined(%s)\n' % (noT, dep)
75 dep_end = '#endif /* %s%s */\n' % (noT, dep) + dep_end
76 return dep_start, dep_end
77
78
79def gen_deps_one_line(deps):
80 """
81 Generates dependency checks in one line. Useful for writing code in #else case.
82
83 :param deps:
84 :return:
85 """
86 defines = []
87 for dep in deps:
88 if dep[0] == '!':
89 noT = '!'
90 dep = dep[1:]
91 else:
92 noT = ''
93 defines.append('%sdefined(%s)' % (noT, dep))
94 return '#if ' + ' && '.join(defines)
95
96
97def gen_function_wrapper(name, args_dispatch):
98 """
99 Creates test function code
100
101 :param name:
102 :param args_dispatch:
103 :return:
104 """
105 # Then create the wrapper
106 wrapper = '''
107void {name}_wrapper( void ** params )
108{{
109 {unused_params}
110 {name}( {args} );
111}}
112'''.format(name=name, unused_params='(void)params;' if len(args_dispatch) == 0 else '', args=', '.join(args_dispatch))
113 return wrapper
114
115
116def gen_dispatch(name, deps):
117 """
118 Generates dispatch condition for the functions.
119
120 :param name:
121 :param deps:
122 :return:
123 """
124 if len(deps):
125 ifdef = gen_deps_one_line(deps)
126 dispatch_code = '''
127{ifdef}
128 {name}_wrapper,
129#else
130 NULL,
131#endif
132'''.format(ifdef=ifdef, name=name)
133 else:
134 dispatch_code = '''
135 {name}_wrapper,
136'''.format(name=name)
137
138 return dispatch_code
139
140
141def parse_suite_headers(line_no, funcs_f):
142 """
143 Parses function headers.
144
145 :param line_no:
146 :param funcs_f:
147 :return:
148 """
149 headers = '#line %d "%s"\n' % (line_no + 1, funcs_f.name)
150 for line in funcs_f:
151 line_no += 1
152 if re.search(END_HEADER_REGEX, line):
153 break
154 headers += line
155 else:
156 raise InvalidFileFormat("file: %s - end header pattern [%s] not found!" % (funcs_f.name, END_HEADER_REGEX))
157
158 return line_no, headers
159
160
161def parse_suite_deps(line_no, funcs_f):
162 """
163 Parses function dependencies.
164
165 :param line_no:
166 :param funcs_f:
167 :return:
168 """
169 deps = []
170 for line in funcs_f:
171 line_no += 1
172 m = re.search('depends_on\:(.*)', line.strip())
173 if m:
174 deps += [x.strip() for x in m.group(1).split(':')]
175 if re.search(END_DEP_REGEX, line):
176 break
177 else:
178 raise InvalidFileFormat("file: %s - end dependency pattern [%s] not found!" % (funcs_f.name, END_DEP_REGEX))
179
180 return line_no, deps
181
182
183def parse_function_deps(line):
184 """
185
186 :param line:
187 :return:
188 """
189 deps = []
190 m = re.search(BEGIN_CASE_REGEX, line)
191 dep_str = m.group(1)
192 if len(dep_str):
193 m = re.search('depends_on:(.*)', dep_str)
194 if m:
195 deps = m.group(1).strip().split(':')
196 return deps
197
198
199def parse_function_signature(line):
200 """
201 Parsing function signature
202
203 :param line:
204 :return:
205 """
206 args = []
207 args_dispatch = []
208 m = re.search('\s*void\s+(\w+)\s*\(', line, re.I)
209 if not m:
210 raise ValueError("Test function should return 'void'\n%s" % line)
211 name = m.group(1)
212 line = line[len(m.group(0)):]
213 arg_idx = 0
214 for arg in line[:line.find(')')].split(','):
215 arg = arg.strip()
216 if arg == '':
217 continue
218 if re.search('int\s+.*', arg.strip()):
219 args.append('int')
220 args_dispatch.append('*( (int *) params[%d] )' % arg_idx)
221 elif re.search('char\s*\*\s*.*', arg.strip()):
222 args.append('char*')
223 args_dispatch.append('(char *) params[%d]' % arg_idx)
224 else:
225 raise ValueError("Test function arguments can only be 'int' or 'char *'\n%s" % line)
226 arg_idx += 1
227
228 return name, args, args_dispatch
229
230
231def parse_function_code(line_no, funcs_f, deps, suite_deps):
232 """
233
234 :param line_no:
235 :param funcs_f:
236 :param deps:
237 :param suite_deps:
238 :return:
239 """
240 code = '#line %d "%s"\n' % (line_no + 1, funcs_f.name)
241 for line in funcs_f:
242 line_no += 1
243 # Check function signature
244 m = re.match('.*?\s+(\w+)\s*\(', line, re.I)
245 if m:
246 # check if we have full signature i.e. split in more lines
247 if not re.match('.*\)', line):
248 for lin in funcs_f:
249 line += lin
250 line_no += 1
251 if re.search('.*?\)', line):
252 break
253 name, args, args_dispatch = parse_function_signature(line)
254 code += line.replace(name, 'test_' + name)
255 name = 'test_' + name
256 break
257 else:
258 raise InvalidFileFormat("file: %s - Test functions not found!" % funcs_f.name)
259
260 for line in funcs_f:
261 line_no += 1
262 if re.search(END_CASE_REGEX, line):
263 break
264 code += line
265 else:
266 raise InvalidFileFormat("file: %s - end case pattern [%s] not found!" % (funcs_f.name, END_CASE_REGEX))
267
268 # Add exit label if not present
269 if code.find('exit:') == -1:
270 s = code.rsplit('}', 1)
271 if len(s) == 2:
272 code = """
273exit:
274 ;;
275}
276""".join(s)
277
278 code += gen_function_wrapper(name, args_dispatch)
279 ifdef, endif = gen_deps(deps)
280 dispatch_code = gen_dispatch(name, suite_deps + deps)
281 return line_no, name, args, ifdef + code + endif, dispatch_code
282
283
284def parse_functions(funcs_f):
285 """
286 Returns functions code pieces
287
288 :param funcs_f:
289 :return:
290 """
291 line_no = 0
292 suite_headers = ''
293 suite_deps = []
294 suite_functions = ''
295 func_info = {}
296 function_idx = 0
297 dispatch_code = ''
298 for line in funcs_f:
299 line_no += 1
300 if re.search(BEGIN_HEADER_REGEX, line):
301 line_no, headers = parse_suite_headers(line_no, funcs_f)
302 suite_headers += headers
303 elif re.search(BEGIN_DEP_REGEX, line):
304 line_no, deps = parse_suite_deps(line_no, funcs_f)
305 suite_deps += deps
306 elif re.search(BEGIN_CASE_REGEX, line):
307 deps = parse_function_deps(line)
308 line_no, func_name, args, func_code, func_dispatch = parse_function_code(line_no, funcs_f, deps, suite_deps)
309 suite_functions += func_code
310 # Generate dispatch code and enumeration info
311 assert func_name not in func_info, "file: %s - function %s re-declared at line %d" % \
312 (funcs_f.name, func_name, line_no)
313 func_info[func_name] = (function_idx, args)
314 dispatch_code += '/* Function Id: %d */\n' % function_idx
315 dispatch_code += func_dispatch
316 function_idx += 1
317
318 ifdef, endif = gen_deps(suite_deps)
319 func_code = ifdef + suite_functions + endif
320 return dispatch_code, suite_headers, func_code, func_info
321
322
323def escaped_split(str, ch):
324 """
325 Split str on character ch but ignore escaped \{ch}
326
327 :param str:
328 :param ch:
329 :return:
330 """
331 if len(ch) > 1:
332 raise ValueError('Expected split character. Found string!')
333 out = []
334 part = ''
335 escape = False
336 for i in range(len(str)):
337 if not escape and str[i] == ch:
338 out.append(part)
339 part = ''
340 else:
341 part += str[i]
342 escape = not escape and str[i] == '\\'
343 if len(part):
344 out.append(part)
345 return out
346
347
348def parse_test_data(data_f):
349 """
350 Parses .data file
351
352 :param data_f:
353 :return:
354 """
355 STATE_READ_NAME = 0
356 STATE_READ_ARGS = 1
357 state = STATE_READ_NAME
358 deps = []
359
360 for line in data_f:
361 line = line.strip()
362 if len(line) and line[0] == '#': # Skip comments
363 continue
364
365 # skip blank lines
366 if len(line) == 0:
367 continue
368
369 if state == STATE_READ_NAME:
370 # Read test name
371 name = line
372 state = STATE_READ_ARGS
373 elif state == STATE_READ_ARGS:
374 # Check dependencies
375 m = re.search('depends_on\:(.*)', line)
376 if m:
377 deps = m.group(1).split(':')
378 else:
379 # Read test vectors
380 parts = escaped_split(line, ':')
381 function = parts[0]
382 args = parts[1:]
383 yield name, function, deps, args
384 deps = []
385 state = STATE_READ_NAME
386
387
388def gen_dep_check(dep_id, dep):
389 """
390 Generate code for the dependency.
391
392 :param dep_id:
393 :param dep:
394 :return:
395 """
396 if dep[0] == '!':
397 noT = '!'
398 dep = dep[1:]
399 else:
400 noT = ''
401 dep_check = '''
402if ( dep_id == {id} )
403{{
404#if {noT}defined({macro})
405 return( DEPENDENCY_SUPPORTED );
406#else
407 return( DEPENDENCY_NOT_SUPPORTED );
408#endif
409}}
410else
411'''.format(noT=noT, macro=dep, id=dep_id)
412
413 return dep_check
414
415
416def gen_expression_check(exp_id, exp):
417 """
418 Generates code for expression check
419
420 :param exp_id:
421 :param exp:
422 :return:
423 """
424 exp_code = '''
425if ( exp_id == {exp_id} )
426{{
427 *out_value = {expression};
428}}
429else
430'''.format(exp_id=exp_id, expression=exp)
431 return exp_code
432
433
434def gen_from_test_data(data_f, out_data_f, func_info):
435 """
436 Generates dependency checks, expression code and intermediate data file from test data file.
437
438 :param data_f:
439 :param out_data_f:
440 :param func_info:
441 :return:
442 """
443 unique_deps = []
444 unique_expressions = []
445 dep_check_code = ''
446 expression_code = ''
447 for test_name, function_name, test_deps, test_args in parse_test_data(data_f):
448 out_data_f.write(test_name + '\n')
449
450 func_id, func_args = func_info['test_' + function_name]
451 if len(test_deps):
452 out_data_f.write('depends_on')
453 for dep in test_deps:
454 if dep not in unique_deps:
455 unique_deps.append(dep)
456 dep_id = unique_deps.index(dep)
457 dep_check_code += gen_dep_check(dep_id, dep)
458 else:
459 dep_id = unique_deps.index(dep)
460 out_data_f.write(':' + str(dep_id))
461 out_data_f.write('\n')
462
463 assert len(test_args) == len(func_args), \
464 "Invalid number of arguments in test %s. See function %s signature." % (test_name, function_name)
465 out_data_f.write(str(func_id))
466 for i in xrange(len(test_args)):
467 typ = func_args[i]
468 val = test_args[i]
469
470 # check if val is a non literal int val
471 if typ == 'int' and not re.match('\d+', val): # its an expression # FIXME: Handle hex format. Tip: instead try converting int(str, 10) and int(str, 16)
472 typ = 'exp'
473 if val not in unique_expressions:
474 unique_expressions.append(val)
475 # exp_id can be derived from len(). But for readability and consistency with case of existing let's
476 # use index().
477 exp_id = unique_expressions.index(val)
478 expression_code += gen_expression_check(exp_id, val)
479 val = exp_id
480 else:
481 val = unique_expressions.index(val)
482 out_data_f.write(':' + typ + ':' + str(val))
483 out_data_f.write('\n\n')
484
485 # void unused params
486 if len(dep_check_code) == 0:
487 dep_check_code = '(void) dep_id;\n'
488 if len(expression_code) == 0:
489 expression_code = '(void) exp_id;\n'
490 expression_code += '(void) out_value;\n'
491
492 return dep_check_code, expression_code
493
494
495def gen_mbed_code(funcs_file, data_file, template_file, platform_file, help_file, suites_dir, c_file, out_data_file):
496 """
497 Generate mbed-os test code.
498
499 :param funcs_file:
500 :param dat a_file:
501 :param template_file:
502 :param platform_file:
503 :param help_file:
504 :param suites_dir:
505 :param c_file:
506 :param out_data_file:
507 :return:
508 """
509 for name, path in [('Functions file', funcs_file),
510 ('Data file', data_file),
511 ('Template file', template_file),
512 ('Platform file', platform_file),
513 ('Help code file', help_file),
514 ('Suites dir', suites_dir)]:
515 if not os.path.exists(path):
516 raise IOError("ERROR: %s [%s] not found!" % (name, path))
517
518 snippets = {'generator_script' : os.path.basename(__file__)}
519
520 # Read helpers
521 with open(help_file, 'r') as help_f, open(platform_file, 'r') as platform_f:
522 snippets['test_common_helper_file'] = help_file
523 snippets['test_common_helpers'] = help_f.read()
524 snippets['test_platform_file'] = platform_file
525 snippets['platform_code'] = platform_f.read().replace('DATA_FILE',
526 out_data_file.replace('\\', '\\\\')) # escape '\'
527
528 # Function code
529 with open(funcs_file, 'r') as funcs_f, open(data_file, 'r') as data_f, open(out_data_file, 'w') as out_data_f:
530 dispatch_code, func_headers, func_code, func_info = parse_functions(funcs_f)
531 snippets['function_headers'] = func_headers
532 snippets['functions_code'] = func_code
533 snippets['dispatch_code'] = dispatch_code
534 dep_check_code, expression_code = gen_from_test_data(data_f, out_data_f, func_info)
535 snippets['dep_check_code'] = dep_check_code
536 snippets['expression_code'] = expression_code
537
538 snippets['test_file'] = c_file
539 snippets['test_main_file'] = template_file
540 snippets['test_case_file'] = funcs_file
541 snippets['test_case_data_file'] = data_file
542 # Read Template
543 # Add functions
544 #
545 with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
546 line_no = 1
547 for line in template_f.readlines():
548 snippets['line_no'] = line_no + 1 # Increment as it sets next line number
549 code = line.format(**snippets)
550 c_f.write(code)
551 line_no += 1
552
553
554def check_cmd():
555 """
556 Command line parser.
557
558 :return:
559 """
560 parser = argparse.ArgumentParser(description='Generate code for mbed-os tests.')
561
562 parser.add_argument("-f", "--functions-file",
563 dest="funcs_file",
564 help="Functions file",
565 metavar="FUNCTIONS",
566 required=True)
567
568 parser.add_argument("-d", "--data-file",
569 dest="data_file",
570 help="Data file",
571 metavar="DATA",
572 required=True)
573
574 parser.add_argument("-t", "--template-file",
575 dest="template_file",
576 help="Template file",
577 metavar="TEMPLATE",
578 required=True)
579
580 parser.add_argument("-s", "--suites-dir",
581 dest="suites_dir",
582 help="Suites dir",
583 metavar="SUITES",
584 required=True)
585
586 parser.add_argument("--help-file",
587 dest="help_file",
588 help="Help file",
589 metavar="HELPER",
590 required=True)
591
592 parser.add_argument("-p", "--platform-file",
593 dest="platform_file",
594 help="Platform code file",
595 metavar="PLATFORM_FILE",
596 required=True)
597
598 parser.add_argument("-o", "--out-dir",
599 dest="out_dir",
600 help="Dir where generated code and scripts are copied",
601 metavar="OUT_DIR",
602 required=True)
603
604 args = parser.parse_args()
605
606 data_file_name = os.path.basename(args.data_file)
607 data_name = os.path.splitext(data_file_name)[0]
608
609 out_c_file = os.path.join(args.out_dir, data_name + '.c')
610 out_data_file = os.path.join(args.out_dir, data_file_name)
611
612 out_c_file_dir = os.path.dirname(out_c_file)
613 out_data_file_dir = os.path.dirname(out_data_file)
614 for d in [out_c_file_dir, out_data_file_dir]:
615 if not os.path.exists(d):
616 os.makedirs(d)
617
618 gen_mbed_code(args.funcs_file, args.data_file, args.template_file, args.platform_file,
619 args.help_file, args.suites_dir, out_c_file, out_data_file)
620
621
622if __name__ == "__main__":
623 check_cmd()