CSIT-944: PAL Extensions: Mapping table
[csit.git] / resources / tools / presentation / input_data_parser.py
1 # Copyright (c) 2018 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Data pre-processing
15
16 - extract data from output.xml files generated by Jenkins jobs and store in
17   pandas' Series,
18 - provide access to the data.
19 - filter the data using tags,
20 """
21
22 import multiprocessing
23 import os
24 import re
25 import pandas as pd
26 import logging
27
28 from robot.api import ExecutionResult, ResultVisitor
29 from robot import errors
30 from collections import OrderedDict
31 from string import replace
32 from os import remove
33 from jumpavg.AvgStdevMetadataFactory import AvgStdevMetadataFactory
34
35 from input_data_files import download_and_unzip_data_file
36 from utils import Worker
37
38
39 class ExecutionChecker(ResultVisitor):
40     """Class to traverse through the test suite structure.
41
42     The functionality implemented in this class generates a json structure:
43
44     Performance tests:
45
46     {
47         "metadata": {
48             "generated": "Timestamp",
49             "version": "SUT version",
50             "job": "Jenkins job name",
51             "build": "Information about the build"
52         },
53         "suites": {
54             "Suite long name 1": {
55                 "name": Suite name,
56                 "doc": "Suite 1 documentation",
57                 "parent": "Suite 1 parent",
58                 "level": "Level of the suite in the suite hierarchy"
59             }
60             "Suite long name N": {
61                 "name": Suite name,
62                 "doc": "Suite N documentation",
63                 "parent": "Suite 2 parent",
64                 "level": "Level of the suite in the suite hierarchy"
65             }
66         }
67         "tests": {
68             # NDRPDR tests:
69             "ID": {
70                 "name": "Test name",
71                 "parent": "Name of the parent of the test",
72                 "doc": "Test documentation",
73                 "msg": "Test message",
74                 "vat-history": "DUT1 and DUT2 VAT History",
75                 "show-run": "Show Run",
76                 "tags": ["tag 1", "tag 2", "tag n"],
77                 "type": "NDRPDR",
78                 "status": "PASS" | "FAIL",
79                 "throughput": {
80                     "NDR": {
81                         "LOWER": float,
82                         "UPPER": float
83                     },
84                     "PDR": {
85                         "LOWER": float,
86                         "UPPER": float
87                     }
88                 },
89                 "latency": {
90                     "NDR": {
91                         "direction1": {
92                             "min": float,
93                             "avg": float,
94                             "max": float
95                         },
96                         "direction2": {
97                             "min": float,
98                             "avg": float,
99                             "max": float
100                         }
101                     },
102                     "PDR": {
103                         "direction1": {
104                             "min": float,
105                             "avg": float,
106                             "max": float
107                         },
108                         "direction2": {
109                             "min": float,
110                             "avg": float,
111                             "max": float
112                         }
113                     }
114                 }
115             }
116
117             # TCP tests:
118             "ID": {
119                 "name": "Test name",
120                 "parent": "Name of the parent of the test",
121                 "doc": "Test documentation",
122                 "msg": "Test message",
123                 "tags": ["tag 1", "tag 2", "tag n"],
124                 "type": "TCP",
125                 "status": "PASS" | "FAIL",
126                 "result": int
127             }
128
129             # MRR, BMRR tests:
130             "ID": {
131                 "name": "Test name",
132                 "parent": "Name of the parent of the test",
133                 "doc": "Test documentation",
134                 "msg": "Test message",
135                 "tags": ["tag 1", "tag 2", "tag n"],
136                 "type": "MRR" | "BMRR",
137                 "status": "PASS" | "FAIL",
138                 "result": {
139                     "receive-rate": AvgStdevMetadata,
140                 }
141             }
142
143             # TODO: Remove when definitely no NDRPDRDISC tests are used:
144             # NDRPDRDISC tests:
145             "ID": {
146                 "name": "Test name",
147                 "parent": "Name of the parent of the test",
148                 "doc": "Test documentation",
149                 "msg": "Test message",
150                 "tags": ["tag 1", "tag 2", "tag n"],
151                 "type": "PDR" | "NDR",
152                 "status": "PASS" | "FAIL",
153                 "throughput": {  # Only type: "PDR" | "NDR"
154                     "value": int,
155                     "unit": "pps" | "bps" | "percentage"
156                 },
157                 "latency": {  # Only type: "PDR" | "NDR"
158                     "direction1": {
159                         "100": {
160                             "min": int,
161                             "avg": int,
162                             "max": int
163                         },
164                         "50": {  # Only for NDR
165                             "min": int,
166                             "avg": int,
167                             "max": int
168                         },
169                         "10": {  # Only for NDR
170                             "min": int,
171                             "avg": int,
172                             "max": int
173                         }
174                     },
175                     "direction2": {
176                         "100": {
177                             "min": int,
178                             "avg": int,
179                             "max": int
180                         },
181                         "50": {  # Only for NDR
182                             "min": int,
183                             "avg": int,
184                             "max": int
185                         },
186                         "10": {  # Only for NDR
187                             "min": int,
188                             "avg": int,
189                             "max": int
190                         }
191                     }
192                 },
193                 "lossTolerance": "lossTolerance",  # Only type: "PDR"
194                 "vat-history": "DUT1 and DUT2 VAT History"
195                 "show-run": "Show Run"
196             },
197             "ID" {
198                 # next test
199             }
200         }
201     }
202
203
204     Functional tests:
205
206     {
207         "metadata": {  # Optional
208             "version": "VPP version",
209             "job": "Jenkins job name",
210             "build": "Information about the build"
211         },
212         "suites": {
213             "Suite name 1": {
214                 "doc": "Suite 1 documentation",
215                 "parent": "Suite 1 parent",
216                 "level": "Level of the suite in the suite hierarchy"
217             }
218             "Suite name N": {
219                 "doc": "Suite N documentation",
220                 "parent": "Suite 2 parent",
221                 "level": "Level of the suite in the suite hierarchy"
222             }
223         }
224         "tests": {
225             "ID": {
226                 "name": "Test name",
227                 "parent": "Name of the parent of the test",
228                 "doc": "Test documentation"
229                 "msg": "Test message"
230                 "tags": ["tag 1", "tag 2", "tag n"],
231                 "vat-history": "DUT1 and DUT2 VAT History"
232                 "show-run": "Show Run"
233                 "status": "PASS" | "FAIL"
234             },
235             "ID" {
236                 # next test
237             }
238         }
239     }
240
241     .. note:: ID is the lowercase full path to the test.
242     """
243
244     # TODO: Remove when definitely no NDRPDRDISC tests are used:
245     REGEX_RATE = re.compile(r'^[\D\d]*FINAL_RATE:\s(\d+\.\d+)\s(\w+)')
246
247     REGEX_NDRPDR_RATE = re.compile(r'NDR_LOWER:\s(\d+.\d+).*\n.*\n'
248                                    r'NDR_UPPER:\s(\d+.\d+).*\n'
249                                    r'PDR_LOWER:\s(\d+.\d+).*\n.*\n'
250                                    r'PDR_UPPER:\s(\d+.\d+)')
251
252     # TODO: Remove when definitely no NDRPDRDISC tests are used:
253     REGEX_LAT_NDR = re.compile(r'^[\D\d]*'
254                                r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
255                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
256                                r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
257                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
258                                r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
259                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]')
260
261     REGEX_LAT_PDR = re.compile(r'^[\D\d]*'
262                                r'LAT_\d+%PDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
263                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\][\D\d]*')
264
265     REGEX_NDRPDR_LAT = re.compile(r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
266                                   r'LATENCY.*\[\'(.*)\', \'(.*)\'\]')
267
268     REGEX_TOLERANCE = re.compile(r'^[\D\d]*LOSS_ACCEPTANCE:\s(\d*\.\d*)\s'
269                                  r'[\D\d]*')
270
271     REGEX_VERSION_VPP = re.compile(r"(return STDOUT Version:\s*)(.*)")
272
273     REGEX_VERSION_DPDK = re.compile(r"(return STDOUT testpmd)([\d\D\n]*)"
274                                     r"(RTE Version: 'DPDK )(.*)(')")
275
276     REGEX_TCP = re.compile(r'Total\s(rps|cps|throughput):\s([0-9]*).*$')
277
278     REGEX_MRR = re.compile(r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
279                            r'tx\s(\d*),\srx\s(\d*)')
280
281     REGEX_BMRR = re.compile(r'Maximum Receive Rate trial results'
282                             r' in packets per second: \[(.*)\]')
283
284     REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
285
286     REGEX_TC_NAME_OLD = re.compile(r'-\d+[tT]\d+[cC]-')
287
288     REGEX_TC_NAME_NEW = re.compile(r'-\d+[cC]-')
289
290     REGEX_TC_NUMBER = re.compile(r'tc[0-9]{2}-')
291
292     def __init__(self, metadata, mapping, ignore):
293         """Initialisation.
294
295         :param metadata: Key-value pairs to be included in "metadata" part of
296             JSON structure.
297         :param mapping: Mapping of the old names of test cases to the new
298             (actual) one.
299         :param ignore: List of TCs to be ignored.
300         :type metadata: dict
301         :type mapping: dict
302         :type ignore: list
303         """
304
305         # Type of message to parse out from the test messages
306         self._msg_type = None
307
308         # VPP version
309         self._version = None
310
311         # Timestamp
312         self._timestamp = None
313
314         # Mapping of TCs long names
315         self._mapping = mapping
316
317         # Ignore list
318         self._ignore = ignore
319
320         # Number of VAT History messages found:
321         # 0 - no message
322         # 1 - VAT History of DUT1
323         # 2 - VAT History of DUT2
324         self._lookup_kw_nr = 0
325         self._vat_history_lookup_nr = 0
326
327         # Number of Show Running messages found
328         # 0 - no message
329         # 1 - Show run message found
330         self._show_run_lookup_nr = 0
331
332         # Test ID of currently processed test- the lowercase full path to the
333         # test
334         self._test_ID = None
335
336         # The main data structure
337         self._data = {
338             "metadata": OrderedDict(),
339             "suites": OrderedDict(),
340             "tests": OrderedDict()
341         }
342
343         # Save the provided metadata
344         for key, val in metadata.items():
345             self._data["metadata"][key] = val
346
347         # Dictionary defining the methods used to parse different types of
348         # messages
349         self.parse_msg = {
350             "timestamp": self._get_timestamp,
351             "vpp-version": self._get_vpp_version,
352             "dpdk-version": self._get_dpdk_version,
353             "teardown-vat-history": self._get_vat_history,
354             "test-show-runtime": self._get_show_run
355         }
356
357     @property
358     def data(self):
359         """Getter - Data parsed from the XML file.
360
361         :returns: Data parsed from the XML file.
362         :rtype: dict
363         """
364         return self._data
365
366     def _get_vpp_version(self, msg):
367         """Called when extraction of VPP version is required.
368
369         :param msg: Message to process.
370         :type msg: Message
371         :returns: Nothing.
372         """
373
374         if msg.message.count("return STDOUT Version:"):
375             self._version = str(re.search(self.REGEX_VERSION_VPP, msg.message).
376                                 group(2))
377             self._data["metadata"]["version"] = self._version
378             self._msg_type = None
379
380     def _get_dpdk_version(self, msg):
381         """Called when extraction of DPDK version is required.
382
383         :param msg: Message to process.
384         :type msg: Message
385         :returns: Nothing.
386         """
387
388         if msg.message.count("return STDOUT testpmd"):
389             try:
390                 self._version = str(re.search(
391                     self.REGEX_VERSION_DPDK, msg.message). group(4))
392                 self._data["metadata"]["version"] = self._version
393             except IndexError:
394                 pass
395             finally:
396                 self._msg_type = None
397
398     def _get_timestamp(self, msg):
399         """Called when extraction of timestamp is required.
400
401         :param msg: Message to process.
402         :type msg: Message
403         :returns: Nothing.
404         """
405
406         self._timestamp = msg.timestamp[:14]
407         self._data["metadata"]["generated"] = self._timestamp
408         self._msg_type = None
409
410     def _get_vat_history(self, msg):
411         """Called when extraction of VAT command history is required.
412
413         :param msg: Message to process.
414         :type msg: Message
415         :returns: Nothing.
416         """
417         if msg.message.count("VAT command history:"):
418             self._vat_history_lookup_nr += 1
419             if self._vat_history_lookup_nr == 1:
420                 self._data["tests"][self._test_ID]["vat-history"] = str()
421             else:
422                 self._msg_type = None
423             text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
424                           "VAT command history:", "", msg.message, count=1). \
425                 replace("\n\n", "\n").replace('\n', ' |br| ').\
426                 replace('\r', '').replace('"', "'")
427
428             self._data["tests"][self._test_ID]["vat-history"] += " |br| "
429             self._data["tests"][self._test_ID]["vat-history"] += \
430                 "**DUT" + str(self._vat_history_lookup_nr) + ":** " + text
431
432     def _get_show_run(self, msg):
433         """Called when extraction of VPP operational data (output of CLI command
434         Show Runtime) is required.
435
436         :param msg: Message to process.
437         :type msg: Message
438         :returns: Nothing.
439         """
440         if msg.message.count("return STDOUT Thread "):
441             self._show_run_lookup_nr += 1
442             if self._lookup_kw_nr == 1 and self._show_run_lookup_nr == 1:
443                 self._data["tests"][self._test_ID]["show-run"] = str()
444             if self._lookup_kw_nr > 1:
445                 self._msg_type = None
446             if self._show_run_lookup_nr == 1:
447                 text = msg.message.replace("vat# ", "").\
448                     replace("return STDOUT ", "").replace("\n\n", "\n").\
449                     replace('\n', ' |br| ').\
450                     replace('\r', '').replace('"', "'")
451                 try:
452                     self._data["tests"][self._test_ID]["show-run"] += " |br| "
453                     self._data["tests"][self._test_ID]["show-run"] += \
454                         "**DUT" + str(self._lookup_kw_nr) + ":** |br| " + text
455                 except KeyError:
456                     pass
457
458     # TODO: Remove when definitely no NDRPDRDISC tests are used:
459     def _get_latency(self, msg, test_type):
460         """Get the latency data from the test message.
461
462         :param msg: Message to be parsed.
463         :param test_type: Type of the test - NDR or PDR.
464         :type msg: str
465         :type test_type: str
466         :returns: Latencies parsed from the message.
467         :rtype: dict
468         """
469
470         if test_type == "NDR":
471             groups = re.search(self.REGEX_LAT_NDR, msg)
472             groups_range = range(1, 7)
473         elif test_type == "PDR":
474             groups = re.search(self.REGEX_LAT_PDR, msg)
475             groups_range = range(1, 3)
476         else:
477             return {}
478
479         latencies = list()
480         for idx in groups_range:
481             try:
482                 lat = [int(item) for item in str(groups.group(idx)).split('/')]
483             except (AttributeError, ValueError):
484                 lat = [-1, -1, -1]
485             latencies.append(lat)
486
487         keys = ("min", "avg", "max")
488         latency = {
489             "direction1": {
490             },
491             "direction2": {
492             }
493         }
494
495         latency["direction1"]["100"] = dict(zip(keys, latencies[0]))
496         latency["direction2"]["100"] = dict(zip(keys, latencies[1]))
497         if test_type == "NDR":
498             latency["direction1"]["50"] = dict(zip(keys, latencies[2]))
499             latency["direction2"]["50"] = dict(zip(keys, latencies[3]))
500             latency["direction1"]["10"] = dict(zip(keys, latencies[4]))
501             latency["direction2"]["10"] = dict(zip(keys, latencies[5]))
502
503         return latency
504
505     def _get_ndrpdr_throughput(self, msg):
506         """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
507         message.
508
509         :param msg: The test message to be parsed.
510         :type msg: str
511         :returns: Parsed data as a dict and the status (PASS/FAIL).
512         :rtype: tuple(dict, str)
513         """
514
515         throughput = {
516             "NDR": {"LOWER": -1.0, "UPPER": -1.0},
517             "PDR": {"LOWER": -1.0, "UPPER": -1.0}
518         }
519         status = "FAIL"
520         groups = re.search(self.REGEX_NDRPDR_RATE, msg)
521
522         if groups is not None:
523             try:
524                 throughput["NDR"]["LOWER"] = float(groups.group(1))
525                 throughput["NDR"]["UPPER"] = float(groups.group(2))
526                 throughput["PDR"]["LOWER"] = float(groups.group(3))
527                 throughput["PDR"]["UPPER"] = float(groups.group(4))
528                 status = "PASS"
529             except (IndexError, ValueError):
530                 pass
531
532         return throughput, status
533
534     def _get_ndrpdr_latency(self, msg):
535         """Get LATENCY from the test message.
536
537         :param msg: The test message to be parsed.
538         :type msg: str
539         :returns: Parsed data as a dict and the status (PASS/FAIL).
540         :rtype: tuple(dict, str)
541         """
542
543         latency = {
544             "NDR": {
545                 "direction1": {"min": -1.0, "avg": -1.0, "max": -1.0},
546                 "direction2": {"min": -1.0, "avg": -1.0, "max": -1.0}
547             },
548             "PDR": {
549                 "direction1": {"min": -1.0, "avg": -1.0, "max": -1.0},
550                 "direction2": {"min": -1.0, "avg": -1.0, "max": -1.0}
551             }
552         }
553         status = "FAIL"
554         groups = re.search(self.REGEX_NDRPDR_LAT, msg)
555
556         if groups is not None:
557             keys = ("min", "avg", "max")
558             try:
559                 latency["NDR"]["direction1"] = dict(
560                     zip(keys, [float(l) for l in groups.group(1).split('/')]))
561                 latency["NDR"]["direction2"] = dict(
562                     zip(keys, [float(l) for l in groups.group(2).split('/')]))
563                 latency["PDR"]["direction1"] = dict(
564                     zip(keys, [float(l) for l in groups.group(3).split('/')]))
565                 latency["PDR"]["direction2"] = dict(
566                     zip(keys, [float(l) for l in groups.group(4).split('/')]))
567                 status = "PASS"
568             except (IndexError, ValueError):
569                 pass
570
571         return latency, status
572
573     def visit_suite(self, suite):
574         """Implements traversing through the suite and its direct children.
575
576         :param suite: Suite to process.
577         :type suite: Suite
578         :returns: Nothing.
579         """
580         if self.start_suite(suite) is not False:
581             suite.suites.visit(self)
582             suite.tests.visit(self)
583             self.end_suite(suite)
584
585     def start_suite(self, suite):
586         """Called when suite starts.
587
588         :param suite: Suite to process.
589         :type suite: Suite
590         :returns: Nothing.
591         """
592
593         try:
594             parent_name = suite.parent.name
595         except AttributeError:
596             return
597
598         doc_str = suite.doc.replace('"', "'").replace('\n', ' ').\
599             replace('\r', '').replace('*[', ' |br| *[').replace("*", "**")
600         doc_str = replace(doc_str, ' |br| *[', '*[', maxreplace=1)
601
602         self._data["suites"][suite.longname.lower().replace('"', "'").
603             replace(" ", "_")] = {
604                 "name": suite.name.lower(),
605                 "doc": doc_str,
606                 "parent": parent_name,
607                 "level": len(suite.longname.split("."))
608             }
609
610         suite.keywords.visit(self)
611
612     def end_suite(self, suite):
613         """Called when suite ends.
614
615         :param suite: Suite to process.
616         :type suite: Suite
617         :returns: Nothing.
618         """
619         pass
620
621     def visit_test(self, test):
622         """Implements traversing through the test.
623
624         :param test: Test to process.
625         :type test: Test
626         :returns: Nothing.
627         """
628         if self.start_test(test) is not False:
629             test.keywords.visit(self)
630             self.end_test(test)
631
632     def start_test(self, test):
633         """Called when test starts.
634
635         :param test: Test to process.
636         :type test: Test
637         :returns: Nothing.
638         """
639
640         longname_orig = test.longname.lower()
641
642         # Check the ignore list
643         if longname_orig in self._ignore:
644             return
645
646         tags = [str(tag) for tag in test.tags]
647         test_result = dict()
648
649         # Change the TC long name and name if defined in the mapping table
650         longname = self._mapping.get(longname_orig, None)
651         if longname is not None:
652             name = longname.split('.')[-1]
653             logging.debug("{0}\n{1}\n{2}\n{3}".format(
654                 self._data["metadata"], longname_orig, longname, name))
655         else:
656             longname = longname_orig
657             name = test.name.lower()
658
659         # Remove TC number from the TC long name (backward compatibility):
660         self._test_ID = re.sub(self.REGEX_TC_NUMBER, "", longname)
661         # Remove TC number from the TC name (not needed):
662         test_result["name"] = re.sub(self.REGEX_TC_NUMBER, "", name)
663
664         test_result["parent"] = test.parent.name.lower()
665         test_result["tags"] = tags
666         doc_str = test.doc.replace('"', "'").replace('\n', ' '). \
667             replace('\r', '').replace('[', ' |br| [')
668         test_result["doc"] = replace(doc_str, ' |br| [', '[', maxreplace=1)
669         test_result["msg"] = test.message.replace('\n', ' |br| '). \
670             replace('\r', '').replace('"', "'")
671         test_result["type"] = "FUNC"
672         test_result["status"] = test.status
673
674         if "PERFTEST" in tags:
675             # Replace info about cores (e.g. -1c-) with the info about threads
676             # and cores (e.g. -1t1c-) in the long test case names and in the
677             # test case names if necessary.
678             groups = re.search(self.REGEX_TC_NAME_OLD, self._test_ID)
679             if not groups:
680                 tag_count = 0
681                 for tag in test_result["tags"]:
682                     groups = re.search(self.REGEX_TC_TAG, tag)
683                     if groups:
684                         tag_count += 1
685                         tag_tc = tag
686
687                 if tag_count == 1:
688                     self._test_ID = re.sub(self.REGEX_TC_NAME_NEW,
689                                            "-{0}-".format(tag_tc.lower()),
690                                            self._test_ID,
691                                            count=1)
692                     test_result["name"] = re.sub(self.REGEX_TC_NAME_NEW,
693                                                  "-{0}-".format(tag_tc.lower()),
694                                                  test_result["name"],
695                                                  count=1)
696                 else:
697                     test_result["status"] = "FAIL"
698                     self._data["tests"][self._test_ID] = test_result
699                     logging.error("The test '{0}' has no or more than one "
700                                   "multi-threading tags.".format(self._test_ID))
701                     return
702
703         if test.status == "PASS" and ("NDRPDRDISC" in tags or
704                                       "NDRPDR" in tags or
705                                       "TCP" in tags or
706                                       "MRR" in tags or
707                                       "BMRR" in tags):
708             # TODO: Remove when definitely no NDRPDRDISC tests are used:
709             if "NDRDISC" in tags:
710                 test_result["type"] = "NDR"
711             # TODO: Remove when definitely no NDRPDRDISC tests are used:
712             elif "PDRDISC" in tags:
713                 test_result["type"] = "PDR"
714             elif "NDRPDR" in tags:
715                 test_result["type"] = "NDRPDR"
716             elif "TCP" in tags:
717                 test_result["type"] = "TCP"
718             elif "MRR" in tags:
719                 test_result["type"] = "MRR"
720             elif "FRMOBL" in tags or "BMRR" in tags:
721                 test_result["type"] = "BMRR"
722             else:
723                 test_result["status"] = "FAIL"
724                 self._data["tests"][self._test_ID] = test_result
725                 return
726
727             # TODO: Remove when definitely no NDRPDRDISC tests are used:
728             if test_result["type"] in ("NDR", "PDR"):
729                 try:
730                     rate_value = str(re.search(
731                         self.REGEX_RATE, test.message).group(1))
732                 except AttributeError:
733                     rate_value = "-1"
734                 try:
735                     rate_unit = str(re.search(
736                         self.REGEX_RATE, test.message).group(2))
737                 except AttributeError:
738                     rate_unit = "-1"
739
740                 test_result["throughput"] = dict()
741                 test_result["throughput"]["value"] = \
742                     int(rate_value.split('.')[0])
743                 test_result["throughput"]["unit"] = rate_unit
744                 test_result["latency"] = \
745                     self._get_latency(test.message, test_result["type"])
746                 if test_result["type"] == "PDR":
747                     test_result["lossTolerance"] = str(re.search(
748                         self.REGEX_TOLERANCE, test.message).group(1))
749
750             elif test_result["type"] in ("NDRPDR", ):
751                 test_result["throughput"], test_result["status"] = \
752                     self._get_ndrpdr_throughput(test.message)
753                 test_result["latency"], test_result["status"] = \
754                     self._get_ndrpdr_latency(test.message)
755
756             elif test_result["type"] in ("TCP", ):
757                 groups = re.search(self.REGEX_TCP, test.message)
758                 test_result["result"] = int(groups.group(2))
759
760             elif test_result["type"] in ("MRR", "BMRR"):
761                 test_result["result"] = dict()
762                 groups = re.search(self.REGEX_BMRR, test.message)
763                 if groups is not None:
764                     items_str = groups.group(1)
765                     items_float = [float(item.strip()) for item
766                                    in items_str.split(",")]
767                     test_result["result"]["receive-rate"] = \
768                         AvgStdevMetadataFactory.from_data(items_float)
769                 else:
770                     groups = re.search(self.REGEX_MRR, test.message)
771                     test_result["result"]["receive-rate"] = \
772                         AvgStdevMetadataFactory.from_data([
773                             float(groups.group(3)) / float(groups.group(1)), ])
774
775         self._data["tests"][self._test_ID] = test_result
776
777     def end_test(self, test):
778         """Called when test ends.
779
780         :param test: Test to process.
781         :type test: Test
782         :returns: Nothing.
783         """
784         pass
785
786     def visit_keyword(self, keyword):
787         """Implements traversing through the keyword and its child keywords.
788
789         :param keyword: Keyword to process.
790         :type keyword: Keyword
791         :returns: Nothing.
792         """
793         if self.start_keyword(keyword) is not False:
794             self.end_keyword(keyword)
795
796     def start_keyword(self, keyword):
797         """Called when keyword starts. Default implementation does nothing.
798
799         :param keyword: Keyword to process.
800         :type keyword: Keyword
801         :returns: Nothing.
802         """
803         try:
804             if keyword.type == "setup":
805                 self.visit_setup_kw(keyword)
806             elif keyword.type == "teardown":
807                 self._lookup_kw_nr = 0
808                 self.visit_teardown_kw(keyword)
809             else:
810                 self._lookup_kw_nr = 0
811                 self.visit_test_kw(keyword)
812         except AttributeError:
813             pass
814
815     def end_keyword(self, keyword):
816         """Called when keyword ends. Default implementation does nothing.
817
818         :param keyword: Keyword to process.
819         :type keyword: Keyword
820         :returns: Nothing.
821         """
822         pass
823
824     def visit_test_kw(self, test_kw):
825         """Implements traversing through the test keyword and its child
826         keywords.
827
828         :param test_kw: Keyword to process.
829         :type test_kw: Keyword
830         :returns: Nothing.
831         """
832         for keyword in test_kw.keywords:
833             if self.start_test_kw(keyword) is not False:
834                 self.visit_test_kw(keyword)
835                 self.end_test_kw(keyword)
836
837     def start_test_kw(self, test_kw):
838         """Called when test keyword starts. Default implementation does
839         nothing.
840
841         :param test_kw: Keyword to process.
842         :type test_kw: Keyword
843         :returns: Nothing.
844         """
845         if test_kw.name.count("Show Runtime Counters On All Duts"):
846             self._lookup_kw_nr += 1
847             self._show_run_lookup_nr = 0
848             self._msg_type = "test-show-runtime"
849         elif test_kw.name.count("Start The L2fwd Test") and not self._version:
850             self._msg_type = "dpdk-version"
851         else:
852             return
853         test_kw.messages.visit(self)
854
855     def end_test_kw(self, test_kw):
856         """Called when keyword ends. Default implementation does nothing.
857
858         :param test_kw: Keyword to process.
859         :type test_kw: Keyword
860         :returns: Nothing.
861         """
862         pass
863
864     def visit_setup_kw(self, setup_kw):
865         """Implements traversing through the teardown keyword and its child
866         keywords.
867
868         :param setup_kw: Keyword to process.
869         :type setup_kw: Keyword
870         :returns: Nothing.
871         """
872         for keyword in setup_kw.keywords:
873             if self.start_setup_kw(keyword) is not False:
874                 self.visit_setup_kw(keyword)
875                 self.end_setup_kw(keyword)
876
877     def start_setup_kw(self, setup_kw):
878         """Called when teardown keyword starts. Default implementation does
879         nothing.
880
881         :param setup_kw: Keyword to process.
882         :type setup_kw: Keyword
883         :returns: Nothing.
884         """
885         if setup_kw.name.count("Show Vpp Version On All Duts") \
886                 and not self._version:
887             self._msg_type = "vpp-version"
888
889         elif setup_kw.name.count("Setup performance global Variables") \
890                 and not self._timestamp:
891             self._msg_type = "timestamp"
892         else:
893             return
894         setup_kw.messages.visit(self)
895
896     def end_setup_kw(self, setup_kw):
897         """Called when keyword ends. Default implementation does nothing.
898
899         :param setup_kw: Keyword to process.
900         :type setup_kw: Keyword
901         :returns: Nothing.
902         """
903         pass
904
905     def visit_teardown_kw(self, teardown_kw):
906         """Implements traversing through the teardown keyword and its child
907         keywords.
908
909         :param teardown_kw: Keyword to process.
910         :type teardown_kw: Keyword
911         :returns: Nothing.
912         """
913         for keyword in teardown_kw.keywords:
914             if self.start_teardown_kw(keyword) is not False:
915                 self.visit_teardown_kw(keyword)
916                 self.end_teardown_kw(keyword)
917
918     def start_teardown_kw(self, teardown_kw):
919         """Called when teardown keyword starts. Default implementation does
920         nothing.
921
922         :param teardown_kw: Keyword to process.
923         :type teardown_kw: Keyword
924         :returns: Nothing.
925         """
926
927         if teardown_kw.name.count("Show Vat History On All Duts"):
928             self._vat_history_lookup_nr = 0
929             self._msg_type = "teardown-vat-history"
930             teardown_kw.messages.visit(self)
931
932     def end_teardown_kw(self, teardown_kw):
933         """Called when keyword ends. Default implementation does nothing.
934
935         :param teardown_kw: Keyword to process.
936         :type teardown_kw: Keyword
937         :returns: Nothing.
938         """
939         pass
940
941     def visit_message(self, msg):
942         """Implements visiting the message.
943
944         :param msg: Message to process.
945         :type msg: Message
946         :returns: Nothing.
947         """
948         if self.start_message(msg) is not False:
949             self.end_message(msg)
950
951     def start_message(self, msg):
952         """Called when message starts. Get required information from messages:
953         - VPP version.
954
955         :param msg: Message to process.
956         :type msg: Message
957         :returns: Nothing.
958         """
959
960         if self._msg_type:
961             self.parse_msg[self._msg_type](msg)
962
963     def end_message(self, msg):
964         """Called when message ends. Default implementation does nothing.
965
966         :param msg: Message to process.
967         :type msg: Message
968         :returns: Nothing.
969         """
970         pass
971
972
973 class InputData(object):
974     """Input data
975
976     The data is extracted from output.xml files generated by Jenkins jobs and
977     stored in pandas' DataFrames.
978
979     The data structure:
980     - job name
981       - build number
982         - metadata
983           (as described in ExecutionChecker documentation)
984         - suites
985           (as described in ExecutionChecker documentation)
986         - tests
987           (as described in ExecutionChecker documentation)
988     """
989
990     def __init__(self, spec):
991         """Initialization.
992
993         :param spec: Specification.
994         :type spec: Specification
995         """
996
997         # Specification:
998         self._cfg = spec
999
1000         # Data store:
1001         self._input_data = pd.Series()
1002
1003     @property
1004     def data(self):
1005         """Getter - Input data.
1006
1007         :returns: Input data
1008         :rtype: pandas.Series
1009         """
1010         return self._input_data
1011
1012     def metadata(self, job, build):
1013         """Getter - metadata
1014
1015         :param job: Job which metadata we want.
1016         :param build: Build which metadata we want.
1017         :type job: str
1018         :type build: str
1019         :returns: Metadata
1020         :rtype: pandas.Series
1021         """
1022
1023         return self.data[job][build]["metadata"]
1024
1025     def suites(self, job, build):
1026         """Getter - suites
1027
1028         :param job: Job which suites we want.
1029         :param build: Build which suites we want.
1030         :type job: str
1031         :type build: str
1032         :returns: Suites.
1033         :rtype: pandas.Series
1034         """
1035
1036         return self.data[job][str(build)]["suites"]
1037
1038     def tests(self, job, build):
1039         """Getter - tests
1040
1041         :param job: Job which tests we want.
1042         :param build: Build which tests we want.
1043         :type job: str
1044         :type build: str
1045         :returns: Tests.
1046         :rtype: pandas.Series
1047         """
1048
1049         return self.data[job][build]["tests"]
1050
1051     def _parse_tests(self, job, build, log):
1052         """Process data from robot output.xml file and return JSON structured
1053         data.
1054
1055         :param job: The name of job which build output data will be processed.
1056         :param build: The build which output data will be processed.
1057         :param log: List of log messages.
1058         :type job: str
1059         :type build: dict
1060         :type log: list of tuples (severity, msg)
1061         :returns: JSON data structure.
1062         :rtype: dict
1063         """
1064
1065         metadata = {
1066             "job": job,
1067             "build": build
1068         }
1069
1070         with open(build["file-name"], 'r') as data_file:
1071             try:
1072                 result = ExecutionResult(data_file)
1073             except errors.DataError as err:
1074                 log.append(("ERROR", "Error occurred while parsing output.xml: "
1075                                      "{0}".format(err)))
1076                 return None
1077         checker = ExecutionChecker(metadata, self._cfg.mapping,
1078                                    self._cfg.ignore)
1079         result.visit(checker)
1080
1081         return checker.data
1082
1083     def _download_and_parse_build(self, pid, data_queue, job, build, repeat):
1084         """Download and parse the input data file.
1085
1086         :param pid: PID of the process executing this method.
1087         :param data_queue: Shared memory between processes. Queue which keeps
1088             the result data. This data is then read by the main process and used
1089             in further processing.
1090         :param job: Name of the Jenkins job which generated the processed input
1091             file.
1092         :param build: Information about the Jenkins build which generated the
1093             processed input file.
1094         :param repeat: Repeat the download specified number of times if not
1095             successful.
1096         :type pid: int
1097         :type data_queue: multiprocessing.Manager().Queue()
1098         :type job: str
1099         :type build: dict
1100         :type repeat: int
1101         """
1102
1103         logs = list()
1104
1105         logging.info("  Processing the job/build: {0}: {1}".
1106                      format(job, build["build"]))
1107
1108         logs.append(("INFO", "  Processing the job/build: {0}: {1}".
1109                      format(job, build["build"])))
1110
1111         state = "failed"
1112         success = False
1113         data = None
1114         do_repeat = repeat
1115         while do_repeat:
1116             success = download_and_unzip_data_file(self._cfg, job, build, pid,
1117                                                    logs)
1118             if success:
1119                 break
1120             do_repeat -= 1
1121         if not success:
1122             logs.append(("ERROR", "It is not possible to download the input "
1123                                   "data file from the job '{job}', build "
1124                                   "'{build}', or it is damaged. Skipped.".
1125                          format(job=job, build=build["build"])))
1126         if success:
1127             logs.append(("INFO", "  Processing data from the build '{0}' ...".
1128                          format(build["build"])))
1129             data = self._parse_tests(job, build, logs)
1130             if data is None:
1131                 logs.append(("ERROR", "Input data file from the job '{job}', "
1132                                       "build '{build}' is damaged. Skipped.".
1133                              format(job=job, build=build["build"])))
1134             else:
1135                 state = "processed"
1136
1137             try:
1138                 remove(build["file-name"])
1139             except OSError as err:
1140                 logs.append(("ERROR", "Cannot remove the file '{0}': {1}".
1141                              format(build["file-name"], err)))
1142         logs.append(("INFO", "  Done."))
1143
1144         result = {
1145             "data": data,
1146             "state": state,
1147             "job": job,
1148             "build": build,
1149             "logs": logs
1150         }
1151         data_queue.put(result)
1152
1153     def download_and_parse_data(self, repeat=1):
1154         """Download the input data files, parse input data from input files and
1155         store in pandas' Series.
1156
1157         :param repeat: Repeat the download specified number of times if not
1158             successful.
1159         :type repeat: int
1160         """
1161
1162         logging.info("Downloading and parsing input files ...")
1163
1164         work_queue = multiprocessing.JoinableQueue()
1165         manager = multiprocessing.Manager()
1166         data_queue = manager.Queue()
1167         cpus = multiprocessing.cpu_count()
1168
1169         workers = list()
1170         for cpu in range(cpus):
1171             worker = Worker(work_queue,
1172                             data_queue,
1173                             self._download_and_parse_build)
1174             worker.daemon = True
1175             worker.start()
1176             workers.append(worker)
1177             os.system("taskset -p -c {0} {1} > /dev/null 2>&1".
1178                       format(cpu, worker.pid))
1179
1180         for job, builds in self._cfg.builds.items():
1181             for build in builds:
1182                 work_queue.put((job, build, repeat))
1183
1184         work_queue.join()
1185
1186         logging.info("Done.")
1187
1188         while not data_queue.empty():
1189             result = data_queue.get()
1190
1191             job = result["job"]
1192             build_nr = result["build"]["build"]
1193
1194             if result["data"]:
1195                 data = result["data"]
1196                 build_data = pd.Series({
1197                     "metadata": pd.Series(data["metadata"].values(),
1198                                           index=data["metadata"].keys()),
1199                     "suites": pd.Series(data["suites"].values(),
1200                                         index=data["suites"].keys()),
1201                     "tests": pd.Series(data["tests"].values(),
1202                                        index=data["tests"].keys())})
1203
1204                 if self._input_data.get(job, None) is None:
1205                     self._input_data[job] = pd.Series()
1206                 self._input_data[job][str(build_nr)] = build_data
1207
1208                 self._cfg.set_input_file_name(job, build_nr,
1209                                               result["build"]["file-name"])
1210
1211             self._cfg.set_input_state(job, build_nr, result["state"])
1212
1213             for item in result["logs"]:
1214                 if item[0] == "INFO":
1215                     logging.info(item[1])
1216                 elif item[0] == "ERROR":
1217                     logging.error(item[1])
1218                 elif item[0] == "DEBUG":
1219                     logging.debug(item[1])
1220                 elif item[0] == "CRITICAL":
1221                     logging.critical(item[1])
1222                 elif item[0] == "WARNING":
1223                     logging.warning(item[1])
1224
1225         del data_queue
1226
1227         # Terminate all workers
1228         for worker in workers:
1229             worker.terminate()
1230             worker.join()
1231
1232         logging.info("Done.")
1233
1234     @staticmethod
1235     def _end_of_tag(tag_filter, start=0, closer="'"):
1236         """Return the index of character in the string which is the end of tag.
1237
1238         :param tag_filter: The string where the end of tag is being searched.
1239         :param start: The index where the searching is stated.
1240         :param closer: The character which is the tag closer.
1241         :type tag_filter: str
1242         :type start: int
1243         :type closer: str
1244         :returns: The index of the tag closer.
1245         :rtype: int
1246         """
1247
1248         try:
1249             idx_opener = tag_filter.index(closer, start)
1250             return tag_filter.index(closer, idx_opener + 1)
1251         except ValueError:
1252             return None
1253
1254     @staticmethod
1255     def _condition(tag_filter):
1256         """Create a conditional statement from the given tag filter.
1257
1258         :param tag_filter: Filter based on tags from the element specification.
1259         :type tag_filter: str
1260         :returns: Conditional statement which can be evaluated.
1261         :rtype: str
1262         """
1263
1264         index = 0
1265         while True:
1266             index = InputData._end_of_tag(tag_filter, index)
1267             if index is None:
1268                 return tag_filter
1269             index += 1
1270             tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
1271
1272     def filter_data(self, element, params=None, data_set="tests",
1273                     continue_on_error=False):
1274         """Filter required data from the given jobs and builds.
1275
1276         The output data structure is:
1277
1278         - job 1
1279           - build 1
1280             - test (or suite) 1 ID:
1281               - param 1
1282               - param 2
1283               ...
1284               - param n
1285             ...
1286             - test (or suite) n ID:
1287             ...
1288           ...
1289           - build n
1290         ...
1291         - job n
1292
1293         :param element: Element which will use the filtered data.
1294         :param params: Parameters which will be included in the output. If None,
1295         all parameters are included.
1296         :param data_set: The set of data to be filtered: tests, suites,
1297         metadata.
1298         :param continue_on_error: Continue if there is error while reading the
1299         data. The Item will be empty then
1300         :type element: pandas.Series
1301         :type params: list
1302         :type data_set: str
1303         :type continue_on_error: bool
1304         :returns: Filtered data.
1305         :rtype pandas.Series
1306         """
1307
1308         try:
1309             if element["filter"] in ("all", "template"):
1310                 cond = "True"
1311             else:
1312                 cond = InputData._condition(element["filter"])
1313             logging.debug("   Filter: {0}".format(cond))
1314         except KeyError:
1315             logging.error("  No filter defined.")
1316             return None
1317
1318         if params is None:
1319             params = element.get("parameters", None)
1320             if params:
1321                 params.append("type")
1322
1323         data = pd.Series()
1324         try:
1325             for job, builds in element["data"].items():
1326                 data[job] = pd.Series()
1327                 for build in builds:
1328                     data[job][str(build)] = pd.Series()
1329                     try:
1330                         data_iter = self.data[job][str(build)][data_set].\
1331                             iteritems()
1332                     except KeyError:
1333                         if continue_on_error:
1334                             continue
1335                         else:
1336                             return None
1337                     for test_ID, test_data in data_iter:
1338                         if eval(cond, {"tags": test_data.get("tags", "")}):
1339                             data[job][str(build)][test_ID] = pd.Series()
1340                             if params is None:
1341                                 for param, val in test_data.items():
1342                                     data[job][str(build)][test_ID][param] = val
1343                             else:
1344                                 for param in params:
1345                                     try:
1346                                         data[job][str(build)][test_ID][param] =\
1347                                             test_data[param]
1348                                     except KeyError:
1349                                         data[job][str(build)][test_ID][param] =\
1350                                             "No Data"
1351             return data
1352
1353         except (KeyError, IndexError, ValueError) as err:
1354             logging.error("   Missing mandatory parameter in the element "
1355                           "specification: {0}".format(err))
1356             return None
1357         except AttributeError:
1358             return None
1359         except SyntaxError:
1360             logging.error("   The filter '{0}' is not correct. Check if all "
1361                           "tags are enclosed by apostrophes.".format(cond))
1362             return None
1363
1364     @staticmethod
1365     def merge_data(data):
1366         """Merge data from more jobs and builds to a simple data structure.
1367
1368         The output data structure is:
1369
1370         - test (suite) 1 ID:
1371           - param 1
1372           - param 2
1373           ...
1374           - param n
1375         ...
1376         - test (suite) n ID:
1377         ...
1378
1379         :param data: Data to merge.
1380         :type data: pandas.Series
1381         :returns: Merged data.
1382         :rtype: pandas.Series
1383         """
1384
1385         logging.info("    Merging data ...")
1386
1387         merged_data = pd.Series()
1388         for _, builds in data.iteritems():
1389             for _, item in builds.iteritems():
1390                 for ID, item_data in item.iteritems():
1391                     merged_data[ID] = item_data
1392
1393         return merged_data