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 output.write(args.title + '\n' +
253 len(args.title) + '\n\n')
256 if int(item['level']) < start:
258 if 'ndrchk' in item['title'].lower():
260 output.write(item['title'].lower() + '\n' +
261 hdrs[int(item['level']) - start + shift] *
262 len(item['title']) + '\n\n')
263 output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
266 output.write(gen_rst_table(item['tests']) + '\n\n')
272 def gen_rst_table(data):
273 """Generates a table with TCs' names, documentation and messages / statuses
276 :param data: Json data representing a table with TCs.
278 :returns: Table with TCs' names, documentation and messages / statuses in
284 # max size of each column
285 lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
288 vert_separator = ' | '
292 meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
293 for i in range(len(lengths))])
294 template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
296 # determine top/bottom borders
297 to_separator = string.maketrans('| ', '+-')
298 start_of_line = start_of_line.translate(to_separator)
299 vert_separator = vert_separator.translate(to_separator)
300 end_of_line = end_of_line.translate(to_separator)
301 separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
302 join([x * line_marker for x in lengths]),
304 # determine header separator
305 th_separator_tr = string.maketrans('-', '=')
306 start_of_line = start_of_line.translate(th_separator_tr)
307 line_marker = line_marker.translate(th_separator_tr)
308 vertical_separator = vert_separator.translate(th_separator_tr)
309 end_of_line = end_of_line.translate(th_separator_tr)
310 th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
311 join([x * line_marker for x in lengths]),
314 table.append(separator)
317 table.append(template.format(*titles))
318 table.append(th_separator)
319 # generate table rows
320 for item in data[0:-2]:
321 desc = re.sub(r'(^ \|br\| )', r'', item[1])
322 table.append(template.format(item[0], desc, item[2]))
323 table.append(separator)
324 desc = re.sub(r'(^ \|br\| )', r'', data[-2][1])
325 table.append(template.format(data[-2][0], desc, data[-2][2]))
326 table.append(separator)
327 return '\n'.join(table)
330 def do_md(data, args):
331 """Generation of a rst file from json data.
333 :param data: List of suites from json file.
334 :param args: Parsed arguments.
335 :type data: list of dict
336 :type args: ArgumentParser
339 raise NotImplementedError("Export to 'md' format is not implemented.")
342 def do_wiki(data, args):
343 """Generation of a wiki page from json data.
345 :param data: List of suites from json file.
346 :param args: Parsed arguments.
347 :type data: list of dict
348 :type args: ArgumentParser
352 shift = int(args.level)
353 start = int(args.start)
355 output = open(args.output, 'w')
358 if int(item['level']) < start:
360 if 'ndrchk' in item['title'].lower():
362 mark = "=" * (int(item['level']) - start + shift) + ' '
363 output.write(mark + item['title'].lower() + mark + '\n')
364 output.write(item['doc'].replace('*', "'''").replace('|br|', '\n*') +
367 output.write(gen_wiki_table(item['tests']) + '\n\n')
373 def gen_wiki_table(data):
374 """Generates a table with TCs' names, documentation and messages / statuses
377 :param data: Json data representing a table with TCs.
379 :returns: Table with TCs' names, documentation and messages / statuses in
384 table = '{| class="wikitable"\n'
386 for item in data[-1]:
387 header += '!{}\n'.format(item)
389 for item in data[0:-1]:
390 desc = re.sub(r'(^ \|br\| )', r'', item[1]).replace(' |br| ', '\n\n')
391 msg = item[2].replace(' |br| ', '\n\n')
392 table += '|-\n|{}\n|{}\n|{}\n'.format(item[0], desc, msg)
398 def process_robot_file(args):
399 """Process data from robot output.xml file and generate defined file type.
401 :param args: Parsed arguments.
402 :type args: ArgumentParser
406 old_sys_stdout = sys.stdout
407 sys.stdout = open(args.output + '.json', 'w')
409 result = ExecutionResult(args.input)
410 checker = ExecutionChecker(args)
412 sys.stdout.write('[')
413 result.visit(checker)
414 sys.stdout.write('{}]')
416 sys.stdout = old_sys_stdout
418 with open(args.output + '.json', 'r') as json_file:
419 data = json.load(json_file)
424 regex = re.compile(args.regex)
426 if re.search(regex, item['title'].lower()):
431 if args.formatting == 'rst':
432 do_rst(results, args)
433 elif args.formatting == 'wiki':
434 do_wiki(results, args)
435 elif args.formatting == 'html':
436 do_html(results, args)
437 elif args.formatting == 'md':
442 """Parse arguments from cmd line.
444 :return: Parsed arguments.
445 :rtype ArgumentParser
448 parser = argparse.ArgumentParser(description=__doc__,
449 formatter_class=argparse.
450 RawDescriptionHelpFormatter)
451 parser.add_argument("-i", "--input",
453 type=argparse.FileType('r'),
454 help="Robot XML log file")
455 parser.add_argument("-o", "--output",
459 parser.add_argument("-f", "--formatting",
461 choices=['html', 'wiki', 'rst', 'md'],
462 help="Output file format")
463 parser.add_argument("-s", "--start",
466 help="The first level to be taken from xml file")
467 parser.add_argument("-l", "--level",
470 help="The level of the first chapter in generated file")
471 parser.add_argument("-r", "--regex",
474 help="Regular expression used to select test suites. "
475 "If None, all test suites are selected.")
476 parser.add_argument("-t", "--title",
479 help="Title of the output.")
481 return parser.parse_args()
484 if __name__ == "__main__":
485 sys.exit(process_robot_file(parse_args()))