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