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 -') +
399 for test in item['tests']:
401 test_data.append(test[0])
402 test_data.append(test[1].replace('*', '**'))
403 test_set.append(test_data)
404 output.write(gen_rst_table(test_set) + '\n\n')
410 def gen_rst_table(data):
411 """Generates a table with TCs' names and VAT command histories / VPP
412 operational data in rst format.
414 :param data: Json data representing a table with TCs.
416 :returns: Table with TCs' names and VAT command histories / VPP operational
422 # max size of each column
423 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
426 vert_separator = ' | '
430 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
431 for i in range(len(lengths))])
432 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
434 # determine top/bottom borders
435 to_separator = string.maketrans('| ', '+-')
436 start_of_line = start_of_line.translate(to_separator)
437 vert_separator = vert_separator.translate(to_separator)
438 end_of_line = end_of_line.translate(to_separator)
439 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
440 join([x * line_marker for x in lengths]),
442 # determine header separator
443 th_separator_tr = string.maketrans('-', '=')
444 start_of_line = start_of_line.translate(th_separator_tr)
445 line_marker = line_marker.translate(th_separator_tr)
446 vertical_separator = vert_separator.translate(th_separator_tr)
447 end_of_line = end_of_line.translate(th_separator_tr)
448 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
449 join([x * line_marker for x in lengths]),
452 table.append(separator)
455 table.append(template.format(*titles))
456 table.append(th_separator)
457 # generate table rows
458 for item in data[0:-2]:
459 table.append(template.format(item[0], item[1]))
460 table.append(separator)
461 table.append(template.format(data[-2][0], data[-2][1]))
462 table.append(separator)
463 return '\n'.join(table)
466 def do_md(data, args):
467 """Generation of a rst file from json data.
469 :param data: List of suites from json file.
470 :param args: Parsed arguments.
471 :type data: list of dict
472 :type args: ArgumentParser
475 raise NotImplementedError("Export to 'md' format is not implemented.")
478 def do_wiki(data, args):
479 """Generation of a wiki page from json data.
481 :param data: List of suites from json file.
482 :param args: Parsed arguments.
483 :type data: list of dict
484 :type args: ArgumentParser
488 shift = int(args.level)
489 start = int(args.start)
491 output = open(args.output, 'w')
494 if int(item['level']) < start:
496 if 'ndrchk' in item['title'].lower():
498 mark = "=" * (int(item['level']) - start + shift) + ' '
499 output.write(mark + item['title'].lower() + mark + '\n')
501 output.write(gen_wiki_table(item['tests'], mark) +
508 def gen_wiki_table(data, mark):
509 """Generates a table with TCs' names and VAT command histories / VPP
510 operational data in wiki format.
512 :param data: Json data representing a table with TCs.
514 :returns: Table with TCs' names and VAT command histories / VPP operational
519 table = '{| class="wikitable"\n'
521 mark = mark[0:-2] + "= "
522 for item in data[-1]:
523 header += '!{}\n'.format(item)
525 for item in data[0:-1]:
526 msg = item[1].replace('*', mark).replace(' |br| ', '\n\n')
527 table += '|-\n|{}\n|{}\n'.format(item[0], msg)
533 def process_robot_file(args):
534 """Process data from robot output.xml file and generate defined file type.
536 :param args: Parsed arguments.
537 :type args: ArgumentParser
541 old_sys_stdout = sys.stdout
542 sys.stdout = open(args.output + '.json', 'w')
544 result = ExecutionResult(args.input)
545 checker = ExecutionChecker(args)
547 sys.stdout.write('[')
548 result.visit(checker)
549 sys.stdout.write('{}]')
551 sys.stdout = old_sys_stdout
553 with open(args.output + '.json', 'r') as json_file:
554 data = json.load(json_file)
559 regex = re.compile(args.regex)
561 if re.search(regex, item['title'].lower()):
566 if args.formatting == 'rst':
567 do_rst(results, args)
568 elif args.formatting == 'wiki':
569 do_wiki(results, args)
570 elif args.formatting == 'html':
571 do_html(results, args)
572 elif args.formatting == 'md':
577 """Parse arguments from cmd line.
579 :return: Parsed arguments.
580 :rtype ArgumentParser
583 parser = argparse.ArgumentParser(description=__doc__,
584 formatter_class=argparse.
585 RawDescriptionHelpFormatter)
586 parser.add_argument("-i", "--input",
588 type=argparse.FileType('r'),
589 help="Robot XML log file")
590 parser.add_argument("-o", "--output",
594 parser.add_argument("-d", "--data",
597 help="Required data: VAT_H (VAT history), SH_RUN "
598 "(show runtime output)")
599 parser.add_argument("-f", "--formatting",
601 choices=['html', 'wiki', 'rst', 'md'],
602 help="Output file format")
603 parser.add_argument("-s", "--start",
606 help="The first level to be taken from xml file")
607 parser.add_argument("-l", "--level",
610 help="The level of the first chapter in generated file")
611 parser.add_argument("-r", "--regex",
614 help="Regular expression used to select test suites. "
615 "If None, all test suites are selected.")
616 parser.add_argument("-t", "--title",
619 help="Title of the output.")
621 return parser.parse_args()
624 if __name__ == "__main__":
625 sys.exit(process_robot_file(parse_args()))