3 # Copyright (c) 2017 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 Script extracts interested data (name, VAT command history or table from Show
18 Runtime command) from robot framework output file (output.xml) and prints in
19 specified format (wiki, html, rst) to defined output file.
31 run_robot_teardown_data.py -i "output.xml" -o "tests.rst" -d "VAT_H" -f "rst"
34 The example reads the VAT command history data from "output.xml", writes
35 the output to "tests.rst" in rst format. It will start on the 3rd level of xml
36 structure and the generated document hierarchy will start on the 2nd level.
40 run_robot_teardown_data.py -i "output.xml" -o "tests.rst" -f "rst" -d "SH_RUN"
43 The example reads the data from "output.xml", writes the output to "tests.rst"
44 in rst format. It will start on the 1st level of xml structure and the generated
45 document hierarchy will start on the 1st level (default values).
46 Only the test suites which match the given regular expression are processed.
55 from robot.api import ExecutionResult, ResultVisitor
58 class ExecutionChecker(ResultVisitor):
59 """Class to traverse through the test suite structure.
61 The functionality implemented in this class generates a json file. Its
66 "level": "Level of the suite, type: str",
67 "title": "Title of the suite, type: str",
68 "doc": "Documentation of the suite, type: str",
70 ["TC name", "VAT history or show runtime"],
71 ["TC name", "VAT history or show runtime"],
72 ... other test cases ...
73 ["Name","VAT command history or VPP operational data"]
76 ... other test suites ...
79 .. note:: The header of the table with TCs is at the end of the table.
82 def __init__(self, args):
83 self.formatting = args.formatting
85 if self.data == "VAT_H":
86 self.lookup_kw = "Show Vat History On All Duts"
87 self.column_name = "VAT command history"
88 elif self.data == "SH_RUN":
89 self.lookup_kw = "Vpp Show Runtime"
90 self.column_name = "VPP operational data"
92 raise ValueError("{0} look-up not implemented.".format(self.data))
94 self.lookup_msg_nr = 0
96 def visit_suite(self, suite):
97 """Implements traversing through the suite and its direct children.
99 :param suite: Suite to process.
104 if self.start_suite(suite) is not False:
106 sys.stdout.write(',"tests":[')
108 sys.stdout.write('},')
110 suite.suites.visit(self)
111 suite.tests.visit(self)
114 hdr = '["Name","' + self.column_name + '"]'
115 sys.stdout.write(hdr + ']},')
117 self.end_suite(suite)
119 def start_suite(self, suite):
120 """Called when suite starts.
122 :param suite: Suite to process.
127 level = len(suite.longname.split("."))
128 sys.stdout.write('{')
129 sys.stdout.write('"level":"' + str(level) + '",')
130 sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",')
131 sys.stdout.write('"doc":"' + suite.doc.replace('"', "'").
132 replace('\n', ' ').replace('\r', '').
133 replace('*[', ' |br| *[') + '"')
135 def end_suite(self, suite):
136 """Called when suite ends.
138 :param suite: Suite to process.
144 def visit_test(self, test):
145 """Implements traversing through the test.
147 :param test: Test to process.
151 if self.start_test(test) is not False:
152 test.keywords.visit(self)
155 def start_test(self, test):
156 """Called when test starts.
158 :param test: Test to process.
163 name = test.name.replace('"', "'")
164 sys.stdout.write('["' + name + '","')
166 def end_test(self, test):
167 """Called when test ends.
169 :param test: Test to process.
173 sys.stdout.write('"],')
175 def visit_keyword(self, kw):
176 """Implements traversing through the keyword and its child keywords.
178 :param kw: Keyword to process.
182 if self.start_keyword(kw) is not False:
185 def start_keyword(self, kw):
186 """Called when keyword starts. Default implementation does nothing.
188 :param kw: Keyword to process.
193 if kw.type == "teardown":
194 self.lookup_kw_nr = 0
195 self.visit_teardown_kw(kw)
196 except AttributeError:
199 def end_keyword(self, kw):
200 """Called when keyword ends. Default implementation does nothing.
202 :param kw: Keyword to process.
208 def visit_teardown_kw(self, kw):
209 """Implements traversing through the teardown keyword and its child
212 :param kw: Keyword to process.
216 for keyword in kw.keywords:
217 if self.start_teardown_kw(keyword) is not False:
218 self.visit_teardown_kw(keyword)
219 self.end_teardown_kw(keyword)
221 def start_teardown_kw(self, kw):
222 """Called when teardown keyword starts. Default implementation does
225 :param kw: Keyword to process.
229 if kw.name.count(self.lookup_kw):
230 self.lookup_kw_nr += 1
231 self.lookup_msg_nr = 0
232 kw.messages.visit(self)
234 def end_teardown_kw(self, kw):
235 """Called when keyword ends. Default implementation does nothing.
237 :param kw: Keyword to process.
243 def visit_message(self, msg):
244 """Implements visiting the message.
246 :param msg: Message to process.
250 if self.start_message(msg) is not False:
251 self.end_message(msg)
253 def start_message(self, msg):
254 """Called when message starts. Default implementation does nothing.
256 :param msg: Message to process.
260 if self.data == "VAT_H":
261 self.vat_history(msg)
262 elif self.data == "SH_RUN":
265 def end_message(self, msg):
266 """Called when message ends. Default implementation does nothing.
268 :param msg: Message to process.
274 def vat_history(self, msg):
275 """Called when extraction of VAT command history is required.
277 :param msg: Message to process.
281 if msg.message.count("VAT command history:"):
282 self.lookup_msg_nr += 1
283 text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
284 "VAT command history:", "", msg.message, count=1).\
285 replace('\n', ' |br| ').replace('\r', '').replace('"', "'")
286 sys.stdout.write("*DUT" + str(self.lookup_msg_nr) + ":*" + text)
288 def show_run(self, msg):
289 """Called when extraction of VPP operational data (output of CLI command
290 Show Runtime) is required.
292 :param msg: Message to process.
296 if msg.message.count("vat# Thread "):
297 self.lookup_msg_nr += 1
298 text = msg.message.replace("vat# ", "").\
299 replace("return STDOUT ", "").replace('\n', ' |br| ').\
300 replace('\r', '').replace('"', "'")
301 if self.lookup_msg_nr == 1:
302 sys.stdout.write("*DUT" + str(self.lookup_kw_nr) +
306 def do_html(data, args):
307 """Generation of a html file from json data.
309 :param data: List of suites from json file.
310 :param args: Parsed arguments.
311 :type data: list of dict
312 :type args: ArgumentParser
316 shift = int(args.level)
317 start = int(args.start)
319 output = open(args.output, 'w')
321 output.write('<html>')
323 if int(item['level']) < start:
325 level = str(int(item['level']) - start + shift)
326 output.write('<h' + level + '>' + item['title'].lower() +
328 output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
329 0, flags=re.MULTILINE).
330 replace(' |br| ', '<br>') + '</p>')
332 output.write(gen_html_table(item['tests']))
335 output.write('</html>')
339 def gen_html_table(data):
340 """Generates a table with TCs' names and VAT command histories / VPP
341 operational data in html format. There is no css used.
343 :param data: Json data representing a table with TCs.
345 :returns: Table with TCs' names and VAT command histories / VPP operational
350 table = '<table width=100% border=1><tr>'
351 table += '<th width=30%>' + data[-1][0] + '</th>'
352 table += '<th width=70%>' + data[-1][1] + '</th></tr>'
354 for item in data[0:-1]:
357 table += '<td>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", element,
358 0, flags=re.MULTILINE).\
359 replace(' |br| ', '<br>') + '</td>'
360 table += '</tr></table>'
365 def do_rst(data, args):
366 """Generation of a rst file from json data.
368 :param data: List of suites from json file.
369 :param args: Parsed arguments.
370 :type data: list of dict
371 :type args: ArgumentParser
375 hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
376 shift = int(args.level)
377 start = int(args.start)
379 output = open(args.output, 'w')
380 output.write('\n.. |br| raw:: html\n\n <br />\n\n')
383 output.write(args.title + '\n' +
385 len(args.title) + '\n\n')
388 if int(item['level']) < start:
390 if 'ndrchk' in item['title'].lower():
392 output.write(item['title'].lower() + '\n' +
393 hdrs[int(item['level']) - start + shift] *
394 len(item['title']) + '\n\n')
395 output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
398 output.write(gen_rst_table(item['tests']) + '\n\n')
404 def gen_rst_table(data):
405 """Generates a table with TCs' names and VAT command histories / VPP
406 operational data in rst format.
408 :param data: Json data representing a table with TCs.
410 :returns: Table with TCs' names and VAT command histories / VPP operational
416 # max size of each column
417 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
420 vert_separator = ' | '
424 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
425 for i in range(len(lengths))])
426 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
428 # determine top/bottom borders
429 to_separator = string.maketrans('| ', '+-')
430 start_of_line = start_of_line.translate(to_separator)
431 vert_separator = vert_separator.translate(to_separator)
432 end_of_line = end_of_line.translate(to_separator)
433 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
434 join([x * line_marker for x in lengths]),
436 # determine header separator
437 th_separator_tr = string.maketrans('-', '=')
438 start_of_line = start_of_line.translate(th_separator_tr)
439 line_marker = line_marker.translate(th_separator_tr)
440 vertical_separator = vert_separator.translate(th_separator_tr)
441 end_of_line = end_of_line.translate(th_separator_tr)
442 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
443 join([x * line_marker for x in lengths]),
446 table.append(separator)
449 table.append(template.format(*titles))
450 table.append(th_separator)
451 # generate table rows
452 for item in data[0:-2]:
453 table.append(template.format(item[0], item[1].replace('*', '**')))
454 table.append(separator)
455 table.append(template.format(data[-2][0], data[-2][1].replace('*', '**')))
456 table.append(separator)
457 return '\n'.join(table)
460 def do_md(data, args):
461 """Generation of a rst file from json data.
463 :param data: List of suites from json file.
464 :param args: Parsed arguments.
465 :type data: list of dict
466 :type args: ArgumentParser
469 raise NotImplementedError("Export to 'md' format is not implemented.")
472 def do_wiki(data, args):
473 """Generation of a wiki page from json data.
475 :param data: List of suites from json file.
476 :param args: Parsed arguments.
477 :type data: list of dict
478 :type args: ArgumentParser
482 shift = int(args.level)
483 start = int(args.start)
485 output = open(args.output, 'w')
488 if int(item['level']) < start:
490 if 'ndrchk' in item['title'].lower():
492 mark = "=" * (int(item['level']) - start + shift) + ' '
493 output.write(mark + item['title'].lower() + mark + '\n')
495 output.write(gen_wiki_table(item['tests'], mark) +
502 def gen_wiki_table(data, mark):
503 """Generates a table with TCs' names and VAT command histories / VPP
504 operational data in wiki format.
506 :param data: Json data representing a table with TCs.
508 :returns: Table with TCs' names and VAT command histories / VPP operational
513 table = '{| class="wikitable"\n'
515 mark = mark[0:-2] + "= "
516 for item in data[-1]:
517 header += '!{}\n'.format(item)
519 for item in data[0:-1]:
520 msg = item[1].replace('*', mark).replace(' |br| ', '\n\n')
521 table += '|-\n|{}\n|{}\n'.format(item[0], msg)
527 def process_robot_file(args):
528 """Process data from robot output.xml file and generate defined file type.
530 :param args: Parsed arguments.
531 :type args: ArgumentParser
535 old_sys_stdout = sys.stdout
536 sys.stdout = open(args.output + '.json', 'w')
538 result = ExecutionResult(args.input)
539 checker = ExecutionChecker(args)
541 sys.stdout.write('[')
542 result.visit(checker)
543 sys.stdout.write('{}]')
545 sys.stdout = old_sys_stdout
547 with open(args.output + '.json', 'r') as json_file:
548 data = json.load(json_file)
553 regex = re.compile(args.regex)
555 if re.search(regex, item['title'].lower()):
560 if args.formatting == 'rst':
561 do_rst(results, args)
562 elif args.formatting == 'wiki':
563 do_wiki(results, args)
564 elif args.formatting == 'html':
565 do_html(results, args)
566 elif args.formatting == 'md':
571 """Parse arguments from cmd line.
573 :return: Parsed arguments.
574 :rtype ArgumentParser
577 parser = argparse.ArgumentParser(description=__doc__,
578 formatter_class=argparse.
579 RawDescriptionHelpFormatter)
580 parser.add_argument("-i", "--input",
582 type=argparse.FileType('r'),
583 help="Robot XML log file")
584 parser.add_argument("-o", "--output",
588 parser.add_argument("-d", "--data",
591 help="Required data: VAT_H (VAT history), SH_RUN "
592 "(show runtime output)")
593 parser.add_argument("-f", "--formatting",
595 choices=['html', 'wiki', 'rst', 'md'],
596 help="Output file format")
597 parser.add_argument("-s", "--start",
600 help="The first level to be taken from xml file")
601 parser.add_argument("-l", "--level",
604 help="The level of the first chapter in generated file")
605 parser.add_argument("-r", "--regex",
608 help="Regular expression used to select test suites. "
609 "If None, all test suites are selected.")
610 parser.add_argument("-t", "--title",
613 help="Title of the output.")
615 return parser.parse_args()
618 if __name__ == "__main__":
619 sys.exit(process_robot_file(parse_args()))