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 self.tagin = " |prein| "
86 self.tagout = " |preout| "
87 if self.data == "VAT_H":
88 self.lookup_kw = "Show Vat History On All Duts"
89 self.column_name = "VPP API Test (VAT) Commands History - " \
90 "Commands Used Per Test Case"
91 elif self.data == "SH_RUN":
92 self.lookup_kw = "Vpp Show Runtime"
93 self.column_name = "VPP Operational Data - Outputs of " \
94 "'show runtime' at NDR packet rate"
96 raise ValueError("{0} look-up not implemented.".format(self.data))
98 self.lookup_msg_nr = 0
100 def visit_suite(self, suite):
101 """Implements traversing through the suite and its direct children.
103 :param suite: Suite to process.
108 if self.start_suite(suite) is not False:
110 sys.stdout.write(',"tests":[')
112 sys.stdout.write('},')
114 suite.suites.visit(self)
115 suite.tests.visit(self)
118 hdr = '["Name","' + self.column_name + '"]'
119 sys.stdout.write(hdr + ']},')
121 self.end_suite(suite)
123 def start_suite(self, suite):
124 """Called when suite starts.
126 :param suite: Suite to process.
131 level = len(suite.longname.split("."))
132 sys.stdout.write('{')
133 sys.stdout.write('"level":"' + str(level) + '",')
134 sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",')
135 sys.stdout.write('"doc":"' + suite.doc.replace('"', "'").
136 replace('\n', ' ').replace('\r', '').
137 replace('*[', ' |br| *[') + '"')
139 def end_suite(self, suite):
140 """Called when suite ends.
142 :param suite: Suite to process.
148 def visit_test(self, test):
149 """Implements traversing through the test.
151 :param test: Test to process.
155 if self.start_test(test) is not False:
156 test.keywords.visit(self)
159 def start_test(self, test):
160 """Called when test starts.
162 :param test: Test to process.
167 name = test.name.replace('"', "'")
168 sys.stdout.write('["' + name + '","' + self.tagin)
170 def end_test(self, test):
171 """Called when test ends.
173 :param test: Test to process.
177 sys.stdout.write(self.tagout + '"],')
179 def visit_keyword(self, kw):
180 """Implements traversing through the keyword and its child keywords.
182 :param kw: Keyword to process.
186 if self.start_keyword(kw) is not False:
189 def start_keyword(self, kw):
190 """Called when keyword starts. Default implementation does nothing.
192 :param kw: Keyword to process.
197 if kw.type == "teardown":
198 self.lookup_kw_nr = 0
199 self.visit_teardown_kw(kw)
200 except AttributeError:
203 def end_keyword(self, kw):
204 """Called when keyword ends. Default implementation does nothing.
206 :param kw: Keyword to process.
212 def visit_teardown_kw(self, kw):
213 """Implements traversing through the teardown keyword and its child
216 :param kw: Keyword to process.
220 for keyword in kw.keywords:
221 if self.start_teardown_kw(keyword) is not False:
222 self.visit_teardown_kw(keyword)
223 self.end_teardown_kw(keyword)
225 def start_teardown_kw(self, kw):
226 """Called when teardown keyword starts. Default implementation does
229 :param kw: Keyword to process.
233 if kw.name.count(self.lookup_kw):
234 self.lookup_kw_nr += 1
235 self.lookup_msg_nr = 0
236 kw.messages.visit(self)
238 def end_teardown_kw(self, kw):
239 """Called when keyword ends. Default implementation does nothing.
241 :param kw: Keyword to process.
247 def visit_message(self, msg):
248 """Implements visiting the message.
250 :param msg: Message to process.
254 if self.start_message(msg) is not False:
255 self.end_message(msg)
257 def start_message(self, msg):
258 """Called when message starts. Default implementation does nothing.
260 :param msg: Message to process.
264 if self.data == "VAT_H":
265 self.vat_history(msg)
266 elif self.data == "SH_RUN":
269 def end_message(self, msg):
270 """Called when message ends. Default implementation does nothing.
272 :param msg: Message to process.
278 def vat_history(self, msg):
279 """Called when extraction of VAT command history is required.
281 :param msg: Message to process.
285 if msg.message.count("VAT command history:"):
286 self.lookup_msg_nr += 1
287 text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
288 "VAT command history:", "", msg.message, count=1).\
289 replace('\n', ' |br| ').replace('\r', '').replace('"', "'")
290 if self.lookup_msg_nr > 1:
291 sys.stdout.write(" |br| ")
292 sys.stdout.write("*DUT" + str(self.lookup_msg_nr) + ":*" + text)
294 def show_run(self, msg):
295 """Called when extraction of VPP operational data (output of CLI command
296 Show Runtime) is required.
298 :param msg: Message to process.
302 if msg.message.count("vat# Thread "):
303 self.lookup_msg_nr += 1
304 text = msg.message.replace("vat# ", "").\
305 replace("return STDOUT ", "").replace('\n', ' |br| ').\
306 replace('\r', '').replace('"', "'")
307 if self.lookup_msg_nr == 1:
308 sys.stdout.write("*DUT" + str(self.lookup_kw_nr) +
312 def do_html(data, args):
313 """Generation of a html file from json data.
315 :param data: List of suites from json file.
316 :param args: Parsed arguments.
317 :type data: list of dict
318 :type args: ArgumentParser
322 shift = int(args.level)
323 start = int(args.start)
325 output = open(args.output, 'w')
327 output.write('<html>')
329 if int(item['level']) < start:
331 level = str(int(item['level']) - start + shift)
332 output.write('<h' + level + '>' + item['title'].lower() +
334 output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
335 0, flags=re.MULTILINE).
336 replace(' |br| ', '<br>') + '</p>')
338 output.write(gen_html_table(item['tests']))
341 output.write('</html>')
345 def gen_html_table(data):
346 """Generates a table with TCs' names and VAT command histories / VPP
347 operational data in html format. There is no css used.
349 :param data: Json data representing a table with TCs.
351 :returns: Table with TCs' names and VAT command histories / VPP operational
356 table = '<table width=100% border=1><tr>'
357 table += '<th width=30%>' + data[-1][0] + '</th>'
358 table += '<th width=70%>' + data[-1][1] + '</th></tr>'
360 for item in data[0:-1]:
363 table += '<td>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", element,
364 0, flags=re.MULTILINE).\
365 replace(' |br| ', '<br>').replace(' |prein| ', '<pre>').\
366 replace(' |preout| ', '</pre>') + '</td>'
367 table += '</tr></table>'
372 def do_rst(data, args):
373 """Generation of a rst file from json data.
375 :param data: List of suites from json file.
376 :param args: Parsed arguments.
377 :type data: list of dict
378 :type args: ArgumentParser
382 hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
383 shift = int(args.level)
384 start = int(args.start)
386 output = open(args.output, 'w')
387 output.write('\n.. |br| raw:: html\n\n <br />\n\n')
388 output.write('\n.. |prein| raw:: html\n\n <pre>\n\n')
389 output.write('\n.. |preout| raw:: html\n\n </pre>\n\n')
392 output.write(args.title + '\n' +
394 len(args.title) + '\n\n')
397 if int(item['level']) < start:
399 if 'ndrchk' in item['title'].lower():
401 output.write(item['title'].lower() + '\n' +
402 hdrs[int(item['level']) - start + shift] *
403 len(item['title']) + '\n\n')
404 output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
408 for test in item['tests']:
410 test_data.append(test[0])
411 test_data.append(test[1].replace('*', '**'))
412 test_set.append(test_data)
413 output.write(gen_rst_table(test_set) + '\n\n')
419 def gen_rst_table(data):
420 """Generates a table with TCs' names and VAT command histories / VPP
421 operational data in rst format.
423 :param data: Json data representing a table with TCs.
425 :returns: Table with TCs' names and VAT command histories / VPP operational
431 # max size of each column
432 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
435 vert_separator = ' | '
439 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
440 for i in range(len(lengths))])
441 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
443 # determine top/bottom borders
444 to_separator = string.maketrans('| ', '+-')
445 start_of_line = start_of_line.translate(to_separator)
446 vert_separator = vert_separator.translate(to_separator)
447 end_of_line = end_of_line.translate(to_separator)
448 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
449 join([x * line_marker for x in lengths]),
451 # determine header separator
452 th_separator_tr = string.maketrans('-', '=')
453 start_of_line = start_of_line.translate(th_separator_tr)
454 line_marker = line_marker.translate(th_separator_tr)
455 vertical_separator = vert_separator.translate(th_separator_tr)
456 end_of_line = end_of_line.translate(th_separator_tr)
457 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
458 join([x * line_marker for x in lengths]),
461 table.append(separator)
464 table.append(template.format(*titles))
465 table.append(th_separator)
466 # generate table rows
467 for item in data[0:-2]:
468 table.append(template.format(item[0], item[1]))
469 table.append(separator)
470 table.append(template.format(data[-2][0], data[-2][1]))
471 table.append(separator)
472 return '\n'.join(table)
475 def do_md(data, args):
476 """Generation of a rst file from json data.
478 :param data: List of suites from json file.
479 :param args: Parsed arguments.
480 :type data: list of dict
481 :type args: ArgumentParser
484 raise NotImplementedError("Export to 'md' format is not implemented.")
487 def do_wiki(data, args):
488 """Generation of a wiki page from json data.
490 :param data: List of suites from json file.
491 :param args: Parsed arguments.
492 :type data: list of dict
493 :type args: ArgumentParser
497 shift = int(args.level)
498 start = int(args.start)
500 output = open(args.output, 'w')
503 if int(item['level']) < start:
505 if 'ndrchk' in item['title'].lower():
507 mark = "=" * (int(item['level']) - start + shift) + ' '
508 output.write(mark + item['title'].lower() + mark + '\n')
510 output.write(gen_wiki_table(item['tests'], mark) +
517 def gen_wiki_table(data, mark):
518 """Generates a table with TCs' names and VAT command histories / VPP
519 operational data in wiki format.
521 :param data: Json data representing a table with TCs.
523 :returns: Table with TCs' names and VAT command histories / VPP operational
528 table = '{| class="wikitable"\n'
530 mark = mark[0:-2] + "= "
531 for item in data[-1]:
532 header += '!{}\n'.format(item)
534 for item in data[0:-1]:
535 msg = item[1].replace('*', mark).replace(' |br| ', '\n\n').\
536 replace(' |prein| ', '<pre>').replace(' |preout| ', '</pre>')
537 table += '|-\n|{}\n|{}\n'.format(item[0], msg)
543 def process_robot_file(args):
544 """Process data from robot output.xml file and generate defined file type.
546 :param args: Parsed arguments.
547 :type args: ArgumentParser
551 old_sys_stdout = sys.stdout
552 sys.stdout = open(args.output + '.json', 'w')
554 result = ExecutionResult(args.input)
555 checker = ExecutionChecker(args)
557 sys.stdout.write('[')
558 result.visit(checker)
559 sys.stdout.write('{}]')
561 sys.stdout = old_sys_stdout
563 with open(args.output + '.json', 'r') as json_file:
564 data = json.load(json_file)
569 regex = re.compile(args.regex)
571 if re.search(regex, item['title'].lower()):
576 if args.formatting == 'rst':
577 do_rst(results, args)
578 elif args.formatting == 'wiki':
579 do_wiki(results, args)
580 elif args.formatting == 'html':
581 do_html(results, args)
582 elif args.formatting == 'md':
587 """Parse arguments from cmd line.
589 :return: Parsed arguments.
590 :rtype ArgumentParser
593 parser = argparse.ArgumentParser(description=__doc__,
594 formatter_class=argparse.
595 RawDescriptionHelpFormatter)
596 parser.add_argument("-i", "--input",
598 type=argparse.FileType('r'),
599 help="Robot XML log file")
600 parser.add_argument("-o", "--output",
604 parser.add_argument("-d", "--data",
607 help="Required data: VAT_H (VAT history), SH_RUN "
608 "(show runtime output)")
609 parser.add_argument("-f", "--formatting",
611 choices=['html', 'wiki', 'rst', 'md'],
612 help="Output file format")
613 parser.add_argument("-s", "--start",
616 help="The first level to be taken from xml file")
617 parser.add_argument("-l", "--level",
620 help="The level of the first chapter in generated file")
621 parser.add_argument("-r", "--regex",
624 help="Regular expression used to select test suites. "
625 "If None, all test suites are selected.")
626 parser.add_argument("-t", "--title",
629 help="Title of the output.")
631 return parser.parse_args()
634 if __name__ == "__main__":
635 sys.exit(process_robot_file(parse_args()))