bf0ef6f8d5dbaab8db8d81b63fbf85ca2e1fd187
[csit.git] / resources / tools / report_gen / run_robot_teardown_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, VAT command history or table from Show
18 Runtime command) from robot framework output file (output.xml) and prints in
19 specified format (wiki, 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_teardown_data.py -i "output.xml" -o "tests.rst" -d "VAT_H" -f "rst"
32 -s 3 -l 2
33
34 The example reads the VAT command history data from "output.xml", writes
35 the output to "tests.rst" in rst format. It will start on the 3rd level of xml
36 structure and the generated document hierarchy will start on the 2nd level.
37
38 :Example:
39
40 run_robot_teardown_data.py -i "output.xml" -o "tests.rst" -f "rst" -d "SH_RUN"
41  -r "(.*)(lisp)(.*)"
42
43 The example reads the data from "output.xml", writes the output to "tests.rst"
44 in rst format. It will start on the 1st level of xml structure and the generated
45 document hierarchy will start on the 1st level (default values).
46 Only the test suites which match the given regular expression are processed.
47 """
48
49 import argparse
50 import re
51 import sys
52 import json
53 import string
54
55 from robot.api import ExecutionResult, ResultVisitor
56
57
58 class ExecutionChecker(ResultVisitor):
59     """Class to traverse through the test suite structure.
60
61     The functionality implemented in this class generates a json file. Its
62     structure is:
63
64     [
65         {
66             "level": "Level of the suite, type: str",
67             "title": "Title of the suite, type: str",
68             "doc": "Documentation of the suite, type: str",
69             "table": [
70                 ["TC name", "VAT history or show runtime"],
71                 ["TC name", "VAT history or show runtime"],
72                 ... other test cases ...
73                 ["Name","VAT command history or VPP operational data"]
74             ]
75         },
76         ... other test suites ...
77     ]
78
79     .. note:: The header of the table with TCs is at the end of the table.
80     """
81
82     def __init__(self, args):
83         self.formatting = args.formatting
84         self.data = args.data
85         if self.data == "VAT_H":
86             self.lookup_kw = "Show Vat History On All Duts"
87             self.column_name = "VAT command history"
88         elif self.data == "SH_RUN":
89             self.lookup_kw = "Vpp Show Runtime"
90             self.column_name = "VPP operational data"
91         else:
92             raise ValueError("{0} look-up not implemented.".format(self.data))
93         self.lookup_kw_nr = 0
94         self.lookup_msg_nr = 0
95
96     def visit_suite(self, suite):
97         """Implements traversing through the suite and its direct children.
98
99         :param suite: Suite to process.
100         :type suite: Suite
101         :returns: Nothing.
102         """
103
104         if self.start_suite(suite) is not False:
105             if suite.tests:
106                 sys.stdout.write(',"tests":[')
107             else:
108                 sys.stdout.write('},')
109
110             suite.suites.visit(self)
111             suite.tests.visit(self)
112
113             if suite.tests:
114                 hdr = '["Name","' + self.column_name + '"]'
115                 sys.stdout.write(hdr + ']},')
116
117             self.end_suite(suite)
118
119     def start_suite(self, suite):
120         """Called when suite starts.
121
122         :param suite: Suite to process.
123         :type suite: Suite
124         :returns: Nothing.
125         """
126
127         level = len(suite.longname.split("."))
128         sys.stdout.write('{')
129         sys.stdout.write('"level":"' + str(level) + '",')
130         sys.stdout.write('"title":"' + suite.name.replace('"', "'") + '",')
131         sys.stdout.write('"doc":"' + suite.doc.replace('"', "'").
132                          replace('\n', ' ').replace('\r', '').
133                          replace('*[', ' |br| *[') + '"')
134
135     def end_suite(self, suite):
136         """Called when suite ends.
137
138         :param suite: Suite to process.
139         :type suite: Suite
140         :returns: Nothing.
141         """
142         pass
143
144     def visit_test(self, test):
145         """Implements traversing through the test.
146
147         :param test: Test to process.
148         :type test: Test
149         :returns: Nothing.
150         """
151         if self.start_test(test) is not False:
152             test.keywords.visit(self)
153             self.end_test(test)
154
155     def start_test(self, test):
156         """Called when test starts.
157
158         :param test: Test to process.
159         :type test: Test
160         :returns: Nothing.
161         """
162
163         name = test.name.replace('"', "'")
164         sys.stdout.write('["' + name + '","')
165
166     def end_test(self, test):
167         """Called when test ends.
168
169         :param test: Test to process.
170         :type test: Test
171         :returns: Nothing.
172         """
173         sys.stdout.write('"],')
174
175     def visit_keyword(self, kw):
176         """Implements traversing through the keyword and its child keywords.
177
178         :param kw: Keyword to process.
179         :type kw: Keyword
180         :returns: Nothing.
181         """
182         if self.start_keyword(kw) is not False:
183             self.end_keyword(kw)
184
185     def start_keyword(self, kw):
186         """Called when keyword starts. Default implementation does nothing.
187
188         :param kw: Keyword to process.
189         :type kw: Keyword
190         :returns: Nothing.
191         """
192         try:
193             if kw.type == "teardown":
194                 self.lookup_kw_nr = 0
195                 self.visit_teardown_kw(kw)
196         except AttributeError:
197             pass
198
199     def end_keyword(self, kw):
200         """Called when keyword ends. Default implementation does nothing.
201
202         :param kw: Keyword to process.
203         :type kw: Keyword
204         :returns: Nothing.
205         """
206         pass
207
208     def visit_teardown_kw(self, kw):
209         """Implements traversing through the teardown keyword and its child
210         keywords.
211
212         :param kw: Keyword to process.
213         :type kw: Keyword
214         :returns: Nothing.
215         """
216         for keyword in kw.keywords:
217             if self.start_teardown_kw(keyword) is not False:
218                 self.visit_teardown_kw(keyword)
219                 self.end_teardown_kw(keyword)
220
221     def start_teardown_kw(self, kw):
222         """Called when teardown keyword starts. Default implementation does
223         nothing.
224
225         :param kw: Keyword to process.
226         :type kw: Keyword
227         :returns: Nothing.
228         """
229         if kw.name.count(self.lookup_kw):
230             self.lookup_kw_nr += 1
231             self.lookup_msg_nr = 0
232             kw.messages.visit(self)
233
234     def end_teardown_kw(self, kw):
235         """Called when keyword ends. Default implementation does nothing.
236
237         :param kw: Keyword to process.
238         :type kw: Keyword
239         :returns: Nothing.
240         """
241         pass
242
243     def visit_message(self, msg):
244         """Implements visiting the message.
245
246         :param msg: Message to process.
247         :type msg: Message
248         :returns: Nothing.
249         """
250         if self.start_message(msg) is not False:
251             self.end_message(msg)
252
253     def start_message(self, msg):
254         """Called when message starts. Default implementation does nothing.
255
256         :param msg: Message to process.
257         :type msg: Message
258         :returns: Nothing.
259         """
260         if self.data == "VAT_H":
261             self.vat_history(msg)
262         elif self.data == "SH_RUN":
263             self.show_run(msg)
264
265     def end_message(self, msg):
266         """Called when message ends. Default implementation does nothing.
267
268         :param msg: Message to process.
269         :type msg: Message
270         :returns: Nothing.
271         """
272         pass
273
274     def vat_history(self, msg):
275         """Called when extraction of VAT command history is required.
276
277         :param msg: Message to process.
278         :type msg: Message
279         :returns: Nothing.
280         """
281         if msg.message.count("VAT command history:"):
282             self.lookup_msg_nr += 1
283             text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
284                           "VAT command history:", "", msg.message, count=1).\
285                 replace('\n', ' |br| ').replace('\r', '').replace('"', "'")
286             sys.stdout.write("*DUT" + str(self.lookup_msg_nr) + ":*" + text)
287
288     def show_run(self, msg):
289         """Called when extraction of VPP operational data (output of CLI command
290         Show Runtime) is required.
291
292         :param msg: Message to process.
293         :type msg: Message
294         :returns: Nothing.
295         """
296         if msg.message.count("vat# Thread "):
297             self.lookup_msg_nr += 1
298             text = msg.message.replace("vat# ", "").\
299                 replace("return STDOUT ", "").replace('\n', ' |br| ').\
300                 replace('\r', '').replace('"', "'")
301             if self.lookup_msg_nr == 1:
302                 sys.stdout.write("*DUT" + str(self.lookup_kw_nr) +
303                                  ":* |br| " + text)
304
305
306 def do_html(data, args):
307     """Generation of a html file from json data.
308
309     :param data: List of suites from json file.
310     :param args: Parsed arguments.
311     :type data: list of dict
312     :type args: ArgumentParser
313     :returns: Nothing.
314     """
315
316     shift = int(args.level)
317     start = int(args.start)
318
319     output = open(args.output, 'w')
320
321     output.write('<html>')
322     for item in data:
323         if int(item['level']) < start:
324             continue
325         level = str(int(item['level']) - start + shift)
326         output.write('<h' + level + '>' + item['title'].lower() +
327                      '</h' + level + '>')
328         output.write('<p>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", item['doc'],
329                                     0, flags=re.MULTILINE).
330                      replace(' |br| ', '<br>') + '</p>')
331         try:
332             output.write(gen_html_table(item['tests']))
333         except KeyError:
334             continue
335     output.write('</html>')
336     output.close()
337
338
339 def gen_html_table(data):
340     """Generates a table with TCs' names and VAT command histories / VPP
341     operational data in html format. There is no css used.
342
343     :param data: Json data representing a table with TCs.
344     :type data: str
345     :returns: Table with TCs' names and VAT command histories / VPP operational
346     data in html format.
347     :rtype: str
348     """
349
350     table = '<table width=100% border=1><tr>'
351     table += '<th width=30%>' + data[-1][0] + '</th>'
352     table += '<th width=70%>' + data[-1][1] + '</th></tr>'
353
354     for item in data[0:-1]:
355         table += '<tr>'
356         for element in item:
357             table += '<td>' + re.sub(r"(\*)(.*?)(\*)", r"<b>\2</b>", element,
358                                      0, flags=re.MULTILINE).\
359                 replace(' |br| ', '<br>') + '</td>'
360     table += '</tr></table>'
361
362     return table
363
364
365 def do_rst(data, args):
366     """Generation of a rst file from json data.
367
368     :param data: List of suites from json file.
369     :param args: Parsed arguments.
370     :type data: list of dict
371     :type args: ArgumentParser
372     :returns: Nothing.
373     """
374
375     hdrs = ['=', '-', '`', "'", '.', '~', '*', '+', '^']
376     shift = int(args.level)
377     start = int(args.start)
378
379     output = open(args.output, 'w')
380     output.write('\n.. |br| raw:: html\n\n    <br />\n\n')
381
382     if args.title:
383         output.write(args.title + '\n' +
384                      hdrs[shift - 1] *
385                      len(args.title) + '\n\n')
386
387     for item in data:
388         if int(item['level']) < start:
389             continue
390         if 'ndrchk' in item['title'].lower():
391             continue
392         output.write(item['title'].lower() + '\n' +
393                      hdrs[int(item['level']) - start + shift] *
394                      len(item['title']) + '\n\n')
395         output.write(item['doc'].replace('*', '**').replace('|br|', '\n\n -') +
396                      '\n\n')
397         try:
398             output.write(gen_rst_table(item['tests']) + '\n\n')
399         except KeyError:
400             continue
401     output.close()
402
403
404 def gen_rst_table(data):
405     """Generates a table with TCs' names and VAT command histories / VPP
406     operational data in rst format.
407
408     :param data: Json data representing a table with TCs.
409     :type data: str
410     :returns: Table with TCs' names and VAT command histories / VPP operational
411     data in rst format.
412     :rtype: str
413     """
414
415     table = []
416     # max size of each column
417     lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
418
419     start_of_line = '| '
420     vert_separator = ' | '
421     end_of_line = ' |'
422     line_marker = '-'
423
424     meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
425                                          for i in range(len(lengths))])
426     template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
427                                   end_of_line)
428     # determine top/bottom borders
429     to_separator = string.maketrans('| ', '+-')
430     start_of_line = start_of_line.translate(to_separator)
431     vert_separator = vert_separator.translate(to_separator)
432     end_of_line = end_of_line.translate(to_separator)
433     separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
434                                    join([x * line_marker for x in lengths]),
435                                    end_of_line)
436     # determine header separator
437     th_separator_tr = string.maketrans('-', '=')
438     start_of_line = start_of_line.translate(th_separator_tr)
439     line_marker = line_marker.translate(th_separator_tr)
440     vertical_separator = vert_separator.translate(th_separator_tr)
441     end_of_line = end_of_line.translate(th_separator_tr)
442     th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
443                                       join([x * line_marker for x in lengths]),
444                                       end_of_line)
445     # prepare table
446     table.append(separator)
447     # set table header
448     titles = data[-1]
449     table.append(template.format(*titles))
450     table.append(th_separator)
451     # generate table rows
452     for item in data[0:-2]:
453         table.append(template.format(item[0], item[1].replace('*', '**')))
454         table.append(separator)
455     table.append(template.format(data[-2][0], data[-2][1].replace('*', '**')))
456     table.append(separator)
457     return '\n'.join(table)
458
459
460 def do_md(data, args):
461     """Generation of a rst file from json data.
462
463     :param data: List of suites from json file.
464     :param args: Parsed arguments.
465     :type data: list of dict
466     :type args: ArgumentParser
467     :returns: Nothing.
468     """
469     raise NotImplementedError("Export to 'md' format is not implemented.")
470
471
472 def do_wiki(data, args):
473     """Generation of a wiki page from json data.
474
475     :param data: List of suites from json file.
476     :param args: Parsed arguments.
477     :type data: list of dict
478     :type args: ArgumentParser
479     :returns: Nothing.
480     """
481
482     shift = int(args.level)
483     start = int(args.start)
484
485     output = open(args.output, 'w')
486
487     for item in data:
488         if int(item['level']) < start:
489             continue
490         if 'ndrchk' in item['title'].lower():
491             continue
492         mark = "=" * (int(item['level']) - start + shift) + ' '
493         output.write(mark + item['title'].lower() + mark + '\n')
494         try:
495             output.write(gen_wiki_table(item['tests'], mark) +
496                          '\n\n')
497         except KeyError:
498             continue
499     output.close()
500
501
502 def gen_wiki_table(data, mark):
503     """Generates a table with TCs' names and VAT command histories / VPP
504     operational data in wiki format.
505
506     :param data: Json data representing a table with TCs.
507     :type data: str
508     :returns: Table with TCs' names and VAT command histories / VPP operational
509     data in wiki format.
510     :rtype: str
511     """
512
513     table = '{| class="wikitable"\n'
514     header = ""
515     mark = mark[0:-2] + "= "
516     for item in data[-1]:
517         header += '!{}\n'.format(item)
518     table += header
519     for item in data[0:-1]:
520         msg = item[1].replace('*', mark).replace(' |br| ', '\n\n')
521         table += '|-\n|{}\n|{}\n'.format(item[0], msg)
522     table += '|}\n'
523
524     return table
525
526
527 def process_robot_file(args):
528     """Process data from robot output.xml file and generate defined file type.
529
530     :param args: Parsed arguments.
531     :type args: ArgumentParser
532     :return: Nothing.
533     """
534
535     old_sys_stdout = sys.stdout
536     sys.stdout = open(args.output + '.json', 'w')
537
538     result = ExecutionResult(args.input)
539     checker = ExecutionChecker(args)
540
541     sys.stdout.write('[')
542     result.visit(checker)
543     sys.stdout.write('{}]')
544     sys.stdout.close()
545     sys.stdout = old_sys_stdout
546
547     with open(args.output + '.json', 'r') as json_file:
548         data = json.load(json_file)
549     data.pop(-1)
550
551     if args.regex:
552         results = list()
553         regex = re.compile(args.regex)
554         for item in data:
555             if re.search(regex, item['title'].lower()):
556                 results.append(item)
557     else:
558         results = data
559
560     if args.formatting == 'rst':
561         do_rst(results, args)
562     elif args.formatting == 'wiki':
563         do_wiki(results, args)
564     elif args.formatting == 'html':
565         do_html(results, args)
566     elif args.formatting == 'md':
567         do_md(results, args)
568
569
570 def parse_args():
571     """Parse arguments from cmd line.
572
573     :return: Parsed arguments.
574     :rtype ArgumentParser
575     """
576
577     parser = argparse.ArgumentParser(description=__doc__,
578                                      formatter_class=argparse.
579                                      RawDescriptionHelpFormatter)
580     parser.add_argument("-i", "--input",
581                         required=True,
582                         type=argparse.FileType('r'),
583                         help="Robot XML log file")
584     parser.add_argument("-o", "--output",
585                         type=str,
586                         required=True,
587                         help="Output file")
588     parser.add_argument("-d", "--data",
589                         type=str,
590                         required=True,
591                         help="Required data: VAT_H (VAT history), SH_RUN "
592                              "(show runtime output)")
593     parser.add_argument("-f", "--formatting",
594                         required=True,
595                         choices=['html', 'wiki', 'rst', 'md'],
596                         help="Output file format")
597     parser.add_argument("-s", "--start",
598                         type=int,
599                         default=1,
600                         help="The first level to be taken from xml file")
601     parser.add_argument("-l", "--level",
602                         type=int,
603                         default=1,
604                         help="The level of the first chapter in generated file")
605     parser.add_argument("-r", "--regex",
606                         type=str,
607                         default=None,
608                         help="Regular expression used to select test suites. "
609                              "If None, all test suites are selected.")
610     parser.add_argument("-t", "--title",
611                         type=str,
612                         default=None,
613                         help="Title of the output.")
614
615     return parser.parse_args()
616
617
618 if __name__ == "__main__":
619     sys.exit(process_robot_file(parse_args()))