blob: c63555de7800b9ef68255ffc41b209abf73f072e [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
Azim Khana57a4202017-05-31 20:32:32 +0100214 last_was_hex = False
Mohammad Azim Khanfff49042017-03-28 01:48:31 +0100215 for arg in line[:line.find(')')].split(','):
216 arg = arg.strip()
217 if arg == '':
218 continue
219 if re.search('int\s+.*', arg.strip()):
220 args.append('int')
221 args_dispatch.append('*( (int *) params[%d] )' % arg_idx)
222 elif re.search('char\s*\*\s*.*', arg.strip()):
223 args.append('char*')
224 args_dispatch.append('(char *) params[%d]' % arg_idx)
Azim Khana57a4202017-05-31 20:32:32 +0100225 elif re.search('uint8_t\s*\*\s*.*', arg.strip()):
226 args.append('hex')
227 args_dispatch.append('(uint8_t *) params[%d]' % arg_idx)
228 last_was_hex = True
229 elif re.search('uint32_t\s+.*', arg.strip()) and last_was_hex:
230 last_was_hex = False
231 args_dispatch.append('*( (uint32_t *) params[%d] )' % arg_idx)
Mohammad Azim Khanfff49042017-03-28 01:48:31 +0100232 else:
233 raise ValueError("Test function arguments can only be 'int' or 'char *'\n%s" % line)
234 arg_idx += 1
235
236 return name, args, args_dispatch
237
238
239def parse_function_code(line_no, funcs_f, deps, suite_deps):
240 """
241
242 :param line_no:
243 :param funcs_f:
244 :param deps:
245 :param suite_deps:
246 :return:
247 """
248 code = '#line %d "%s"\n' % (line_no + 1, funcs_f.name)
249 for line in funcs_f:
250 line_no += 1
251 # Check function signature
252 m = re.match('.*?\s+(\w+)\s*\(', line, re.I)
253 if m:
254 # check if we have full signature i.e. split in more lines
255 if not re.match('.*\)', line):
256 for lin in funcs_f:
257 line += lin
258 line_no += 1
259 if re.search('.*?\)', line):
260 break
261 name, args, args_dispatch = parse_function_signature(line)
262 code += line.replace(name, 'test_' + name)
263 name = 'test_' + name
264 break
265 else:
266 raise InvalidFileFormat("file: %s - Test functions not found!" % funcs_f.name)
267
268 for line in funcs_f:
269 line_no += 1
270 if re.search(END_CASE_REGEX, line):
271 break
272 code += line
273 else:
274 raise InvalidFileFormat("file: %s - end case pattern [%s] not found!" % (funcs_f.name, END_CASE_REGEX))
275
276 # Add exit label if not present
277 if code.find('exit:') == -1:
278 s = code.rsplit('}', 1)
279 if len(s) == 2:
280 code = """
281exit:
282 ;;
283}
284""".join(s)
285
286 code += gen_function_wrapper(name, args_dispatch)
287 ifdef, endif = gen_deps(deps)
288 dispatch_code = gen_dispatch(name, suite_deps + deps)
289 return line_no, name, args, ifdef + code + endif, dispatch_code
290
291
292def parse_functions(funcs_f):
293 """
294 Returns functions code pieces
295
296 :param funcs_f:
297 :return:
298 """
299 line_no = 0
300 suite_headers = ''
301 suite_deps = []
302 suite_functions = ''
303 func_info = {}
304 function_idx = 0
305 dispatch_code = ''
306 for line in funcs_f:
307 line_no += 1
308 if re.search(BEGIN_HEADER_REGEX, line):
309 line_no, headers = parse_suite_headers(line_no, funcs_f)
310 suite_headers += headers
311 elif re.search(BEGIN_DEP_REGEX, line):
312 line_no, deps = parse_suite_deps(line_no, funcs_f)
313 suite_deps += deps
314 elif re.search(BEGIN_CASE_REGEX, line):
315 deps = parse_function_deps(line)
316 line_no, func_name, args, func_code, func_dispatch = parse_function_code(line_no, funcs_f, deps, suite_deps)
317 suite_functions += func_code
318 # Generate dispatch code and enumeration info
319 assert func_name not in func_info, "file: %s - function %s re-declared at line %d" % \
320 (funcs_f.name, func_name, line_no)
321 func_info[func_name] = (function_idx, args)
322 dispatch_code += '/* Function Id: %d */\n' % function_idx
323 dispatch_code += func_dispatch
324 function_idx += 1
325
326 ifdef, endif = gen_deps(suite_deps)
327 func_code = ifdef + suite_functions + endif
328 return dispatch_code, suite_headers, func_code, func_info
329
330
331def escaped_split(str, ch):
332 """
333 Split str on character ch but ignore escaped \{ch}
334
335 :param str:
336 :param ch:
337 :return:
338 """
339 if len(ch) > 1:
340 raise ValueError('Expected split character. Found string!')
341 out = []
342 part = ''
343 escape = False
344 for i in range(len(str)):
345 if not escape and str[i] == ch:
346 out.append(part)
347 part = ''
348 else:
349 part += str[i]
350 escape = not escape and str[i] == '\\'
351 if len(part):
352 out.append(part)
353 return out
354
355
356def parse_test_data(data_f):
357 """
358 Parses .data file
359
360 :param data_f:
361 :return:
362 """
363 STATE_READ_NAME = 0
364 STATE_READ_ARGS = 1
365 state = STATE_READ_NAME
366 deps = []
367
368 for line in data_f:
369 line = line.strip()
370 if len(line) and line[0] == '#': # Skip comments
371 continue
372
373 # skip blank lines
374 if len(line) == 0:
375 continue
376
377 if state == STATE_READ_NAME:
378 # Read test name
379 name = line
380 state = STATE_READ_ARGS
381 elif state == STATE_READ_ARGS:
382 # Check dependencies
383 m = re.search('depends_on\:(.*)', line)
384 if m:
385 deps = m.group(1).split(':')
386 else:
387 # Read test vectors
388 parts = escaped_split(line, ':')
389 function = parts[0]
390 args = parts[1:]
391 yield name, function, deps, args
392 deps = []
393 state = STATE_READ_NAME
394
395
396def gen_dep_check(dep_id, dep):
397 """
398 Generate code for the dependency.
399
400 :param dep_id:
401 :param dep:
402 :return:
403 """
404 if dep[0] == '!':
405 noT = '!'
406 dep = dep[1:]
407 else:
408 noT = ''
409 dep_check = '''
410if ( dep_id == {id} )
411{{
412#if {noT}defined({macro})
413 return( DEPENDENCY_SUPPORTED );
414#else
415 return( DEPENDENCY_NOT_SUPPORTED );
416#endif
417}}
418else
419'''.format(noT=noT, macro=dep, id=dep_id)
420
421 return dep_check
422
423
424def gen_expression_check(exp_id, exp):
425 """
426 Generates code for expression check
427
428 :param exp_id:
429 :param exp:
430 :return:
431 """
432 exp_code = '''
433if ( exp_id == {exp_id} )
434{{
435 *out_value = {expression};
436}}
437else
438'''.format(exp_id=exp_id, expression=exp)
439 return exp_code
440
441
442def gen_from_test_data(data_f, out_data_f, func_info):
443 """
444 Generates dependency checks, expression code and intermediate data file from test data file.
445
446 :param data_f:
447 :param out_data_f:
448 :param func_info:
449 :return:
450 """
451 unique_deps = []
452 unique_expressions = []
453 dep_check_code = ''
454 expression_code = ''
455 for test_name, function_name, test_deps, test_args in parse_test_data(data_f):
456 out_data_f.write(test_name + '\n')
457
458 func_id, func_args = func_info['test_' + function_name]
459 if len(test_deps):
460 out_data_f.write('depends_on')
461 for dep in test_deps:
462 if dep not in unique_deps:
463 unique_deps.append(dep)
464 dep_id = unique_deps.index(dep)
465 dep_check_code += gen_dep_check(dep_id, dep)
466 else:
467 dep_id = unique_deps.index(dep)
468 out_data_f.write(':' + str(dep_id))
469 out_data_f.write('\n')
470
471 assert len(test_args) == len(func_args), \
472 "Invalid number of arguments in test %s. See function %s signature." % (test_name, function_name)
473 out_data_f.write(str(func_id))
474 for i in xrange(len(test_args)):
475 typ = func_args[i]
476 val = test_args[i]
477
478 # check if val is a non literal int val
479 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)
480 typ = 'exp'
481 if val not in unique_expressions:
482 unique_expressions.append(val)
483 # exp_id can be derived from len(). But for readability and consistency with case of existing let's
484 # use index().
485 exp_id = unique_expressions.index(val)
486 expression_code += gen_expression_check(exp_id, val)
487 val = exp_id
488 else:
489 val = unique_expressions.index(val)
490 out_data_f.write(':' + typ + ':' + str(val))
491 out_data_f.write('\n\n')
492
493 # void unused params
494 if len(dep_check_code) == 0:
495 dep_check_code = '(void) dep_id;\n'
496 if len(expression_code) == 0:
497 expression_code = '(void) exp_id;\n'
498 expression_code += '(void) out_value;\n'
499
500 return dep_check_code, expression_code
501
502
503def gen_mbed_code(funcs_file, data_file, template_file, platform_file, help_file, suites_dir, c_file, out_data_file):
504 """
505 Generate mbed-os test code.
506
507 :param funcs_file:
508 :param dat a_file:
509 :param template_file:
510 :param platform_file:
511 :param help_file:
512 :param suites_dir:
513 :param c_file:
514 :param out_data_file:
515 :return:
516 """
517 for name, path in [('Functions file', funcs_file),
518 ('Data file', data_file),
519 ('Template file', template_file),
520 ('Platform file', platform_file),
521 ('Help code file', help_file),
522 ('Suites dir', suites_dir)]:
523 if not os.path.exists(path):
524 raise IOError("ERROR: %s [%s] not found!" % (name, path))
525
526 snippets = {'generator_script' : os.path.basename(__file__)}
527
528 # Read helpers
529 with open(help_file, 'r') as help_f, open(platform_file, 'r') as platform_f:
530 snippets['test_common_helper_file'] = help_file
531 snippets['test_common_helpers'] = help_f.read()
532 snippets['test_platform_file'] = platform_file
533 snippets['platform_code'] = platform_f.read().replace('DATA_FILE',
534 out_data_file.replace('\\', '\\\\')) # escape '\'
535
536 # Function code
537 with open(funcs_file, 'r') as funcs_f, open(data_file, 'r') as data_f, open(out_data_file, 'w') as out_data_f:
538 dispatch_code, func_headers, func_code, func_info = parse_functions(funcs_f)
539 snippets['function_headers'] = func_headers
540 snippets['functions_code'] = func_code
541 snippets['dispatch_code'] = dispatch_code
542 dep_check_code, expression_code = gen_from_test_data(data_f, out_data_f, func_info)
543 snippets['dep_check_code'] = dep_check_code
544 snippets['expression_code'] = expression_code
545
546 snippets['test_file'] = c_file
547 snippets['test_main_file'] = template_file
548 snippets['test_case_file'] = funcs_file
549 snippets['test_case_data_file'] = data_file
550 # Read Template
551 # Add functions
552 #
553 with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f:
554 line_no = 1
555 for line in template_f.readlines():
556 snippets['line_no'] = line_no + 1 # Increment as it sets next line number
557 code = line.format(**snippets)
558 c_f.write(code)
559 line_no += 1
560
561
562def check_cmd():
563 """
564 Command line parser.
565
566 :return:
567 """
568 parser = argparse.ArgumentParser(description='Generate code for mbed-os tests.')
569
570 parser.add_argument("-f", "--functions-file",
571 dest="funcs_file",
572 help="Functions file",
573 metavar="FUNCTIONS",
574 required=True)
575
576 parser.add_argument("-d", "--data-file",
577 dest="data_file",
578 help="Data file",
579 metavar="DATA",
580 required=True)
581
582 parser.add_argument("-t", "--template-file",
583 dest="template_file",
584 help="Template file",
585 metavar="TEMPLATE",
586 required=True)
587
588 parser.add_argument("-s", "--suites-dir",
589 dest="suites_dir",
590 help="Suites dir",
591 metavar="SUITES",
592 required=True)
593
594 parser.add_argument("--help-file",
595 dest="help_file",
596 help="Help file",
597 metavar="HELPER",
598 required=True)
599
600 parser.add_argument("-p", "--platform-file",
601 dest="platform_file",
602 help="Platform code file",
603 metavar="PLATFORM_FILE",
604 required=True)
605
606 parser.add_argument("-o", "--out-dir",
607 dest="out_dir",
608 help="Dir where generated code and scripts are copied",
609 metavar="OUT_DIR",
610 required=True)
611
612 args = parser.parse_args()
613
614 data_file_name = os.path.basename(args.data_file)
615 data_name = os.path.splitext(data_file_name)[0]
616
617 out_c_file = os.path.join(args.out_dir, data_name + '.c')
618 out_data_file = os.path.join(args.out_dir, data_file_name)
619
620 out_c_file_dir = os.path.dirname(out_c_file)
621 out_data_file_dir = os.path.dirname(out_data_file)
622 for d in [out_c_file_dir, out_data_file_dir]:
623 if not os.path.exists(d):
624 os.makedirs(d)
625
626 gen_mbed_code(args.funcs_file, args.data_file, args.template_file, args.platform_file,
627 args.help_file, args.suites_dir, out_c_file, out_data_file)
628
629
630if __name__ == "__main__":
631 check_cmd()