CSIT doc autogen: Add media wiki format
[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
25 :TODO:
26  - wiki
27  - md
28
29 :Example:
30
31 robot_output_parser_publish.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 """
37
38 import argparse
39 import re
40 import sys
41 import json
42 import string
43
44 from robot.api import ExecutionResult, ResultVisitor
45
46
47 class ExecutionChecker(ResultVisitor):
48     """Class to traverse through the test suite structure.
49
50     The functionality implemented in this class generates a json file. Its
51     structure is:
52
53     [
54         {
55             "level": "Level of the suite, type: str",
56             "title": "Title of the suite, type: str",
57             "doc": "Documentation of the suite, type: str",
58             "table": [
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"]
63             ]
64         },
65         ... other test suites ...
66     ]
67
68     .. note:: The header of the table with TCs is at the and of the table.
69     """
70
71     def __init__(self, args):
72         self.formatting = args.formatting
73
74     def visit_suite(self, suite):
75         """Implements traversing through the suite and its direct children.
76
77         :param suite: Suite to process.
78         :type suite: Suite
79         :returns: Nothing.
80         """
81
82         if self.start_suite(suite) is not False:
83             if suite.tests:
84                 sys.stdout.write(',"tests":[')
85             else:
86                 sys.stdout.write('},')
87
88             suite.suites.visit(self)
89             suite.tests.visit(self)
90
91             if suite.tests:
92                 if "ndrdisc" in suite.longname.lower():
93                     hdr = '["Name","Documentation","Message"]'
94                 else:
95                     hdr = '["Name","Documentation","Status"]'
96                 sys.stdout.write(hdr + ']},')
97
98             self.end_suite(suite)
99
100     def start_suite(self, suite):
101         """Called when suite starts.
102
103         :param suite: Suite to process.
104         :type suite: Suite
105         :returns: Nothing.
106         """
107
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| *[') + '"')
115
116     def end_suite(self, suite):
117         """Called when suite ends.
118
119         :param suite: Suite to process.
120         :type suite: Suite
121         :returns: Nothing.
122         """
123         pass
124
125     def visit_test(self, test):
126         """Implements traversing through the test.
127
128         :param test: Test to process.
129         :type test: Test
130         :returns: Nothing.
131         """
132         if self.start_test(test) is not False:
133             self.end_test(test)
134
135     def start_test(self, test):
136         """Called when test starts.
137
138         :param test: Test to process.
139         :type test: Test
140         :returns: Nothing.
141         """
142
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', ''). \
148                 replace('"', "'")
149
150             sys.stdout.write('["' + name + '","' + doc + '","' + msg + '"]')
151         else:
152             sys.stdout.write(
153                 '["' + name + '","' + doc + '","' + test.status + '"]')
154
155     def end_test(self, test):
156         """Called when test ends.
157
158         :param test: Test to process.
159         :type test: Test
160         :returns: Nothing.
161         """
162         sys.stdout.write(',')
163
164
165 def do_html(data, args):
166     """Generation of a html file from json data.
167
168     :param data: List of suites from json file.
169     :param args: Parsed arguments.
170     :type data: list of dict
171     :type args: ArgumentParser
172     :returns: Nothing.
173     """
174
175     shift = int(args.level)
176     start = int(args.start)
177
178     output = open(args.output, 'w')
179
180     output.write('<html>')
181     for item in data:
182         if int(item['level']) < start:
183             continue
184         level = str(int(item['level']) - start + shift)
185         output.write('<h' + level + '>' + item['title'].lower() +
186                      '</h' + level + '>')
187         output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
188                                     0, flags=re.MULTILINE).
189                      replace(' |br| ', '<br>') + '</p>')
190         try:
191             output.write(gen_html_table(item['tests']))
192         except KeyError:
193             continue
194     output.write('</html>')
195     output.close()
196
197
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.
201
202     :param data: Json data representing a table with TCs.
203     :type data: str
204     :returns: Table with TCs' names, documentation and messages / statuses in
205     html format.
206     :rtype: str
207     """
208
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>'
213
214     for item in data[0:-1]:
215         table += '<tr>'
216         for element in item:
217             table += '<td>' + element.replace(' |br| ', '<br>') + '</td>'
218     table += '</tr></table>'
219
220     return table
221
222
223 def do_rst(data, args):
224     """Generation of a rst file from json data.
225
226     :param data: List of suites from json file.
227     :param args: Parsed arguments.
228     :type data: list of dict
229     :type args: ArgumentParser
230     :returns: Nothing.
231     """
232
233     hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
234     shift = int(args.level)
235     start = int(args.start)
236
237     output = open(args.output, 'w')
238     output.write('\n.. |br| raw:: html\n\n    <br />\n\n')
239
240     for item in data:
241         if int(item['level']) < start:
242             continue
243         if 'ndrchk' in item['title'].lower():
244             continue
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 -') +
249                      '\n\n')
250         try:
251             output.write(gen_rst_table(item['tests']) + '\n\n')
252         except KeyError:
253             continue
254     output.close()
255
256
257 def gen_rst_table(data):
258     """Generates a table with TCs' names, documentation and messages / statuses
259     in rst format.
260
261     :param data: Json data representing a table with TCs.
262     :type data: str
263     :returns: Table with TCs' names, documentation and messages / statuses in
264     rst format.
265     :rtype: str
266     """
267
268     table = []
269     # max size of each column
270     lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
271
272     start_of_line = '| '
273     vert_separator = ' | '
274     end_of_line = ' |'
275     line_marker = '-'
276
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),
280                                   end_of_line)
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]),
288                                    end_of_line)
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]),
297                                       end_of_line)
298     # prepare table
299     table.append(separator)
300     # set table header
301     titles = data[-1]
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)
313
314
315 def do_md(data, args):
316     """Generation of a rst file from json data.
317
318     :param data: List of suites from json file.
319     :param args: Parsed arguments.
320     :type data: list of dict
321     :type args: ArgumentParser
322     :returns: Nothing.
323     """
324     raise NotImplementedError("Export to 'md' format is not implemented.")
325
326
327 def do_wiki(data, args):
328     """Generation of a wiki page from json data.
329
330     :param data: List of suites from json file.
331     :param args: Parsed arguments.
332     :type data: list of dict
333     :type args: ArgumentParser
334     :returns: Nothing.
335     """
336
337     shift = int(args.level)
338     start = int(args.start)
339
340     output = open(args.output, 'w')
341
342     for item in data:
343         if int(item['level']) < start:
344             continue
345         if 'ndrchk' in item['title'].lower():
346             continue
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*') +
350                      '\n')
351         try:
352             output.write(gen_wiki_table(item['tests']) + '\n\n')
353         except KeyError:
354             continue
355     output.close()
356
357
358 def gen_wiki_table(data):
359     """Generates a table with TCs' names, documentation and messages / statuses
360     in wiki format.
361
362     :param data: Json data representing a table with TCs.
363     :type data: str
364     :returns: Table with TCs' names, documentation and messages / statuses in
365     wiki format.
366     :rtype: str
367     """
368
369     table = '{| class="wikitable"\n'
370     header = ""
371     for item in data[-1]:
372         header += '!{}\n'.format(item)
373     table += header
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)
378     table += '|}\n'
379
380     return table
381
382
383 def process_robot_file(args):
384     """Process data from robot output.xml file and generate defined file type.
385
386     :param args: Parsed arguments.
387     :type args: ArgumentParser
388     :return: Nothing.
389     """
390
391     old_sys_stdout = sys.stdout
392     sys.stdout = open(args.output + '.json', 'w')
393
394     result = ExecutionResult(args.input)
395     checker = ExecutionChecker(args)
396
397     sys.stdout.write('[')
398     result.visit(checker)
399     sys.stdout.write('{}]')
400     sys.stdout.close()
401     sys.stdout = old_sys_stdout
402
403     with open(args.output + '.json', 'r') as json_file:
404         data = json.load(json_file)
405     data.pop(-1)
406
407     if args.formatting == 'rst':
408         do_rst(data, args)
409     elif args.formatting == 'wiki':
410         do_wiki(data, args)
411     elif args.formatting == 'html':
412         do_html(data, args)
413     elif args.formatting == 'md':
414         do_md(data, args)
415
416
417 def parse_args():
418     """Parse arguments from cmd line.
419
420     :return: Parsed arguments.
421     :rtype ArgumentParser
422     """
423
424     parser = argparse.ArgumentParser(description=__doc__,
425                                      formatter_class=argparse.
426                                      RawDescriptionHelpFormatter)
427     parser.add_argument("-i", "--input",
428                         required=True,
429                         type=argparse.FileType('r'),
430                         help="Robot XML log file")
431     parser.add_argument("-o", "--output",
432                         type=str,
433                         required=True,
434                         help="Output file")
435     parser.add_argument("-f", "--formatting",
436                         required=True,
437                         choices=['html', 'wiki', 'rst', 'md'],
438                         help="Output file format")
439     parser.add_argument("-s", "--start",
440                         type=int,
441                         default=1,
442                         help="The first level to be taken from xml file")
443     parser.add_argument("-l", "--level",
444                         type=int,
445                         default=1,
446                         help="The level of the first chapter in generated file")
447
448     return parser.parse_args()
449
450
451 if __name__ == "__main__":
452     sys.exit(process_robot_file(parse_args()))