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