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 run_robot_data.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.
36 All test suites will be processed.
40 run_robot_data.py -i "output.xml" -o "tests.rst" -f "rst" -r "(.*)(lisp)(.*)"
42 The example reads the data from "output.xml", writes the output to "tests.rst"
43 in rst format. It will start on the 1st level of xml structure and the generated
44 document hierarchy will start on the 1st level (default values).
45 Only the test suites which match the given regular expression are processed.
54 from robot.api import ExecutionResult, ResultVisitor
57 class ExecutionChecker(ResultVisitor):
58 """Class to traverse through the test suite structure.
60 The functionality implemented in this class generates a json file. Its
65 "level": "Level of the suite, type: str",
66 "title": "Title of the suite, type: str",
67 "doc": "Documentation of the suite, type: str",
69 ["TC name", "TC doc", "message or status"],
70 ["TC name", "TC doc", "message or status"],
71 ... other test cases ...
72 ["Name", "Documentation", "Message or Status"]
75 ... other test suites ...
78 .. note:: The header of the table with TCs is at the and of the table.
81 def __init__(self, args):
82 self.formatting = args.formatting
84 def visit_suite(self, suite):
85 """Implements traversing through the suite and its direct children.
87 :param suite: Suite to process.
92 if self.start_suite(suite) is not False:
94 sys.stdout.write(',"tests":[')
96 sys.stdout.write('},')
98 suite.suites.visit(self)
99 suite.tests.visit(self)
102 if "ndrdisc" in suite.longname.lower():
103 hdr = '["Name","Documentation","Message"]'
105 hdr = '["Name","Documentation","Status"]'
106 sys.stdout.write(hdr + ']},')
108 self.end_suite(suite)
110 def start_suite(self, suite):
111 """Called when suite starts.
113 :param suite: Suite to process.
118 level = len(suite.longname.split("."))
119 sys.stdout.write('{')
120 sys.stdout.write('"level":"' + str(level) + '",')
121 sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",')
122 sys.stdout.write('"doc":"' + suite.doc.replace('"', "'").
123 replace('\n', ' ').replace('\r', '').
124 replace('*[', ' |br| *[') + '"')
126 def end_suite(self, suite):
127 """Called when suite ends.
129 :param suite: Suite to process.
135 def visit_test(self, test):
136 """Implements traversing through the test.
138 :param test: Test to process.
142 if self.start_test(test) is not False:
145 def start_test(self, test):
146 """Called when test starts.
148 :param test: Test to process.
153 name = test.name.replace('"', "'")
154 doc = test.doc.replace('"', "'").replace('\n', ' ').replace('\r', '').\
155 replace('[', ' |br| [')
156 if any("NDRPDRDISC" in tag for tag in test.tags):
157 msg = test.message.replace('\n', ' |br| ').replace('\r', ''). \
160 sys.stdout.write('["' + name + '","' + doc + '","' + msg + '"]')
163 '["' + name + '","' + doc + '","' + test.status + '"]')
165 def end_test(self, test):
166 """Called when test ends.
168 :param test: Test to process.
172 sys.stdout.write(',')
175 def do_html(data, args):
176 """Generation of a html file from json data.
178 :param data: List of suites from json file.
179 :param args: Parsed arguments.
180 :type data: list of dict
181 :type args: ArgumentParser
185 shift = int(args.level)
186 start = int(args.start)
188 output = open(args.output, 'w')
190 output.write('<html>')
192 if int(item['level']) < start:
194 level = str(int(item['level']) - start + shift)
195 output.write('<h' + level + '>' + item['title'].lower() +
197 output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
198 0, flags=re.MULTILINE).
199 replace(' |br| ', '<br>') + '</p>')
201 output.write(gen_html_table(item['tests']))
204 output.write('</html>')
208 def gen_html_table(data):
209 """Generates a table with TCs' names, documentation and messages / statuses
210 in html format. There is no css used.
212 :param data: Json data representing a table with TCs.
214 :returns: Table with TCs' names, documentation and messages / statuses in
219 table = '<table width=100% border=1><tr>'
220 table += '<th width=30%>' + data[-1][0] + '</th>'
221 table += '<th width=50%>' + data[-1][1] + '</th>'
222 table += '<th width=20%>' + data[-1][2] + '</th></tr>'
224 for item in data[0:-1]:
227 table += '<td>' + element.replace(' |br| ', '<br>') + '</td>'
228 table += '</tr></table>'
233 def do_rst(data, args):
234 """Generation of a rst file from json data.
236 :param data: List of suites from json file.
237 :param args: Parsed arguments.
238 :type data: list of dict
239 :type args: ArgumentParser
243 hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
244 shift = int(args.level)
245 start = int(args.start)
247 output = open(args.output, 'w')
248 output.write('\n.. |br| raw:: html\n\n <br />\n\n')
251 if int(item['level']) < start:
253 if 'ndrchk' in item['title'].lower():
255 output.write(item['title'].lower() + '\n' +
256 hdrs[int(item['level']) - start + shift] *
257 len(item['title']) + '\n\n')
258 output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
261 output.write(gen_rst_table(item['tests']) + '\n\n')
267 def gen_rst_table(data):
268 """Generates a table with TCs' names, documentation and messages / statuses
271 :param data: Json data representing a table with TCs.
273 :returns: Table with TCs' names, documentation and messages / statuses in
279 # max size of each column
280 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
283 vert_separator = ' | '
287 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
288 for i in range(len(lengths))])
289 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
291 # determine top/bottom borders
292 to_separator = string.maketrans('| ', '+-')
293 start_of_line = start_of_line.translate(to_separator)
294 vert_separator = vert_separator.translate(to_separator)
295 end_of_line = end_of_line.translate(to_separator)
296 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
297 join([x * line_marker for x in lengths]),
299 # determine header separator
300 th_separator_tr = string.maketrans('-', '=')
301 start_of_line = start_of_line.translate(th_separator_tr)
302 line_marker = line_marker.translate(th_separator_tr)
303 vertical_separator = vert_separator.translate(th_separator_tr)
304 end_of_line = end_of_line.translate(th_separator_tr)
305 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
306 join([x * line_marker for x in lengths]),
309 table.append(separator)
312 table.append(template.format(*titles))
313 table.append(th_separator)
314 # generate table rows
315 for item in data[0:-2]:
316 desc = re.sub(r'(^ \|br\| )', r'', item[1])
317 table.append(template.format(item[0], desc, item[2]))
318 table.append(separator)
319 desc = re.sub(r'(^ \|br\| )', r'', data[-2][1])
320 table.append(template.format(data[-2][0], desc, data[-2][2]))
321 table.append(separator)
322 return '\n'.join(table)
325 def do_md(data, args):
326 """Generation of a rst file from json data.
328 :param data: List of suites from json file.
329 :param args: Parsed arguments.
330 :type data: list of dict
331 :type args: ArgumentParser
334 raise NotImplementedError("Export to 'md' format is not implemented.")
337 def do_wiki(data, args):
338 """Generation of a wiki page from json data.
340 :param data: List of suites from json file.
341 :param args: Parsed arguments.
342 :type data: list of dict
343 :type args: ArgumentParser
347 shift = int(args.level)
348 start = int(args.start)
350 output = open(args.output, 'w')
353 if int(item['level']) < start:
355 if 'ndrchk' in item['title'].lower():
357 mark = "=" * (int(item['level']) - start + shift) + ' '
358 output.write(mark + item['title'].lower() + mark + '\n')
359 output.write(item['doc'].replace('*', "'''").replace('|br|', '\n*') +
362 output.write(gen_wiki_table(item['tests']) + '\n\n')
368 def gen_wiki_table(data):
369 """Generates a table with TCs' names, documentation and messages / statuses
372 :param data: Json data representing a table with TCs.
374 :returns: Table with TCs' names, documentation and messages / statuses in
379 table = '{| class="wikitable"\n'
381 for item in data[-1]:
382 header += '!{}\n'.format(item)
384 for item in data[0:-1]:
385 desc = re.sub(r'(^ \|br\| )', r'', item[1]).replace(' |br| ', '\n\n')
386 msg = item[2].replace(' |br| ', '\n\n')
387 table += '|-\n|{}\n|{}\n|{}\n'.format(item[0], desc, msg)
393 def process_robot_file(args):
394 """Process data from robot output.xml file and generate defined file type.
396 :param args: Parsed arguments.
397 :type args: ArgumentParser
401 old_sys_stdout = sys.stdout
402 sys.stdout = open(args.output + '.json', 'w')
404 result = ExecutionResult(args.input)
405 checker = ExecutionChecker(args)
407 sys.stdout.write('[')
408 result.visit(checker)
409 sys.stdout.write('{}]')
411 sys.stdout = old_sys_stdout
413 with open(args.output + '.json', 'r') as json_file:
414 data = json.load(json_file)
419 regex = re.compile(args.regex)
421 if re.search(regex, item['title'].lower()):
426 if args.formatting == 'rst':
427 do_rst(results, args)
428 elif args.formatting == 'wiki':
429 do_wiki(results, args)
430 elif args.formatting == 'html':
431 do_html(results, args)
432 elif args.formatting == 'md':
437 """Parse arguments from cmd line.
439 :return: Parsed arguments.
440 :rtype ArgumentParser
443 parser = argparse.ArgumentParser(description=__doc__,
444 formatter_class=argparse.
445 RawDescriptionHelpFormatter)
446 parser.add_argument("-i", "--input",
448 type=argparse.FileType('r'),
449 help="Robot XML log file")
450 parser.add_argument("-o", "--output",
454 parser.add_argument("-f", "--formatting",
456 choices=['html', 'wiki', 'rst', 'md'],
457 help="Output file format")
458 parser.add_argument("-s", "--start",
461 help="The first level to be taken from xml file")
462 parser.add_argument("-l", "--level",
465 help="The level of the first chapter in generated file")
466 parser.add_argument("-r", "--regex",
469 help="Regular expression used to select test suites. "
470 "If None, all test suites are selected.")
472 return parser.parse_args()
475 if __name__ == "__main__":
476 sys.exit(process_robot_file(parse_args()))