e2bcfa2f0de012861ae4be72591c87d8273c60ef
[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%>Name</th>'
211     table += '<th width=50%>Documentation</th>'
212     table += '<th width=20%>Status</th></tr>'
213
214     for item in data[0:-2]:
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 d in data[0:-2]:
306         table.append(template.format(*d))
307         table.append(separator)
308     table.append(template.format(*data[-2]))
309     table.append(separator)
310     return '\n'.join(table)
311
312
313 def do_md(data, args):
314     """Generation of a rst file from json data.
315
316     :param data: List of suites from json file.
317     :param args: Parsed arguments.
318     :type data: list of dict
319     :type args: ArgumentParser
320     :returns: Nothing.
321     """
322     raise NotImplementedError("Export to 'md' format is not implemented.")
323
324
325 def do_wiki(data, args):
326     """Generation of a wiki page 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 'wiki' format is not implemented.")
335
336
337 def process_robot_file(args):
338     """Process data from robot output.xml file and generate defined file type.
339
340     :param args: Parsed arguments.
341     :type args: ArgumentParser
342     :return: Nothing.
343     """
344
345     old_sys_stdout = sys.stdout
346     sys.stdout = open(args.output + '.json', 'w')
347
348     result = ExecutionResult(args.input)
349     checker = ExecutionChecker(args)
350
351     sys.stdout.write('[')
352     result.visit(checker)
353     sys.stdout.write('{}]')
354     sys.stdout.close()
355     sys.stdout = old_sys_stdout
356
357     with open(args.output + '.json', 'r') as json_file:
358         data = json.load(json_file)
359     data.pop(-1)
360
361     if args.formatting == 'rst':
362         do_rst(data, args)
363     elif args.formatting == 'wiki':
364         do_wiki(data, args)
365     elif args.formatting == 'html':
366         do_html(data, args)
367     elif args.formatting == 'md':
368         do_md(data, args)
369
370
371 def parse_args():
372     """Parse arguments from cmd line.
373
374     :return: Parsed arguments.
375     :rtype ArgumentParser
376     """
377
378     parser = argparse.ArgumentParser(description=__doc__,
379                                      formatter_class=argparse.
380                                      RawDescriptionHelpFormatter)
381     parser.add_argument("-i", "--input",
382                         required=True,
383                         type=argparse.FileType('r'),
384                         help="Robot XML log file")
385     parser.add_argument("-o", "--output",
386                         type=str,
387                         required=True,
388                         help="Output file")
389     parser.add_argument("-f", "--formatting",
390                         required=True,
391                         choices=['html', 'wiki', 'rst', 'md'],
392                         help="Output file format")
393     parser.add_argument("-s", "--start",
394                         type=int,
395                         default=1,
396                         help="The first level to be taken from xml file")
397     parser.add_argument("-l", "--level",
398                         type=int,
399                         default=1,
400                         help="The level of the first chapter in generated file")
401
402     return parser.parse_args()
403
404
405 if __name__ == "__main__":
406     sys.exit(process_robot_file(parse_args()))