cf0fc8941867336e5dd36686aa3dbcbc65f69059
[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             test_set = list()
399             for test in item['tests']:
400                 test_data = list()
401                 test_data.append(test[0])
402                 test_data.append(test[1].replace('*', '**'))
403                 test_set.append(test_data)
404             output.write(gen_rst_table(test_set) + '\n\n')
405         except KeyError:
406             continue
407     output.close()
408
409
410 def gen_rst_table(data):
411     """Generates a table with TCs' names and VAT command histories / VPP
412     operational data in rst format.
413
414     :param data: Json data representing a table with TCs.
415     :type data: str
416     :returns: Table with TCs' names and VAT command histories / VPP operational
417     data in rst format.
418     :rtype: str
419     """
420
421     table = []
422     # max size of each column
423     lengths = map(max, zip(*[[len(str(elt)) for elt in item] for item in data]))
424
425     start_of_line = '| '
426     vert_separator = ' | '
427     end_of_line = ' |'
428     line_marker = '-'
429
430     meta_template = vert_separator.join(['{{{{{0}:{{{0}}}}}}}'.format(i)
431                                          for i in range(len(lengths))])
432     template = '{0}{1}{2}'.format(start_of_line, meta_template.format(*lengths),
433                                   end_of_line)
434     # determine top/bottom borders
435     to_separator = string.maketrans('| ', '+-')
436     start_of_line = start_of_line.translate(to_separator)
437     vert_separator = vert_separator.translate(to_separator)
438     end_of_line = end_of_line.translate(to_separator)
439     separator = '{0}{1}{2}'.format(start_of_line, vert_separator.
440                                    join([x * line_marker for x in lengths]),
441                                    end_of_line)
442     # determine header separator
443     th_separator_tr = string.maketrans('-', '=')
444     start_of_line = start_of_line.translate(th_separator_tr)
445     line_marker = line_marker.translate(th_separator_tr)
446     vertical_separator = vert_separator.translate(th_separator_tr)
447     end_of_line = end_of_line.translate(th_separator_tr)
448     th_separator = '{0}{1}{2}'.format(start_of_line, vertical_separator.
449                                       join([x * line_marker for x in lengths]),
450                                       end_of_line)
451     # prepare table
452     table.append(separator)
453     # set table header
454     titles = data[-1]
455     table.append(template.format(*titles))
456     table.append(th_separator)
457     # generate table rows
458     for item in data[0:-2]:
459         table.append(template.format(item[0], item[1]))
460         table.append(separator)
461     table.append(template.format(data[-2][0], data[-2][1]))
462     table.append(separator)
463     return '\n'.join(table)
464
465
466 def do_md(data, args):
467     """Generation of a rst file from json data.
468
469     :param data: List of suites from json file.
470     :param args: Parsed arguments.
471     :type data: list of dict
472     :type args: ArgumentParser
473     :returns: Nothing.
474     """
475     raise NotImplementedError("Export to 'md' format is not implemented.")
476
477
478 def do_wiki(data, args):
479     """Generation of a wiki page from json data.
480
481     :param data: List of suites from json file.
482     :param args: Parsed arguments.
483     :type data: list of dict
484     :type args: ArgumentParser
485     :returns: Nothing.
486     """
487
488     shift = int(args.level)
489     start = int(args.start)
490
491     output = open(args.output, 'w')
492
493     for item in data:
494         if int(item['level']) < start:
495             continue
496         if 'ndrchk' in item['title'].lower():
497             continue
498         mark = "=" * (int(item['level']) - start + shift) + ' '
499         output.write(mark + item['title'].lower() + mark + '\n')
500         try:
501             output.write(gen_wiki_table(item['tests'], mark) +
502                          '\n\n')
503         except KeyError:
504             continue
505     output.close()
506
507
508 def gen_wiki_table(data, mark):
509     """Generates a table with TCs' names and VAT command histories / VPP
510     operational data in wiki format.
511
512     :param data: Json data representing a table with TCs.
513     :type data: str
514     :returns: Table with TCs' names and VAT command histories / VPP operational
515     data in wiki format.
516     :rtype: str
517     """
518
519     table = '{| class="wikitable"\n'
520     header = ""
521     mark = mark[0:-2] + "= "
522     for item in data[-1]:
523         header += '!{}\n'.format(item)
524     table += header
525     for item in data[0:-1]:
526         msg = item[1].replace('*', mark).replace(' |br| ', '\n\n')
527         table += '|-\n|{}\n|{}\n'.format(item[0], msg)
528     table += '|}\n'
529
530     return table
531
532
533 def process_robot_file(args):
534     """Process data from robot output.xml file and generate defined file type.
535
536     :param args: Parsed arguments.
537     :type args: ArgumentParser
538     :return: Nothing.
539     """
540
541     old_sys_stdout = sys.stdout
542     sys.stdout = open(args.output + '.json', 'w')
543
544     result = ExecutionResult(args.input)
545     checker = ExecutionChecker(args)
546
547     sys.stdout.write('[')
548     result.visit(checker)
549     sys.stdout.write('{}]')
550     sys.stdout.close()
551     sys.stdout = old_sys_stdout
552
553     with open(args.output + '.json', 'r') as json_file:
554         data = json.load(json_file)
555     data.pop(-1)
556
557     if args.regex:
558         results = list()
559         regex = re.compile(args.regex)
560         for item in data:
561             if re.search(regex, item['title'].lower()):
562                 results.append(item)
563     else:
564         results = data
565
566     if args.formatting == 'rst':
567         do_rst(results, args)
568     elif args.formatting == 'wiki':
569         do_wiki(results, args)
570     elif args.formatting == 'html':
571         do_html(results, args)
572     elif args.formatting == 'md':
573         do_md(results, args)
574
575
576 def parse_args():
577     """Parse arguments from cmd line.
578
579     :return: Parsed arguments.
580     :rtype ArgumentParser
581     """
582
583     parser = argparse.ArgumentParser(description=__doc__,
584                                      formatter_class=argparse.
585                                      RawDescriptionHelpFormatter)
586     parser.add_argument("-i", "--input",
587                         required=True,
588                         type=argparse.FileType('r'),
589                         help="Robot XML log file")
590     parser.add_argument("-o", "--output",
591                         type=str,
592                         required=True,
593                         help="Output file")
594     parser.add_argument("-d", "--data",
595                         type=str,
596                         required=True,
597                         help="Required data: VAT_H (VAT history), SH_RUN "
598                              "(show runtime output)")
599     parser.add_argument("-f", "--formatting",
600                         required=True,
601                         choices=['html', 'wiki', 'rst', 'md'],
602                         help="Output file format")
603     parser.add_argument("-s", "--start",
604                         type=int,
605                         default=1,
606                         help="The first level to be taken from xml file")
607     parser.add_argument("-l", "--level",
608                         type=int,
609                         default=1,
610                         help="The level of the first chapter in generated file")
611     parser.add_argument("-r", "--regex",
612                         type=str,
613                         default=None,
614                         help="Regular expression used to select test suites. "
615                              "If None, all test suites are selected.")
616     parser.add_argument("-t", "--title",
617                         type=str,
618                         default=None,
619                         help="Title of the output.")
620
621     return parser.parse_args()
622
623
624 if __name__ == "__main__":
625     sys.exit(process_robot_file(parse_args()))