0712156f778e4b91b1a0c6333d4bdf60cf2f183b
[csit.git] / resources / tools / presentation_new / 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                     logging.debug("Tags: {0}".format(test_result["tags"]))
702                     return
703
704         if test.status == "PASS" and ("NDRPDRDISC" in tags or
705                                       "NDRPDR" in tags or
706                                       "TCP" in tags or
707                                       "MRR" in tags or
708                                       "BMRR" in tags):
709             # TODO: Remove when definitely no NDRPDRDISC tests are used:
710             if "NDRDISC" in tags:
711                 test_result["type"] = "NDR"
712             # TODO: Remove when definitely no NDRPDRDISC tests are used:
713             elif "PDRDISC" in tags:
714                 test_result["type"] = "PDR"
715             elif "NDRPDR" in tags:
716                 test_result["type"] = "NDRPDR"
717             elif "TCP" in tags:
718                 test_result["type"] = "TCP"
719             elif "MRR" in tags:
720                 test_result["type"] = "MRR"
721             elif "FRMOBL" in tags or "BMRR" in tags:
722                 test_result["type"] = "BMRR"
723             else:
724                 test_result["status"] = "FAIL"
725                 self._data["tests"][self._test_ID] = test_result
726                 return
727
728             # TODO: Remove when definitely no NDRPDRDISC tests are used:
729             if test_result["type"] in ("NDR", "PDR"):
730                 try:
731                     rate_value = str(re.search(
732                         self.REGEX_RATE, test.message).group(1))
733                 except AttributeError:
734                     rate_value = "-1"
735                 try:
736                     rate_unit = str(re.search(
737                         self.REGEX_RATE, test.message).group(2))
738                 except AttributeError:
739                     rate_unit = "-1"
740
741                 test_result["throughput"] = dict()
742                 test_result["throughput"]["value"] = \
743                     int(rate_value.split('.')[0])
744                 test_result["throughput"]["unit"] = rate_unit
745                 test_result["latency"] = \
746                     self._get_latency(test.message, test_result["type"])
747                 if test_result["type"] == "PDR":
748                     test_result["lossTolerance"] = str(re.search(
749                         self.REGEX_TOLERANCE, test.message).group(1))
750
751             elif test_result["type"] in ("NDRPDR", ):
752                 test_result["throughput"], test_result["status"] = \
753                     self._get_ndrpdr_throughput(test.message)
754                 test_result["latency"], test_result["status"] = \
755                     self._get_ndrpdr_latency(test.message)
756
757             elif test_result["type"] in ("TCP", ):
758                 groups = re.search(self.REGEX_TCP, test.message)
759                 test_result["result"] = int(groups.group(2))
760
761             elif test_result["type"] in ("MRR", "BMRR"):
762                 test_result["result"] = dict()
763                 groups = re.search(self.REGEX_BMRR, test.message)
764                 if groups is not None:
765                     items_str = groups.group(1)
766                     items_float = [float(item.strip()) for item
767                                    in items_str.split(",")]
768                     metadata = AvgStdevMetadataFactory.from_data(items_float)
769                     test_result["result"]["receive-rate"] = metadata
770                 else:
771                     groups = re.search(self.REGEX_MRR, test.message)
772                     test_result["result"]["receive-rate"] = \
773                         AvgStdevMetadataFactory.from_data([
774                             float(groups.group(3)) / float(groups.group(1)), ])
775
776         self._data["tests"][self._test_ID] = test_result
777
778     def end_test(self, test):
779         """Called when test ends.
780
781         :param test: Test to process.
782         :type test: Test
783         :returns: Nothing.
784         """
785         pass
786
787     def visit_keyword(self, keyword):
788         """Implements traversing through the keyword and its child keywords.
789
790         :param keyword: Keyword to process.
791         :type keyword: Keyword
792         :returns: Nothing.
793         """
794         if self.start_keyword(keyword) is not False:
795             self.end_keyword(keyword)
796
797     def start_keyword(self, keyword):
798         """Called when keyword starts. Default implementation does nothing.
799
800         :param keyword: Keyword to process.
801         :type keyword: Keyword
802         :returns: Nothing.
803         """
804         try:
805             if keyword.type == "setup":
806                 self.visit_setup_kw(keyword)
807             elif keyword.type == "teardown":
808                 self._lookup_kw_nr = 0
809                 self.visit_teardown_kw(keyword)
810             else:
811                 self._lookup_kw_nr = 0
812                 self.visit_test_kw(keyword)
813         except AttributeError:
814             pass
815
816     def end_keyword(self, keyword):
817         """Called when keyword ends. Default implementation does nothing.
818
819         :param keyword: Keyword to process.
820         :type keyword: Keyword
821         :returns: Nothing.
822         """
823         pass
824
825     def visit_test_kw(self, test_kw):
826         """Implements traversing through the test keyword and its child
827         keywords.
828
829         :param test_kw: Keyword to process.
830         :type test_kw: Keyword
831         :returns: Nothing.
832         """
833         for keyword in test_kw.keywords:
834             if self.start_test_kw(keyword) is not False:
835                 self.visit_test_kw(keyword)
836                 self.end_test_kw(keyword)
837
838     def start_test_kw(self, test_kw):
839         """Called when test keyword starts. Default implementation does
840         nothing.
841
842         :param test_kw: Keyword to process.
843         :type test_kw: Keyword
844         :returns: Nothing.
845         """
846         if test_kw.name.count("Show Runtime Counters On All Duts"):
847             self._lookup_kw_nr += 1
848             self._show_run_lookup_nr = 0
849             self._msg_type = "test-show-runtime"
850         elif test_kw.name.count("Start The L2fwd Test") and not self._version:
851             self._msg_type = "dpdk-version"
852         else:
853             return
854         test_kw.messages.visit(self)
855
856     def end_test_kw(self, test_kw):
857         """Called when keyword ends. Default implementation does nothing.
858
859         :param test_kw: Keyword to process.
860         :type test_kw: Keyword
861         :returns: Nothing.
862         """
863         pass
864
865     def visit_setup_kw(self, setup_kw):
866         """Implements traversing through the teardown keyword and its child
867         keywords.
868
869         :param setup_kw: Keyword to process.
870         :type setup_kw: Keyword
871         :returns: Nothing.
872         """
873         for keyword in setup_kw.keywords:
874             if self.start_setup_kw(keyword) is not False:
875                 self.visit_setup_kw(keyword)
876                 self.end_setup_kw(keyword)
877
878     def start_setup_kw(self, setup_kw):
879         """Called when teardown keyword starts. Default implementation does
880         nothing.
881
882         :param setup_kw: Keyword to process.
883         :type setup_kw: Keyword
884         :returns: Nothing.
885         """
886         if setup_kw.name.count("Show Vpp Version On All Duts") \
887                 and not self._version:
888             self._msg_type = "vpp-version"
889
890         elif setup_kw.name.count("Setup performance global Variables") \
891                 and not self._timestamp:
892             self._msg_type = "timestamp"
893         else:
894             return
895         setup_kw.messages.visit(self)
896
897     def end_setup_kw(self, setup_kw):
898         """Called when keyword ends. Default implementation does nothing.
899
900         :param setup_kw: Keyword to process.
901         :type setup_kw: Keyword
902         :returns: Nothing.
903         """
904         pass
905
906     def visit_teardown_kw(self, teardown_kw):
907         """Implements traversing through the teardown keyword and its child
908         keywords.
909
910         :param teardown_kw: Keyword to process.
911         :type teardown_kw: Keyword
912         :returns: Nothing.
913         """
914         for keyword in teardown_kw.keywords:
915             if self.start_teardown_kw(keyword) is not False:
916                 self.visit_teardown_kw(keyword)
917                 self.end_teardown_kw(keyword)
918
919     def start_teardown_kw(self, teardown_kw):
920         """Called when teardown keyword starts. Default implementation does
921         nothing.
922
923         :param teardown_kw: Keyword to process.
924         :type teardown_kw: Keyword
925         :returns: Nothing.
926         """
927
928         if teardown_kw.name.count("Show Vat History On All Duts"):
929             self._vat_history_lookup_nr = 0
930             self._msg_type = "teardown-vat-history"
931             teardown_kw.messages.visit(self)
932
933     def end_teardown_kw(self, teardown_kw):
934         """Called when keyword ends. Default implementation does nothing.
935
936         :param teardown_kw: Keyword to process.
937         :type teardown_kw: Keyword
938         :returns: Nothing.
939         """
940         pass
941
942     def visit_message(self, msg):
943         """Implements visiting the message.
944
945         :param msg: Message to process.
946         :type msg: Message
947         :returns: Nothing.
948         """
949         if self.start_message(msg) is not False:
950             self.end_message(msg)
951
952     def start_message(self, msg):
953         """Called when message starts. Get required information from messages:
954         - VPP version.
955
956         :param msg: Message to process.
957         :type msg: Message
958         :returns: Nothing.
959         """
960
961         if self._msg_type:
962             self.parse_msg[self._msg_type](msg)
963
964     def end_message(self, msg):
965         """Called when message ends. Default implementation does nothing.
966
967         :param msg: Message to process.
968         :type msg: Message
969         :returns: Nothing.
970         """
971         pass
972
973
974 class InputData(object):
975     """Input data
976
977     The data is extracted from output.xml files generated by Jenkins jobs and
978     stored in pandas' DataFrames.
979
980     The data structure:
981     - job name
982       - build number
983         - metadata
984           (as described in ExecutionChecker documentation)
985         - suites
986           (as described in ExecutionChecker documentation)
987         - tests
988           (as described in ExecutionChecker documentation)
989     """
990
991     def __init__(self, spec):
992         """Initialization.
993
994         :param spec: Specification.
995         :type spec: Specification
996         """
997
998         # Specification:
999         self._cfg = spec
1000
1001         # Data store:
1002         self._input_data = pd.Series()
1003
1004     @property
1005     def data(self):
1006         """Getter - Input data.
1007
1008         :returns: Input data
1009         :rtype: pandas.Series
1010         """
1011         return self._input_data
1012
1013     def metadata(self, job, build):
1014         """Getter - metadata
1015
1016         :param job: Job which metadata we want.
1017         :param build: Build which metadata we want.
1018         :type job: str
1019         :type build: str
1020         :returns: Metadata
1021         :rtype: pandas.Series
1022         """
1023
1024         return self.data[job][build]["metadata"]
1025
1026     def suites(self, job, build):
1027         """Getter - suites
1028
1029         :param job: Job which suites we want.
1030         :param build: Build which suites we want.
1031         :type job: str
1032         :type build: str
1033         :returns: Suites.
1034         :rtype: pandas.Series
1035         """
1036
1037         return self.data[job][str(build)]["suites"]
1038
1039     def tests(self, job, build):
1040         """Getter - tests
1041
1042         :param job: Job which tests we want.
1043         :param build: Build which tests we want.
1044         :type job: str
1045         :type build: str
1046         :returns: Tests.
1047         :rtype: pandas.Series
1048         """
1049
1050         return self.data[job][build]["tests"]
1051
1052     def _parse_tests(self, job, build, log):
1053         """Process data from robot output.xml file and return JSON structured
1054         data.
1055
1056         :param job: The name of job which build output data will be processed.
1057         :param build: The build which output data will be processed.
1058         :param log: List of log messages.
1059         :type job: str
1060         :type build: dict
1061         :type log: list of tuples (severity, msg)
1062         :returns: JSON data structure.
1063         :rtype: dict
1064         """
1065
1066         metadata = {
1067             "job": job,
1068             "build": build
1069         }
1070
1071         with open(build["file-name"], 'r') as data_file:
1072             try:
1073                 result = ExecutionResult(data_file)
1074             except errors.DataError as err:
1075                 log.append(("ERROR", "Error occurred while parsing output.xml: "
1076                                      "{0}".format(err)))
1077                 return None
1078         checker = ExecutionChecker(metadata, self._cfg.mapping,
1079                                    self._cfg.ignore)
1080         result.visit(checker)
1081
1082         return checker.data
1083
1084     def _download_and_parse_build(self, pid, data_queue, job, build, repeat):
1085         """Download and parse the input data file.
1086
1087         :param pid: PID of the process executing this method.
1088         :param data_queue: Shared memory between processes. Queue which keeps
1089             the result data. This data is then read by the main process and used
1090             in further processing.
1091         :param job: Name of the Jenkins job which generated the processed input
1092             file.
1093         :param build: Information about the Jenkins build which generated the
1094             processed input file.
1095         :param repeat: Repeat the download specified number of times if not
1096             successful.
1097         :type pid: int
1098         :type data_queue: multiprocessing.Manager().Queue()
1099         :type job: str
1100         :type build: dict
1101         :type repeat: int
1102         """
1103
1104         logs = list()
1105
1106         logging.info("  Processing the job/build: {0}: {1}".
1107                      format(job, build["build"]))
1108
1109         logs.append(("INFO", "  Processing the job/build: {0}: {1}".
1110                      format(job, build["build"])))
1111
1112         state = "failed"
1113         success = False
1114         data = None
1115         do_repeat = repeat
1116         while do_repeat:
1117             success = download_and_unzip_data_file(self._cfg, job, build, pid,
1118                                                    logs)
1119             if success:
1120                 break
1121             do_repeat -= 1
1122         if not success:
1123             logs.append(("ERROR", "It is not possible to download the input "
1124                                   "data file from the job '{job}', build "
1125                                   "'{build}', or it is damaged. Skipped.".
1126                          format(job=job, build=build["build"])))
1127         if success:
1128             logs.append(("INFO", "  Processing data from the build '{0}' ...".
1129                          format(build["build"])))
1130             data = self._parse_tests(job, build, logs)
1131             if data is None:
1132                 logs.append(("ERROR", "Input data file from the job '{job}', "
1133                                       "build '{build}' is damaged. Skipped.".
1134                              format(job=job, build=build["build"])))
1135             else:
1136                 state = "processed"
1137
1138             try:
1139                 remove(build["file-name"])
1140             except OSError as err:
1141                 logs.append(("ERROR", "Cannot remove the file '{0}': {1}".
1142                              format(build["file-name"], err)))
1143         logs.append(("INFO", "  Done."))
1144
1145         result = {
1146             "data": data,
1147             "state": state,
1148             "job": job,
1149             "build": build,
1150             "logs": logs
1151         }
1152         data_queue.put(result)
1153
1154     def download_and_parse_data(self, repeat=1):
1155         """Download the input data files, parse input data from input files and
1156         store in pandas' Series.
1157
1158         :param repeat: Repeat the download specified number of times if not
1159             successful.
1160         :type repeat: int
1161         """
1162
1163         logging.info("Downloading and parsing input files ...")
1164
1165         work_queue = multiprocessing.JoinableQueue()
1166         manager = multiprocessing.Manager()
1167         data_queue = manager.Queue()
1168         cpus = multiprocessing.cpu_count()
1169
1170         workers = list()
1171         for cpu in range(cpus):
1172             worker = Worker(work_queue,
1173                             data_queue,
1174                             self._download_and_parse_build)
1175             worker.daemon = True
1176             worker.start()
1177             workers.append(worker)
1178             os.system("taskset -p -c {0} {1} > /dev/null 2>&1".
1179                       format(cpu, worker.pid))
1180
1181         for job, builds in self._cfg.builds.items():
1182             for build in builds:
1183                 work_queue.put((job, build, repeat))
1184
1185         work_queue.join()
1186
1187         logging.info("Done.")
1188
1189         while not data_queue.empty():
1190             result = data_queue.get()
1191
1192             job = result["job"]
1193             build_nr = result["build"]["build"]
1194
1195             if result["data"]:
1196                 data = result["data"]
1197                 build_data = pd.Series({
1198                     "metadata": pd.Series(data["metadata"].values(),
1199                                           index=data["metadata"].keys()),
1200                     "suites": pd.Series(data["suites"].values(),
1201                                         index=data["suites"].keys()),
1202                     "tests": pd.Series(data["tests"].values(),
1203                                        index=data["tests"].keys())})
1204
1205                 if self._input_data.get(job, None) is None:
1206                     self._input_data[job] = pd.Series()
1207                 self._input_data[job][str(build_nr)] = build_data
1208
1209                 self._cfg.set_input_file_name(job, build_nr,
1210                                               result["build"]["file-name"])
1211
1212             self._cfg.set_input_state(job, build_nr, result["state"])
1213
1214             for item in result["logs"]:
1215                 if item[0] == "INFO":
1216                     logging.info(item[1])
1217                 elif item[0] == "ERROR":
1218                     logging.error(item[1])
1219                 elif item[0] == "DEBUG":
1220                     logging.debug(item[1])
1221                 elif item[0] == "CRITICAL":
1222                     logging.critical(item[1])
1223                 elif item[0] == "WARNING":
1224                     logging.warning(item[1])
1225
1226         del data_queue
1227
1228         # Terminate all workers
1229         for worker in workers:
1230             worker.terminate()
1231             worker.join()
1232
1233         logging.info("Done.")
1234
1235     @staticmethod
1236     def _end_of_tag(tag_filter, start=0, closer="'"):
1237         """Return the index of character in the string which is the end of tag.
1238
1239         :param tag_filter: The string where the end of tag is being searched.
1240         :param start: The index where the searching is stated.
1241         :param closer: The character which is the tag closer.
1242         :type tag_filter: str
1243         :type start: int
1244         :type closer: str
1245         :returns: The index of the tag closer.
1246         :rtype: int
1247         """
1248
1249         try:
1250             idx_opener = tag_filter.index(closer, start)
1251             return tag_filter.index(closer, idx_opener + 1)
1252         except ValueError:
1253             return None
1254
1255     @staticmethod
1256     def _condition(tag_filter):
1257         """Create a conditional statement from the given tag filter.
1258
1259         :param tag_filter: Filter based on tags from the element specification.
1260         :type tag_filter: str
1261         :returns: Conditional statement which can be evaluated.
1262         :rtype: str
1263         """
1264
1265         index = 0
1266         while True:
1267             index = InputData._end_of_tag(tag_filter, index)
1268             if index is None:
1269                 return tag_filter
1270             index += 1
1271             tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
1272
1273     def filter_data(self, element, params=None, data_set="tests",
1274                     continue_on_error=False):
1275         """Filter required data from the given jobs and builds.
1276
1277         The output data structure is:
1278
1279         - job 1
1280           - build 1
1281             - test (or suite) 1 ID:
1282               - param 1
1283               - param 2
1284               ...
1285               - param n
1286             ...
1287             - test (or suite) n ID:
1288             ...
1289           ...
1290           - build n
1291         ...
1292         - job n
1293
1294         :param element: Element which will use the filtered data.
1295         :param params: Parameters which will be included in the output. If None,
1296         all parameters are included.
1297         :param data_set: The set of data to be filtered: tests, suites,
1298         metadata.
1299         :param continue_on_error: Continue if there is error while reading the
1300         data. The Item will be empty then
1301         :type element: pandas.Series
1302         :type params: list
1303         :type data_set: str
1304         :type continue_on_error: bool
1305         :returns: Filtered data.
1306         :rtype pandas.Series
1307         """
1308
1309         try:
1310             if element["filter"] in ("all", "template"):
1311                 cond = "True"
1312             else:
1313                 cond = InputData._condition(element["filter"])
1314             logging.debug("   Filter: {0}".format(cond))
1315         except KeyError:
1316             logging.error("  No filter defined.")
1317             return None
1318
1319         if params is None:
1320             params = element.get("parameters", None)
1321             if params:
1322                 params.append("type")
1323
1324         data = pd.Series()
1325         try:
1326             for job, builds in element["data"].items():
1327                 data[job] = pd.Series()
1328                 for build in builds:
1329                     data[job][str(build)] = pd.Series()
1330                     try:
1331                         data_iter = self.data[job][str(build)][data_set].\
1332                             iteritems()
1333                     except KeyError:
1334                         if continue_on_error:
1335                             continue
1336                         else:
1337                             return None
1338                     for test_ID, test_data in data_iter:
1339                         if eval(cond, {"tags": test_data.get("tags", "")}):
1340                             data[job][str(build)][test_ID] = pd.Series()
1341                             if params is None:
1342                                 for param, val in test_data.items():
1343                                     data[job][str(build)][test_ID][param] = val
1344                             else:
1345                                 for param in params:
1346                                     try:
1347                                         data[job][str(build)][test_ID][param] =\
1348                                             test_data[param]
1349                                     except KeyError:
1350                                         data[job][str(build)][test_ID][param] =\
1351                                             "No Data"
1352             return data
1353
1354         except (KeyError, IndexError, ValueError) as err:
1355             logging.error("   Missing mandatory parameter in the element "
1356                           "specification: {0}".format(err))
1357             return None
1358         except AttributeError:
1359             return None
1360         except SyntaxError:
1361             logging.error("   The filter '{0}' is not correct. Check if all "
1362                           "tags are enclosed by apostrophes.".format(cond))
1363             return None
1364
1365     @staticmethod
1366     def merge_data(data):
1367         """Merge data from more jobs and builds to a simple data structure.
1368
1369         The output data structure is:
1370
1371         - test (suite) 1 ID:
1372           - param 1
1373           - param 2
1374           ...
1375           - param n
1376         ...
1377         - test (suite) n ID:
1378         ...
1379
1380         :param data: Data to merge.
1381         :type data: pandas.Series
1382         :returns: Merged data.
1383         :rtype: pandas.Series
1384         """
1385
1386         logging.info("    Merging data ...")
1387
1388         merged_data = pd.Series()
1389         for _, builds in data.iteritems():
1390             for _, item in builds.iteritems():
1391                 for ID, item_data in item.iteritems():
1392                     merged_data[ID] = item_data
1393
1394         return merged_data