From 964635aef2a6a209565b286cae61acb80a42de3e Mon Sep 17 00:00:00 2001 From: Tibor Frank Date: Tue, 24 Jan 2017 13:04:23 +0100 Subject: CSIT-511: Add script to generate report with TC results Change-Id: Iee1c3310e445487bb216c4e9c6a3bc7ee7879788 Signed-off-by: Tibor Frank (cherry picked from commit 4c7c2be2054c023037ec451a9d040e2b10fe8f0d) --- resources/tools/robot_output_parser_publish.py | 386 ++++++++++++++++++------- 1 file changed, 286 insertions(+), 100 deletions(-) diff --git a/resources/tools/robot_output_parser_publish.py b/resources/tools/robot_output_parser_publish.py index caa5bd5d2b..e2bcfa2f0d 100755 --- a/resources/tools/robot_output_parser_publish.py +++ b/resources/tools/robot_output_parser_publish.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright (c) 2016 Cisco and/or its affiliates. +# Copyright (c) 2017 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -13,19 +13,60 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Script extracts intersted data (name, documentation, message, status) from -robot framework output file (output.xml) and print in specified format (wiki, -html) to stdout or, if specified by parameter, redirect to file.""" +""" +Script extracts interested data (name, documentation, message, status) from +robot framework output file (output.xml) and prints in specified format (wiki, +html, rst) to defined output file. + +Supported formats: + - html + - rst + +:TODO: + - wiki + - md + +:Example: + +robot_output_parser_publish.py -i output.xml" -o "tests.rst" -f "rst" -s 3 -l 2 + +The example reads the data from "output.xml", writes the output to "tests.rst" +in rst format. It will start on the 3rd level of xml structure and the generated +document hierarchy will start on the 2nd level. +""" import argparse import re import sys +import json +import string from robot.api import ExecutionResult, ResultVisitor class ExecutionChecker(ResultVisitor): - """Abstract class to traverse through the test suite structure.""" + """Class to traverse through the test suite structure. + + The functionality implemented in this class generates a json file. Its + structure is: + + [ + { + "level": "Level of the suite, type: str", + "title": "Title of the suite, type: str", + "doc": "Documentation of the suite, type: str", + "table": [ + ["TC name", "TC doc", "message or status"], + ["TC name", "TC doc", "message or status"], + ... other test cases ... + ["Name", "Documentation", "Message or Status"] + ] + }, + ... other test suites ... + ] + + .. note:: The header of the table with TCs is at the and of the table. + """ def __init__(self, args): self.formatting = args.formatting @@ -35,39 +76,24 @@ class ExecutionChecker(ResultVisitor): :param suite: Suite to process. :type suite: Suite - :return: Nothing. + :returns: Nothing. """ if self.start_suite(suite) is not False: if suite.tests: - if self.formatting == 'html': - sys.stdout.write(''+'\n') - sys.stdout.write(''+'\n') - sys.stdout.write(''+'\n') - if "ndrdisc" in suite.longname: - sys.stdout.write(''+'\n') - sys.stdout.write(''+'\n') - sys.stdout.write('') - elif self.formatting == 'wiki': - sys.stdout.write('{| class="wikitable"'+'\n') - if "ndrdisc" in suite.longname: - header = '!Name!!Documentation!!Message!!Status' - else: - header = '!Name!!Documentation!!Status' - sys.stdout.write(header+'\n') - else: - pass + sys.stdout.write(',"tests":[') + else: + sys.stdout.write('},') suite.suites.visit(self) suite.tests.visit(self) if suite.tests: - if self.formatting == 'html': - sys.stdout.write('
NameDocumentationMessageStatus
'+'\n') - elif self.formatting == 'wiki': - sys.stdout.write('|}'+'\n') + if "ndrdisc" in suite.longname.lower(): + hdr = '["Name","Documentation","Message"]' else: - pass + hdr = '["Name","Documentation","Status"]' + sys.stdout.write(hdr + ']},') self.end_suite(suite) @@ -76,35 +102,24 @@ class ExecutionChecker(ResultVisitor): :param suite: Suite to process. :type suite: Suite - :return: Nothing. + :returns: Nothing. """ level = len(suite.longname.split(".")) - - if self.formatting == 'html': - mark_l = '' - mark_r = '' - sys.stdout.write(mark_l+suite.name+mark_r+'\n') - sys.stdout.write('

