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, documentation, message, status) from
18 robot framework output file (output.xml) and prints in specified format (wiki,
19 html, rst) to defined output file.
31 robot_output_parser_publish.py -i output.xml" -o "tests.rst" -f "rst" -s 3 -l 2
33 The example reads the data from "output.xml", writes the output to "tests.rst"
34 in rst format. It will start on the 3rd level of xml structure and the generated
35 document hierarchy will start on the 2nd level.
44 from robot.api import ExecutionResult, ResultVisitor
47 class ExecutionChecker(ResultVisitor):
48 """Class to traverse through the test suite structure.
50 The functionality implemented in this class generates a json file. Its
55 "level": "Level of the suite, type: str",
56 "title": "Title of the suite, type: str",
57 "doc": "Documentation of the suite, type: str",
59 ["TC name", "TC doc", "message or status"],
60 ["TC name", "TC doc", "message or status"],
61 ... other test cases ...
62 ["Name", "Documentation", "Message or Status"]
65 ... other test suites ...
68 .. note:: The header of the table with TCs is at the and of the table.
71 def __init__(self, args):
72 self.formatting = args.formatting
74 def visit_suite(self, suite):
75 """Implements traversing through the suite and its direct children.
77 :param suite: Suite to process.
82 if self.start_suite(suite) is not False:
84 sys.stdout.write(',"tests":[')
86 sys.stdout.write('},')
88 suite.suites.visit(self)
89 suite.tests.visit(self)
92 if "ndrdisc" in suite.longname.lower():
93 hdr = '["Name","Documentation","Message"]'
95 hdr = '["Name","Documentation","Status"]'
96 sys.stdout.write(hdr + ']},')
100 def start_suite(self, suite):
101 """Called when suite starts.
103 :param suite: Suite to process.
108 level = len(suite.longname.split("."))
109 sys.stdout.write('{')
110 sys.stdout.write('"level":"' + str(level) + '",')
111 sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",')
112 sys.stdout.write('"doc":"' + suite.doc.replace('"', "'").
113 replace('\n', ' ').replace('\r', '').
114 replace('*[', ' |br| *[') + '"')
116 def end_suite(self, suite):
117 """Called when suite ends.
119 :param suite: Suite to process.
125 def visit_test(self, test):
126 """Implements traversing through the test.
128 :param test: Test to process.
132 if self.start_test(test) is not False:
135 def start_test(self, test):
136 """Called when test starts.
138 :param test: Test to process.
143 name = test.name.replace('"', "'")
144 doc = test.doc.replace('"', "'").replace('\n', ' ').replace('\r', '').\
145 replace('[', ' |br| [')
146 if any("NDRPDRDISC" in tag for tag in test.tags):
147 msg = test.message.replace('\n', ' |br| ').replace('\r', ''). \
150 sys.stdout.write('["' + name + '","' + doc + '","' + msg + '"]')
153 '["' + name + '","' + doc + '","' + test.status + '"]')
155 def end_test(self, test):
156 """Called when test ends.
158 :param test: Test to process.
162 sys.stdout.write(',')
165 def do_html(data, args):
166 """Generation of a html file from json data.
168 :param data: List of suites from json file.
169 :param args: Parsed arguments.
170 :type data: list of dict
171 :type args: ArgumentParser
175 shift = int(args.level)
176 start = int(args.start)
178 output = open(args.output, 'w')
180 output.write('<html>')
182 if int(item['level']) < start:
184 level = str(int(item['level']) - start + shift)
185 output.write('<h' + level + '>' + item['title'].lower() +
187 output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
188 0, flags=re.MULTILINE).
189 replace(' |br| ', '<br>') + '</p>')
191 output.write(gen_html_table(item['tests']))
194 output.write('</html>')
198 def gen_html_table(data):
199 """Generates a table with TCs' names, documentation and messages / statuses
200 in html format. There is no css used.
202 :param data: Json data representing a table with TCs.
204 :returns: Table with TCs' names, documentation and messages / statuses in
209 table = '<table width=100% border=1><tr>'
210 table += '<th width=30%>' + data[-1][0] + '</th>'
211 table += '<th width=50%>' + data[-1][1] + '</th>'
212 table += '<th width=20%>' + data[-1][2] + '</th></tr>'
214 for item in data[0:-1]:
217 table += '<td>' + element.replace(' |br| ', '<br>') + '</td>'
218 table += '</tr></table>'
223 def do_rst(data, args):
224 """Generation of a rst file from json data.
226 :param data: List of suites from json file.
227 :param args: Parsed arguments.
228 :type data: list of dict
229 :type args: ArgumentParser
233 hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
234 shift = int(args.level)
235 start = int(args.start)
237 output = open(args.output, 'w')
238 output.write('\n.. |br| raw:: html\n\n <br />\n\n')
241 if int(item['level']) < start:
243 if 'ndrchk' in item['title'].lower():
245 output.write(item['title'].lower() + '\n' +
246 hdrs[int(item['level']) - start + shift] *
247 len(item['title']) + '\n\n')
248 output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
251 output.write(gen_rst_table(item['tests']) + '\n\n')
257 def gen_rst_table(data):
258 """Generates a table with TCs' names, documentation and messages / statuses
261 :param data: Json data representing a table with TCs.
263 :returns: Table with TCs' names, documentation and messages / statuses in
269 # max size of each column
270 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
273 vert_separator = ' | '
277 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
278 for i in range(len(lengths))])
279 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
281 # determine top/bottom borders
282 to_separator = string.maketrans('| ', '+-')
283 start_of_line = start_of_line.translate(to_separator)
284 vert_separator = vert_separator.translate(to_separator)
285 end_of_line = end_of_line.translate(to_separator)
286 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
287 join([x * line_marker for x in lengths]),
289 # determine header separator
290 th_separator_tr = string.maketrans('-', '=')
291 start_of_line = start_of_line.translate(th_separator_tr)
292 line_marker = line_marker.translate(th_separator_tr)
293 vertical_separator = vert_separator.translate(th_separator_tr)
294 end_of_line = end_of_line.translate(th_separator_tr)
295 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
296 join([x * line_marker for x in lengths]),
299 table.append(separator)
302 table.append(template.format(*titles))
303 table.append(th_separator)
304 # generate table rows
305 for item in data[0:-2]:
306 desc = re.sub(r'(^ \|br\| )', r'', item[1])
307 table.append(template.format(item[0], desc, item[2]))
308 table.append(separator)
309 desc = re.sub(r'(^ \|br\| )', r'', data[-2][1])
310 table.append(template.format(data[-2][0], desc, data[-2][2]))
311 table.append(separator)
312 return '\n'.join(table)
315 def do_md(data, args):
316 """Generation of a rst file from json data.
318 :param data: List of suites from json file.
319 :param args: Parsed arguments.
320 :type data: list of dict
321 :type args: ArgumentParser
324 raise NotImplementedError("Export to 'md' format is not implemented.")
327 def do_wiki(data, args):
328 """Generation of a wiki page from json data.
330 :param data: List of suites from json file.
331 :param args: Parsed arguments.
332 :type data: list of dict
333 :type args: ArgumentParser
337 shift = int(args.level)
338 start = int(args.start)
340 output = open(args.output, 'w')
343 if int(item['level']) < start:
345 if 'ndrchk' in item['title'].lower():
347 mark = "=" * (int(item['level']) - start + shift) + ' '
348 output.write(mark + item['title'].lower() + mark + '\n')
349 output.write(item['doc'].replace('*', "'''").replace('|br|', '\n*') +
352 output.write(gen_wiki_table(item['tests']) + '\n\n')
358 def gen_wiki_table(data):
359 """Generates a table with TCs' names, documentation and messages / statuses
362 :param data: Json data representing a table with TCs.
364 :returns: Table with TCs' names, documentation and messages / statuses in
369 table = '{| class="wikitable"\n'
371 for item in data[-1]:
372 header += '!{}\n'.format(item)
374 for item in data[0:-1]:
375 desc = re.sub(r'(^ \|br\| )', r'', item[1]).replace(' |br| ', '\n\n')
376 msg = item[2].replace(' |br| ', '\n\n')
377 table += '|-\n|{}\n|{}\n|{}\n'.format(item[0], desc, msg)
383 def process_robot_file(args):
384 """Process data from robot output.xml file and generate defined file type.
386 :param args: Parsed arguments.
387 :type args: ArgumentParser
391 old_sys_stdout = sys.stdout
392 sys.stdout = open(args.output + '.json', 'w')
394 result = ExecutionResult(args.input)
395 checker = ExecutionChecker(args)
397 sys.stdout.write('[')
398 result.visit(checker)
399 sys.stdout.write('{}]')
401 sys.stdout = old_sys_stdout
403 with open(args.output + '.json', 'r') as json_file:
404 data = json.load(json_file)
407 if args.formatting == 'rst':
409 elif args.formatting == 'wiki':
411 elif args.formatting == 'html':
413 elif args.formatting == 'md':
418 """Parse arguments from cmd line.
420 :return: Parsed arguments.
421 :rtype ArgumentParser
424 parser = argparse.ArgumentParser(description=__doc__,
425 formatter_class=argparse.
426 RawDescriptionHelpFormatter)
427 parser.add_argument("-i", "--input",
429 type=argparse.FileType('r'),
430 help="Robot XML log file")
431 parser.add_argument("-o", "--output",
435 parser.add_argument("-f", "--formatting",
437 choices=['html', 'wiki', 'rst', 'md'],
438 help="Output file format")
439 parser.add_argument("-s", "--start",
442 help="The first level to be taken from xml file")
443 parser.add_argument("-l", "--level",
446 help="The level of the first chapter in generated file")
448 return parser.parse_args()
451 if __name__ == "__main__":
452 sys.exit(process_robot_file(parse_args()))