CSIT-561: Add filtering of testsuits to report generation
[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     for item in data:
251         if int(item['level']) < start:
252             continue
253         if 'ndrchk' in item['title'].lower():
254             continue
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 -') +
259                      '\n\n')
260         try:
261             output.write(gen_rst_table(item['tests']) + '\n\n')
262         except KeyError:
263             continue
264     output.close()
265
266
267 def gen_rst_table(data):
268     """Generates a table with TCs' names, documentation and messages / statuses
269     in rst format.
270
271     :param data: Json data representing a table with TCs.
272     :type data: str
273     :returns: Table with TCs' names, documentation and messages / statuses in
274     rst format.
275     :rtype: str
276     """
277
278     table = []
279     # max size of each column
280     lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
281
282     start_of_line = '| '
283     vert_separator = ' | '
284     end_of_line = ' |'
285     line_marker = '-'
286
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),
290                                   end_of_line)
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]),
298                                    end_of_line)
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]),
307                                       end_of_line)
308     # prepare table
309     table.append(separator)
310     # set table header
311     titles = data[-1]
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)
323
324
325 def do_md(data, args):
326     """Generation of a rst file from json data.
327
328     :param data: List of suites from json file.
329     :param args: Parsed arguments.
330     :type data: list of dict
331     :type args: ArgumentParser
332     :returns: Nothing.
333     """
334     raise NotImplementedError("Export to 'md' format is not implemented.")
335
336
337 def do_wiki(data, args):
338     """Generation of a wiki page from json data.
339
340     :param data: List of suites from json file.
341     :param args: Parsed arguments.
342     :type data: list of dict
343     :type args: ArgumentParser
344     :returns: Nothing.
345     """
346
347     shift = int(args.level)
348     start = int(args.start)
349
350     output = open(args.output, 'w')
351
352     for item in data:
353         if int(item['level']) < start:
354             continue
355         if 'ndrchk' in item['title'].lower():
356             continue
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*') +
360                      '\n')
361         try:
362             output.write(gen_wiki_table(item['tests']) + '\n\n')
363         except KeyError:
364             continue
365     output.close()
366
367
368 def gen_wiki_table(data):
369     """Generates a table with TCs' names, documentation and messages / statuses
370     in wiki format.
371
372     :param data: Json data representing a table with TCs.
373     :type data: str
374     :returns: Table with TCs' names, documentation and messages / statuses in
375     wiki format.
376     :rtype: str
377     """
378
379     table = '{| class="wikitable"\n'
380     header = ""
381     for item in data[-1]:
382         header += '!{}\n'.format(item)
383     table += header
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)
388     table += '|}\n'
389
390     return table
391
392
393 def process_robot_file(args):
394     """Process data from robot output.xml file and generate defined file type.
395
396     :param args: Parsed arguments.
397     :type args: ArgumentParser
398     :return: Nothing.
399     """
400
401     old_sys_stdout = sys.stdout
402     sys.stdout = open(args.output + '.json', 'w')
403
404     result = ExecutionResult(args.input)
405     checker = ExecutionChecker(args)
406
407     sys.stdout.write('[')
408     result.visit(checker)
409     sys.stdout.write('{}]')
410     sys.stdout.close()
411     sys.stdout = old_sys_stdout
412
413     with open(args.output + '.json', 'r') as json_file:
414         data = json.load(json_file)
415     data.pop(-1)
416
417     if args.regex:
418         results = list()
419         regex = re.compile(args.regex)
420         for item in data:
421             if re.search(regex, item['title'].lower()):
422                 results.append(item)
423     else:
424         results = data
425
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':
433         do_md(results, args)
434
435
436 def parse_args():
437     """Parse arguments from cmd line.
438
439     :return: Parsed arguments.
440     :rtype ArgumentParser
441     """
442
443     parser = argparse.ArgumentParser(description=__doc__,
444                                      formatter_class=argparse.
445                                      RawDescriptionHelpFormatter)
446     parser.add_argument("-i", "--input",
447                         required=True,
448                         type=argparse.FileType('r'),
449                         help="Robot XML log file")
450     parser.add_argument("-o", "--output",
451                         type=str,
452                         required=True,
453                         help="Output file")
454     parser.add_argument("-f", "--formatting",
455                         required=True,
456                         choices=['html', 'wiki', 'rst', 'md'],
457                         help="Output file format")
458     parser.add_argument("-s", "--start",
459                         type=int,
460                         default=1,
461                         help="The first level to be taken from xml file")
462     parser.add_argument("-l", "--level",
463                         type=int,
464                         default=1,
465                         help="The level of the first chapter in generated file")
466     parser.add_argument("-r", "--regex",
467                         type=str,
468                         default=None,
469                         help="Regular expression used to select test suites. "
470                              "If None, all test suites are selected.")
471
472     return parser.parse_args()
473
474
475 if __name__ == "__main__":
476     sys.exit(process_robot_file(parse_args()))