'+re.sub(r"(\*)(.*?)(\*)", r"\2",\ - suite.doc, 0, flags=re.MULTILINE).replace(\ - '[', '
[')+'

\n') - - elif self.formatting == 'wiki': - mark = "=" * (level+2) - sys.stdout.write(mark+suite.name+mark+'\n') - sys.stdout.write(re.sub(r"(\*)(.*?)(\*)", r"\n*'''\2'''",\ - suite.doc.replace('\n', ' '), 0, flags=re.MULTILINE)+'\n') - else: - pass + sys.stdout.write('{') + sys.stdout.write('"level":"' + str(level) + '",') + sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",') + sys.stdout.write('"doc":"' + suite.doc.replace('"', "'"). + replace('\n', ' ').replace('\r', ''). + replace('*[', ' |br| *[') + '"') def end_suite(self, suite): """Called when suite ends. :param suite: Suite to process. :type suite: Suite - :return: Nothing. + :returns: Nothing. """ - pass def visit_test(self, test): @@ -112,9 +127,8 @@ class ExecutionChecker(ResultVisitor): :param test: Test to process. :type test: Test - :return: Nothing. + :returns: Nothing. """ - if self.start_test(test) is not False: self.end_test(test) @@ -123,65 +137,235 @@ class ExecutionChecker(ResultVisitor): :param test: Test to process. :type test: Test - :return: Nothing. + :returns: Nothing. """ - if self.formatting == 'html': - sys.stdout.write(''+'\n') - sys.stdout.write(''+test.name+''+'\n') - sys.stdout.write(''+test.doc+''+'\n') - if any("NDRPDRDISC" in tag for tag in test.tags): - sys.stdout.write(''+test.message+''+'\n') - sys.stdout.write(''+test.status+''+'\n') - elif self.formatting == 'wiki': - sys.stdout.write('|-'+'\n') - sys.stdout.write('|'+test.name+'\n') - sys.stdout.write('|'+test.doc.replace('\n', ' ').replace('\r',\ - '')+'\n') - if any("NDRPDRDISC" in tag for tag in test.tags): - sys.stdout.write('|'+test.message+'\n') - sys.stdout.write('|'+test.status+'\n') + name = test.name.replace('"', "'") + doc = test.doc.replace('"', "'").replace('\n', ' ').replace('\r', '').\ + replace('[', ' |br| [') + if any("NDRPDRDISC" in tag for tag in test.tags): + msg = test.message.replace('\n', ' |br| ').replace('\r', ''). \ + replace('"', "'") + + sys.stdout.write('["' + name + '","' + doc + '","' + msg + '"]') else: - pass + sys.stdout.write( + '["' + name + '","' + doc + '","' + test.status + '"]') def end_test(self, test): """Called when test ends. :param test: Test to process. :type test: Test - :return: Nothing. + :returns: Nothing. """ + sys.stdout.write(',') - if self.formatting == 'html': - sys.stdout.write(''+'\n') - elif self.formatting == 'wiki': - pass - else: - pass + +def do_html(data, args): + """Generation of a html file from json data. + + :param data: List of suites from json file. + :param args: Parsed arguments. + :type data: list of dict + :type args: ArgumentParser + :returns: Nothing. + """ + + shift = int(args.level) + start = int(args.start) + + output = open(args.output, 'w') + + output.write('') + for item in data: + if int(item['level']) < start: + continue + level = str(int(item['level']) - start + shift) + output.write('' + item['title'].lower() + + '') + output.write('

' + re.sub(r"(\*)(.*?)(\*)", r"\2", item['doc'], + 0, flags=re.MULTILINE). + replace(' |br| ', '
') + '

') + try: + output.write(gen_html_table(item['tests'])) + except KeyError: + continue + output.write('') + output.close() + + +def gen_html_table(data): + """Generates a table with TCs' names, documentation and messages / statuses + in html format. There is no css used. + + :param data: Json data representing a table with TCs. + :type data: str + :returns: Table with TCs' names, documentation and messages / statuses in + html format. + :rtype: str + """ + + table = '' + table += '' + table += '' + table += '' + + for item in data[0:-2]: + table += '' + for element in item: + table += '' + table += '
NameDocumentationStatus
' + element.replace(' |br| ', '
') + '
' + + return table + + +def do_rst(data, args): + """Generation of a rst file from json data. + + :param data: List of suites from json file. + :param args: Parsed arguments. + :type data: list of dict + :type args: ArgumentParser + :returns: Nothing. + """ + + hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^'] + shift = int(args.level) + start = int(args.start) + + output = open(args.output, 'w') + output.write('\n.. |br| raw:: html\n\n
\n\n') + + for item in data: + if int(item['level']) < start: + continue + if 'ndrchk' in item['title'].lower(): + continue + output.write(item['title'].lower() + '\n' + + hdrs[int(item['level']) - start + shift] * + len(item['title']) + '\n\n') + output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') + + '\n\n') + try: + output.write(gen_rst_table(item['tests']) + '\n\n') + except KeyError: + continue + output.close() + + +def gen_rst_table(data): + """Generates a table with TCs' names, documentation and messages / statuses + in rst format. + + :param data: Json data representing a table with TCs. + :type data: str + :returns: Table with TCs' names, documentation and messages / statuses in + rst format. + :rtype: str + """ + + table = [] + # max size of each column + lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data])) + + start_of_line = '| ' + vert_separator = ' | ' + end_of_line = ' |' + line_marker = '-' + + meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i) + for i in range(len(lengths))]) + template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths), + end_of_line) + # determine top/bottom borders + to_separator = string.maketrans('| ', '+-') + start_of_line = start_of_line.translate(to_separator) + vert_separator = vert_separator.translate(to_separator) + end_of_line = end_of_line.translate(to_separator) + separator = '{0}{1}{2}'.format(start_of_line, vert_separator. + join([x * line_marker for x in lengths]), + end_of_line) + # determine header separator + th_separator_tr = string.maketrans('-', '=') + start_of_line = start_of_line.translate(th_separator_tr) + line_marker = line_marker.translate(th_separator_tr) + vertical_separator = vert_separator.translate(th_separator_tr) + end_of_line = end_of_line.translate(th_separator_tr) + th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator. + join([x * line_marker for x in lengths]), + end_of_line) + # prepare table + table.append(separator) + # set table header + titles = data[-1] + table.append(template.format(*titles)) + table.append(th_separator) + # generate table rows + for d in data[0:-2]: + table.append(template.format(*d)) + table.append(separator) + table.append(template.format(*data[-2])) + table.append(separator) + return '\n'.join(table) + + +def do_md(data, args): + """Generation of a rst file from json data. + + :param data: List of suites from json file. + :param args: Parsed arguments. + :type data: list of dict + :type args: ArgumentParser + :returns: Nothing. + """ + raise NotImplementedError("Export to 'md' format is not implemented.") + + +def do_wiki(data, args): + """Generation of a wiki page from json data. + + :param data: List of suites from json file. + :param args: Parsed arguments. + :type data: list of dict + :type args: ArgumentParser + :returns: Nothing. + """ + raise NotImplementedError("Export to 'wiki' format is not implemented.") def process_robot_file(args): - """Process data from robot output.xml file and return raw data. + """Process data from robot output.xml file and generate defined file type. :param args: Parsed arguments. :type args: ArgumentParser :return: Nothing. """ + old_sys_stdout = sys.stdout + sys.stdout = open(args.output + '.json', 'w') + result = ExecutionResult(args.input) checker = ExecutionChecker(args) - result.visit(checker) - -def print_error(msg): - """Print error message on stderr. + sys.stdout.write('[') + result.visit(checker) + sys.stdout.write('{}]') + sys.stdout.close() + sys.stdout = old_sys_stdout - :param msg: Error message to print. - :type msg: str - :return: nothing - """ + with open(args.output + '.json', 'r') as json_file: + data = json.load(json_file) + data.pop(-1) - sys.stderr.write(msg+'\n') + if args.formatting == 'rst': + do_rst(data, args) + elif args.formatting == 'wiki': + do_wiki(data, args) + elif args.formatting == 'html': + do_html(data, args) + elif args.formatting == 'md': + do_md(data, args) def parse_args(): @@ -191,30 +375,32 @@ def parse_args(): :rtype ArgumentParser """ - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input", required=True, + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse. + RawDescriptionHelpFormatter) + parser.add_argument("-i", "--input", + required=True, type=argparse.FileType('r'), help="Robot XML log file") parser.add_argument("-o", "--output", - type=argparse.FileType('w'), + type=str, + required=True, help="Output file") - parser.add_argument("-f", "--formatting", required=True, - choices=['html', 'wiki'], + parser.add_argument("-f", "--formatting", + required=True, + choices=['html', 'wiki', 'rst', 'md'], help="Output file format") + parser.add_argument("-s", "--start", + type=int, + default=1, + help="The first level to be taken from xml file") + parser.add_argument("-l", "--level", + type=int, + default=1, + help="The level of the first chapter in generated file") return parser.parse_args() -def main(): - """Main function.""" - - args = parse_args() - - if args.output: - sys.stdout = args.output - - process_robot_file(args) - - if __name__ == "__main__": - sys.exit(main()) + sys.exit(process_robot_file(parse_args())) -- cgit 1.2.3-korg