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