CSIT-755: Presentation and analytics layer
[csit.git] / resources / tools / report_gen / run_robot_data.py
1 #!/usr/bin/python
2
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:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15
16 """
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.
20
21 Supported formats:
22  - html
23  - rst
24  - wiki
25
26 :TODO:
27  - md
28
29 :Example:
30
31 run_robot_data.py -i "output.xml" -o "tests.rst" -f "rst" -s 3 -l 2
32
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.
37
38 :Example:
39
40 run_robot_data.py -i "output.xml" -o "tests.rst" -f "rst" -r "(.*)(lisp)(.*)"
41
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.
46 """
47
48 import argparse
49 import re
50 import sys
51 import json
52 import string
53
54 from robot.api import ExecutionResult, ResultVisitor
55
56
57 class ExecutionChecker(ResultVisitor):
58     """Class to traverse through the test suite structure.
59
60     The functionality implemented in this class generates a json file. Its
61     structure is:
62
63     [
64         {
65             "level": "Level of the suite, type: str",
66             "title": "Title of the suite, type: str",
67             "doc": "Documentation of the suite, type: str",
68             "table": [
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"]
73             ]
74         },
75         ... other test suites ...
76     ]
77
78     .. note:: The header of the table with TCs is at the and of the table.
79     """
80
81     def __init__(self, args):
82         self.formatting = args.formatting
83
84     def visit_suite(self, suite):
85         """Implements traversing through the suite and its direct children.
86
87         :param suite: Suite to process.
88         :type suite: Suite
89         :returns: Nothing.
90         """
91
92         if self.start_suite(suite) is not False:
93             if suite.tests:
94                 sys.stdout.write(',"tests":[')
95             else:
96                 sys.stdout.write('},')
97
98             suite.suites.visit(self)
99             suite.tests.visit(self)
100
101             if suite.tests:
102                 if "ndrdisc" in suite.longname.lower():
103                     hdr = '["Name","Documentation","Message"]'
104                 else:
105                     hdr = '["Name","Documentation","Status"]'
106                 sys.stdout.write(hdr + ']},')
107
108             self.end_suite(suite)
109
110     def start_suite(self, suite):
111         """Called when suite starts.
112
113         :param suite: Suite to process.
114         :type suite: Suite
115         :returns: Nothing.
116         """
117
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| *[') + '"')
125
126     def end_suite(self, suite):
127         """Called when suite ends.
128
129         :param suite: Suite to process.
130         :type suite: Suite
131         :returns: Nothing.
132         """
133         pass
134
135     def visit_test(self, test):
136         """Implements traversing through the test.
137
138         :param test: Test to process.
139         :type test: Test
140         :returns: Nothing.
141         """
142         if self.start_test(test) is not False:
143             self.end_test(test)
144
145     def start_test(self, test):
146         """Called when test starts.
147
148         :param test: Test to process.
149         :type test: Test
150         :returns: Nothing.
151         """
152
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', ''). \
158                 replace('"', "'")
159
160             sys.stdout.write('["' + name + '","' + doc + '","' + msg + '"]')
161         else:
162             sys.stdout.write(
163                 '["' + name + '","' + doc + '","' + test.status + '"]')
164
165     def end_test(self, test):
166         """Called when test ends.
167
168         :param test: Test to process.
169         :type test: Test
170         :returns: Nothing.
171         """
172         sys.stdout.write(',')
173
174
175 def do_html(data, args):
176     """Generation of a html file from json data.
177
178     :param data: List of suites from json file.
179     :param args: Parsed arguments.
180     :type data: list of dict
181     :type args: ArgumentParser
182     :returns: Nothing.
183     """
184
185     shift = int(args.level)
186     start = int(args.start)
187
188     output = open(args.output, 'w')
189
190     output.write('<html>')
191     for item in data:
192         if int(item['level']) < start:
193             continue
194         level = str(int(item['level']) - start + shift)
195         output.write('<h' + level + '>' + item['title'].lower() +
196                      '</h' + level + '>')
197         output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
198                                     0, flags=re.MULTILINE).
199                      replace(' |br| ', '<br>') + '</p>')
200         try:
201             output.write(gen_html_table(item['tests']))
202         except KeyError:
203             continue
204     output.write('</html>')
205     output.close()
206
207
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.
211
212     :param data: Json data representing a table with TCs.
213     :type data: str
214     :returns: Table with TCs' names, documentation and messages / statuses in
215     html format.
216     :rtype: str
217     """
218
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>'
223
224     for item in data[0:-1]:
225         table += '<tr>'
226         for element in item:
227             table += '<td>' + element.replace(' |br| ', '<br>') + '</td>'
228     table += '</tr></table>'
229
230     return table
231
232
233 def do_rst(data, args):
234     """Generation of a rst file from json data.
235
236     :param data: List of suites from json file.
237     :param args: Parsed arguments.
238     :type data: list of dict
239     :type args: ArgumentParser
240     :returns: Nothing.
241     """
242
243     hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
244     shift = int(args.level)
245     start = int(args.start)
246
247     output = open(args.output, 'w')
248     output.write('\n.. |br| raw:: html\n\n    <br />\n\n')
249
250     if (args.title):
251         output.write(args.title + '\n' +
252                      hdrs[shift - 1] *
253                      len(args.title) + '\n\n')
254
255     for item in data:
256         if int(item['level']) < start:
257             continue
258         if 'ndrchk' in item['title'].lower():
259             continue
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 -') +
264                      '\n\n')
265         try:
266             output.write(gen_rst_table(item['tests']) + '\n\n')
267         except KeyError:
268             continue
269     output.close()
270
271
272 def gen_rst_table(data):
273     """Generates a table with TCs' names, documentation and messages / statuses
274     in rst format.
275
276     :param data: Json data representing a table with TCs.
277     :type data: str
278     :returns: Table with TCs' names, documentation and messages / statuses in
279     rst format.
280     :rtype: str
281     """
282
283     table = []
284     # max size of each column
285     lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
286
287     start_of_line = '| '
288     vert_separator = ' | '
289     end_of_line = ' |'
290     line_marker = '-'
291
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),
295                                   end_of_line)
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]),
303                                    end_of_line)
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]),
312                                       end_of_line)
313     # prepare table
314     table.append(separator)
315     # set table header
316     titles = data[-1]
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)
328
329
330 def do_md(data, args):
331     """Generation of a rst file from json data.
332
333     :param data: List of suites from json file.
334     :param args: Parsed arguments.
335     :type data: list of dict
336     :type args: ArgumentParser
337     :returns: Nothing.
338     """
339     raise NotImplementedError("Export to 'md' format is not implemented.")
340
341
342 def do_wiki(data, args):
343     """Generation of a wiki page from json data.
344
345     :param data: List of suites from json file.
346     :param args: Parsed arguments.
347     :type data: list of dict
348     :type args: ArgumentParser
349     :returns: Nothing.
350     """
351
352     shift = int(args.level)
353     start = int(args.start)
354
355     output = open(args.output, 'w')
356
357     for item in data:
358         if int(item['level']) < start:
359             continue
360         if 'ndrchk' in item['title'].lower():
361             continue
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*') +
365                      '\n')
366         try:
367             output.write(gen_wiki_table(item['tests']) + '\n\n')
368         except KeyError:
369             continue
370     output.close()
371
372
373 def gen_wiki_table(data):
374     """Generates a table with TCs' names, documentation and messages / statuses
375     in wiki format.
376
377     :param data: Json data representing a table with TCs.
378     :type data: str
379     :returns: Table with TCs' names, documentation and messages / statuses in
380     wiki format.
381     :rtype: str
382     """
383
384     table = '{| class="wikitable"\n'
385     header = ""
386     for item in data[-1]:
387         header += '!{}\n'.format(item)
388     table += header
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)
393     table += '|}\n'
394
395     return table
396
397
398 def process_robot_file(args):
399     """Process data from robot output.xml file and generate defined file type.
400
401     :param args: Parsed arguments.
402     :type args: ArgumentParser
403     :return: Nothing.
404     """
405
406     old_sys_stdout = sys.stdout
407     sys.stdout = open(args.output + '.json', 'w')
408
409     result = ExecutionResult(args.input)
410     checker = ExecutionChecker(args)
411
412     sys.stdout.write('[')
413     result.visit(checker)
414     sys.stdout.write('{}]')
415     sys.stdout.close()
416     sys.stdout = old_sys_stdout
417
418     with open(args.output + '.json', 'r') as json_file:
419         data = json.load(json_file)
420     data.pop(-1)
421
422     if args.regex:
423         results = list()
424         regex = re.compile(args.regex)
425         for item in data:
426             if re.search(regex, item['title'].lower()):
427                 results.append(item)
428     else:
429         results = data
430
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':
438         do_md(results, args)
439
440
441 def parse_args():
442     """Parse arguments from cmd line.
443
444     :return: Parsed arguments.
445     :rtype ArgumentParser
446     """
447
448     parser = argparse.ArgumentParser(description=__doc__,
449                                      formatter_class=argparse.
450                                      RawDescriptionHelpFormatter)
451     parser.add_argument("-i", "--input",
452                         required=True,
453                         type=argparse.FileType('r'),
454                         help="Robot XML log file")
455     parser.add_argument("-o", "--output",
456                         type=str,
457                         required=True,
458                         help="Output file")
459     parser.add_argument("-f", "--formatting",
460                         required=True,
461                         choices=['html', 'wiki', 'rst', 'md'],
462                         help="Output file format")
463     parser.add_argument("-s", "--start",
464                         type=int,
465                         default=1,
466                         help="The first level to be taken from xml file")
467     parser.add_argument("-l", "--level",
468                         type=int,
469                         default=1,
470                         help="The level of the first chapter in generated file")
471     parser.add_argument("-r", "--regex",
472                         type=str,
473                         default=None,
474                         help="Regular expression used to select test suites. "
475                              "If None, all test suites are selected.")
476     parser.add_argument("-t", "--title",
477                         type=str,
478                         default=None,
479                         help="Title of the output.")
480
481     return parser.parse_args()
482
483
484 if __name__ == "__main__":
485     sys.exit(process_robot_file(parse_args()))