8747f938a90fbabd3b8a9e1694ce24ab6d87dd46
[csit.git] / resources / tools / presentation / input_data_parser.py
1 # Copyright (c) 2021 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 re
23 import copy
24 import resource
25 import logging
26
27 from collections import OrderedDict
28 from os import remove, walk, listdir
29 from os.path import isfile, isdir, join
30 from datetime import datetime as dt
31 from datetime import timedelta
32 from json import loads
33 from json.decoder import JSONDecodeError
34
35 import hdrh.histogram
36 import hdrh.codec
37 import prettytable
38 import pandas as pd
39
40 from robot.api import ExecutionResult, ResultVisitor
41 from robot import errors
42
43 from resources.libraries.python import jumpavg
44 from input_data_files import download_and_unzip_data_file
45 from pal_errors import PresentationError
46
47
48 # Separator used in file names
49 SEPARATOR = u"__"
50
51
52 class ExecutionChecker(ResultVisitor):
53     """Class to traverse through the test suite structure.
54
55     The functionality implemented in this class generates a json structure:
56
57     Performance tests:
58
59     {
60         "metadata": {
61             "generated": "Timestamp",
62             "version": "SUT version",
63             "job": "Jenkins job name",
64             "build": "Information about the build"
65         },
66         "suites": {
67             "Suite long name 1": {
68                 "name": Suite name,
69                 "doc": "Suite 1 documentation",
70                 "parent": "Suite 1 parent",
71                 "level": "Level of the suite in the suite hierarchy"
72             }
73             "Suite long name N": {
74                 "name": Suite name,
75                 "doc": "Suite N documentation",
76                 "parent": "Suite 2 parent",
77                 "level": "Level of the suite in the suite hierarchy"
78             }
79         }
80         "tests": {
81             # NDRPDR tests:
82             "ID": {
83                 "name": "Test name",
84                 "parent": "Name of the parent of the test",
85                 "doc": "Test documentation",
86                 "msg": "Test message",
87                 "conf-history": "DUT1 and DUT2 VAT History",
88                 "show-run": "Show Run",
89                 "tags": ["tag 1", "tag 2", "tag n"],
90                 "type": "NDRPDR",
91                 "status": "PASS" | "FAIL",
92                 "throughput": {
93                     "NDR": {
94                         "LOWER": float,
95                         "UPPER": float
96                     },
97                     "PDR": {
98                         "LOWER": float,
99                         "UPPER": float
100                     }
101                 },
102                 "latency": {
103                     "NDR": {
104                         "direction1": {
105                             "min": float,
106                             "avg": float,
107                             "max": float,
108                             "hdrh": str
109                         },
110                         "direction2": {
111                             "min": float,
112                             "avg": float,
113                             "max": float,
114                             "hdrh": str
115                         }
116                     },
117                     "PDR": {
118                         "direction1": {
119                             "min": float,
120                             "avg": float,
121                             "max": float,
122                             "hdrh": str
123                         },
124                         "direction2": {
125                             "min": float,
126                             "avg": float,
127                             "max": float,
128                             "hdrh": str
129                         }
130                     }
131                 }
132             }
133
134             # TCP tests:
135             "ID": {
136                 "name": "Test name",
137                 "parent": "Name of the parent of the test",
138                 "doc": "Test documentation",
139                 "msg": "Test message",
140                 "tags": ["tag 1", "tag 2", "tag n"],
141                 "type": "TCP",
142                 "status": "PASS" | "FAIL",
143                 "result": int
144             }
145
146             # MRR, BMRR tests:
147             "ID": {
148                 "name": "Test name",
149                 "parent": "Name of the parent of the test",
150                 "doc": "Test documentation",
151                 "msg": "Test message",
152                 "tags": ["tag 1", "tag 2", "tag n"],
153                 "type": "MRR" | "BMRR",
154                 "status": "PASS" | "FAIL",
155                 "result": {
156                     "receive-rate": float,
157                     # Average of a list, computed using AvgStdevStats.
158                     # In CSIT-1180, replace with List[float].
159                 }
160             }
161
162             "ID" {
163                 # next test
164             }
165         }
166     }
167
168
169     Functional tests:
170
171     {
172         "metadata": {  # Optional
173             "version": "VPP version",
174             "job": "Jenkins job name",
175             "build": "Information about the build"
176         },
177         "suites": {
178             "Suite name 1": {
179                 "doc": "Suite 1 documentation",
180                 "parent": "Suite 1 parent",
181                 "level": "Level of the suite in the suite hierarchy"
182             }
183             "Suite name N": {
184                 "doc": "Suite N documentation",
185                 "parent": "Suite 2 parent",
186                 "level": "Level of the suite in the suite hierarchy"
187             }
188         }
189         "tests": {
190             "ID": {
191                 "name": "Test name",
192                 "parent": "Name of the parent of the test",
193                 "doc": "Test documentation"
194                 "msg": "Test message"
195                 "tags": ["tag 1", "tag 2", "tag n"],
196                 "conf-history": "DUT1 and DUT2 VAT History"
197                 "show-run": "Show Run"
198                 "status": "PASS" | "FAIL"
199             },
200             "ID" {
201                 # next test
202             }
203         }
204     }
205
206     .. note:: ID is the lowercase full path to the test.
207     """
208
209     REGEX_PLR_RATE = re.compile(
210         r'PLRsearch lower bound::?\s(\d+.\d+).*\n'
211         r'PLRsearch upper bound::?\s(\d+.\d+)'
212     )
213     REGEX_NDRPDR_RATE = re.compile(
214         r'NDR_LOWER:\s(\d+.\d+).*\n.*\n'
215         r'NDR_UPPER:\s(\d+.\d+).*\n'
216         r'PDR_LOWER:\s(\d+.\d+).*\n.*\n'
217         r'PDR_UPPER:\s(\d+.\d+)'
218     )
219     REGEX_NDRPDR_GBPS = re.compile(
220         r'NDR_LOWER:.*,\s(\d+.\d+).*\n.*\n'
221         r'NDR_UPPER:.*,\s(\d+.\d+).*\n'
222         r'PDR_LOWER:.*,\s(\d+.\d+).*\n.*\n'
223         r'PDR_UPPER:.*,\s(\d+.\d+)'
224     )
225     REGEX_PERF_MSG_INFO = re.compile(
226         r'NDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*\n'
227         r'PDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*\n'
228         r'Latency at 90% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
229         r'Latency at 50% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
230         r'Latency at 10% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
231     )
232     REGEX_CPS_MSG_INFO = re.compile(
233         r'NDR_LOWER:\s(\d+.\d+)\s.*\s.*\n.*\n.*\n'
234         r'PDR_LOWER:\s(\d+.\d+)\s.*\s.*\n.*\n.*'
235     )
236     REGEX_PPS_MSG_INFO = re.compile(
237         r'NDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*\n'
238         r'PDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*'
239     )
240     REGEX_MRR_MSG_INFO = re.compile(r'.*\[(.*)\]')
241
242     REGEX_VSAP_MSG_INFO = re.compile(
243         r'Transfer Rate: (\d*.\d*).*\n'
244         r'Latency: (\d*.\d*).*\n'
245         r'Completed requests: (\d*).*\n'
246         r'Failed requests: (\d*).*\n'
247         r'Total data transferred: (\d*).*\n'
248         r'Connection [cr]ps rate:\s*(\d*.\d*)'
249     )
250
251     # Needed for CPS and PPS tests
252     REGEX_NDRPDR_LAT_BASE = re.compile(
253         r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
254         r'LATENCY.*\[\'(.*)\', \'(.*)\'\]'
255     )
256     REGEX_NDRPDR_LAT = re.compile(
257         r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
258         r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n'
259         r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
260         r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
261         r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
262         r'Latency.*\[\'(.*)\', \'(.*)\'\]'
263     )
264
265     REGEX_VERSION_VPP = re.compile(
266         r"(return STDOUT Version:\s*|"
267         r"VPP Version:\s*|VPP version:\s*)(.*)"
268     )
269     REGEX_VERSION_DPDK = re.compile(
270         r"(DPDK version:\s*|DPDK Version:\s*)(.*)"
271     )
272     REGEX_TCP = re.compile(
273         r'Total\s(rps|cps|throughput):\s(\d*).*$'
274     )
275     REGEX_MRR = re.compile(
276         r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
277         r'tx\s(\d*),\srx\s(\d*)'
278     )
279     REGEX_BMRR = re.compile(
280         r'.*trial results.*: \[(.*)\]'
281     )
282     REGEX_RECONF_LOSS = re.compile(
283         r'Packets lost due to reconfig: (\d*)'
284     )
285     REGEX_RECONF_TIME = re.compile(
286         r'Implied time lost: (\d*.[\de-]*)'
287     )
288     REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
289
290     REGEX_TC_NAME_NEW = re.compile(r'-\d+[cC]-')
291
292     REGEX_TC_NUMBER = re.compile(r'tc\d{2}-')
293
294     REGEX_TC_PAPI_CLI = re.compile(r'.*\((\d+.\d+.\d+.\d+.) - (.*)\)')
295
296     REGEX_SH_RUN_HOST = re.compile(
297         r'hostname=\"(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\",hook=\"(.*)\"'
298     )
299
300     def __init__(self, metadata, mapping, ignore, for_output):
301         """Initialisation.
302
303         :param metadata: Key-value pairs to be included in "metadata" part of
304             JSON structure.
305         :param mapping: Mapping of the old names of test cases to the new
306             (actual) one.
307         :param ignore: List of TCs to be ignored.
308         :param for_output: Output to be generated from downloaded data.
309         :type metadata: dict
310         :type mapping: dict
311         :type ignore: list
312         :type for_output: str
313         """
314
315         # Type of message to parse out from the test messages
316         self._msg_type = None
317
318         # VPP version
319         self._version = None
320
321         # Timestamp
322         self._timestamp = None
323
324         # Testbed. The testbed is identified by TG node IP address.
325         self._testbed = None
326
327         # Mapping of TCs long names
328         self._mapping = mapping
329
330         # Ignore list
331         self._ignore = ignore
332
333         self._for_output = for_output
334
335         # Number of PAPI History messages found:
336         # 0 - no message
337         # 1 - PAPI History of DUT1
338         # 2 - PAPI History of DUT2
339         self._conf_history_lookup_nr = 0
340
341         self._sh_run_counter = 0
342         self._telemetry_kw_counter = 0
343         self._telemetry_msg_counter = 0
344
345         # Test ID of currently processed test- the lowercase full path to the
346         # test
347         self._test_id = None
348
349         # The main data structure
350         self._data = {
351             u"metadata": OrderedDict(),
352             u"suites": OrderedDict(),
353             u"tests": OrderedDict()
354         }
355
356         # Save the provided metadata
357         for key, val in metadata.items():
358             self._data[u"metadata"][key] = val
359
360         # Dictionary defining the methods used to parse different types of
361         # messages
362         self.parse_msg = {
363             u"vpp-version": self._get_vpp_version,
364             u"dpdk-version": self._get_dpdk_version,
365             u"teardown-papi-history": self._get_papi_history,
366             u"test-show-runtime": self._get_show_run,
367             u"testbed": self._get_testbed,
368             u"test-telemetry": self._get_telemetry
369         }
370
371     @property
372     def data(self):
373         """Getter - Data parsed from the XML file.
374
375         :returns: Data parsed from the XML file.
376         :rtype: dict
377         """
378         return self._data
379
380     def _get_data_from_mrr_test_msg(self, msg):
381         """Get info from message of MRR performance tests.
382
383         :param msg: Message to be processed.
384         :type msg: str
385         :returns: Processed message or original message if a problem occurs.
386         :rtype: str
387         """
388
389         groups = re.search(self.REGEX_MRR_MSG_INFO, msg)
390         if not groups or groups.lastindex != 1:
391             return u"Test Failed."
392
393         try:
394             data = groups.group(1).split(u", ")
395         except (AttributeError, IndexError, ValueError, KeyError):
396             return u"Test Failed."
397
398         out_str = u"["
399         try:
400             for item in data:
401                 out_str += f"{(float(item) / 1e6):.2f}, "
402             return out_str[:-2] + u"]"
403         except (AttributeError, IndexError, ValueError, KeyError):
404             return u"Test Failed."
405
406     def _get_data_from_cps_test_msg(self, msg):
407         """Get info from message of NDRPDR CPS tests.
408
409         :param msg: Message to be processed.
410         :type msg: str
411         :returns: Processed message or "Test Failed." if a problem occurs.
412         :rtype: str
413         """
414
415         groups = re.search(self.REGEX_CPS_MSG_INFO, msg)
416         if not groups or groups.lastindex != 2:
417             return u"Test Failed."
418
419         try:
420             return (
421                 f"1. {(float(groups.group(1)) / 1e6):5.2f}\n"
422                 f"2. {(float(groups.group(2)) / 1e6):5.2f}"
423             )
424         except (AttributeError, IndexError, ValueError, KeyError):
425             return u"Test Failed."
426
427     def _get_data_from_pps_test_msg(self, msg):
428         """Get info from message of NDRPDR PPS tests.
429
430         :param msg: Message to be processed.
431         :type msg: str
432         :returns: Processed message or "Test Failed." if a problem occurs.
433         :rtype: str
434         """
435
436         groups = re.search(self.REGEX_PPS_MSG_INFO, msg)
437         if not groups or groups.lastindex != 4:
438             return u"Test Failed."
439
440         try:
441             return (
442                 f"1. {(float(groups.group(1)) / 1e6):5.2f}      "
443                 f"{float(groups.group(2)):5.2f}\n"
444                 f"2. {(float(groups.group(3)) / 1e6):5.2f}      "
445                 f"{float(groups.group(4)):5.2f}"
446             )
447         except (AttributeError, IndexError, ValueError, KeyError):
448             return u"Test Failed."
449
450     def _get_data_from_perf_test_msg(self, msg):
451         """Get info from message of NDRPDR performance tests.
452
453         :param msg: Message to be processed.
454         :type msg: str
455         :returns: Processed message or "Test Failed." if a problem occurs.
456         :rtype: str
457         """
458
459         groups = re.search(self.REGEX_PERF_MSG_INFO, msg)
460         if not groups or groups.lastindex != 10:
461             return u"Test Failed."
462
463         try:
464             data = {
465                 u"ndr_low": float(groups.group(1)),
466                 u"ndr_low_b": float(groups.group(2)),
467                 u"pdr_low": float(groups.group(3)),
468                 u"pdr_low_b": float(groups.group(4)),
469                 u"pdr_lat_90_1": groups.group(5),
470                 u"pdr_lat_90_2": groups.group(6),
471                 u"pdr_lat_50_1": groups.group(7),
472                 u"pdr_lat_50_2": groups.group(8),
473                 u"pdr_lat_10_1": groups.group(9),
474                 u"pdr_lat_10_2": groups.group(10),
475             }
476         except (AttributeError, IndexError, ValueError, KeyError):
477             return u"Test Failed."
478
479         def _process_lat(in_str_1, in_str_2):
480             """Extract min, avg, max values from latency string.
481
482             :param in_str_1: Latency string for one direction produced by robot
483                 framework.
484             :param in_str_2: Latency string for second direction produced by
485                 robot framework.
486             :type in_str_1: str
487             :type in_str_2: str
488             :returns: Processed latency string or None if a problem occurs.
489             :rtype: tuple
490             """
491             in_list_1 = in_str_1.split('/', 3)
492             in_list_2 = in_str_2.split('/', 3)
493
494             if len(in_list_1) != 4 and len(in_list_2) != 4:
495                 return None
496
497             in_list_1[3] += u"=" * (len(in_list_1[3]) % 4)
498             try:
499                 hdr_lat_1 = hdrh.histogram.HdrHistogram.decode(in_list_1[3])
500             except hdrh.codec.HdrLengthException:
501                 return None
502
503             in_list_2[3] += u"=" * (len(in_list_2[3]) % 4)
504             try:
505                 hdr_lat_2 = hdrh.histogram.HdrHistogram.decode(in_list_2[3])
506             except hdrh.codec.HdrLengthException:
507                 return None
508
509             if hdr_lat_1 and hdr_lat_2:
510                 hdr_lat = (
511                     hdr_lat_1.get_value_at_percentile(50.0),
512                     hdr_lat_1.get_value_at_percentile(90.0),
513                     hdr_lat_1.get_value_at_percentile(99.0),
514                     hdr_lat_2.get_value_at_percentile(50.0),
515                     hdr_lat_2.get_value_at_percentile(90.0),
516                     hdr_lat_2.get_value_at_percentile(99.0)
517                 )
518
519                 if all(hdr_lat):
520                     return hdr_lat
521
522             return None
523
524         try:
525             out_msg = (
526                 f"1. {(data[u'ndr_low'] / 1e6):5.2f}      "
527                 f"{data[u'ndr_low_b']:5.2f}"
528                 f"\n2. {(data[u'pdr_low'] / 1e6):5.2f}      "
529                 f"{data[u'pdr_low_b']:5.2f}"
530             )
531             latency = (
532                 _process_lat(data[u'pdr_lat_10_1'], data[u'pdr_lat_10_2']),
533                 _process_lat(data[u'pdr_lat_50_1'], data[u'pdr_lat_50_2']),
534                 _process_lat(data[u'pdr_lat_90_1'], data[u'pdr_lat_90_2'])
535             )
536             if all(latency):
537                 max_len = len(str(max((max(item) for item in latency))))
538                 max_len = 4 if max_len < 4 else max_len
539
540                 for idx, lat in enumerate(latency):
541                     if not idx:
542                         out_msg += u"\n"
543                     out_msg += (
544                         f"\n{idx + 3}. "
545                         f"{lat[0]:{max_len}d} "
546                         f"{lat[1]:{max_len}d} "
547                         f"{lat[2]:{max_len}d}      "
548                         f"{lat[3]:{max_len}d} "
549                         f"{lat[4]:{max_len}d} "
550                         f"{lat[5]:{max_len}d} "
551                     )
552
553             return out_msg
554
555         except (AttributeError, IndexError, ValueError, KeyError):
556             return u"Test Failed."
557
558     def _get_testbed(self, msg):
559         """Called when extraction of testbed IP is required.
560         The testbed is identified by TG node IP address.
561
562         :param msg: Message to process.
563         :type msg: Message
564         :returns: Nothing.
565         """
566
567         if msg.message.count(u"Setup of TG node") or \
568                 msg.message.count(u"Setup of node TG host"):
569             reg_tg_ip = re.compile(
570                 r'.*TG .* (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*')
571             try:
572                 self._testbed = str(re.search(reg_tg_ip, msg.message).group(1))
573             except (KeyError, ValueError, IndexError, AttributeError):
574                 pass
575             finally:
576                 self._data[u"metadata"][u"testbed"] = self._testbed
577                 self._msg_type = None
578
579     def _get_vpp_version(self, msg):
580         """Called when extraction of VPP version is required.
581
582         :param msg: Message to process.
583         :type msg: Message
584         :returns: Nothing.
585         """
586
587         if msg.message.count(u"return STDOUT Version:") or \
588                 msg.message.count(u"VPP Version:") or \
589                 msg.message.count(u"VPP version:"):
590             self._version = str(
591                 re.search(self.REGEX_VERSION_VPP, msg.message).group(2)
592             )
593             self._data[u"metadata"][u"version"] = self._version
594             self._msg_type = None
595
596     def _get_dpdk_version(self, msg):
597         """Called when extraction of DPDK version is required.
598
599         :param msg: Message to process.
600         :type msg: Message
601         :returns: Nothing.
602         """
603
604         if msg.message.count(u"DPDK Version:"):
605             try:
606                 self._version = str(re.search(
607                     self.REGEX_VERSION_DPDK, msg.message).group(2))
608                 self._data[u"metadata"][u"version"] = self._version
609             except IndexError:
610                 pass
611             finally:
612                 self._msg_type = None
613
614     def _get_papi_history(self, msg):
615         """Called when extraction of PAPI command history is required.
616
617         :param msg: Message to process.
618         :type msg: Message
619         :returns: Nothing.
620         """
621         if msg.message.count(u"PAPI command history:"):
622             self._conf_history_lookup_nr += 1
623             if self._conf_history_lookup_nr == 1:
624                 self._data[u"tests"][self._test_id][u"conf-history"] = str()
625             else:
626                 self._msg_type = None
627             text = re.sub(
628                 r"\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} PAPI command history:",
629                 u"",
630                 msg.message,
631                 count=1
632             ).replace(u'"', u"'")
633             self._data[u"tests"][self._test_id][u"conf-history"] += (
634                 f"**DUT{str(self._conf_history_lookup_nr)}:** {text}"
635             )
636
637     def _get_show_run(self, msg):
638         """Called when extraction of VPP operational data (output of CLI command
639         Show Runtime) is required.
640
641         :param msg: Message to process.
642         :type msg: Message
643         :returns: Nothing.
644         """
645
646         if not msg.message.count(u"stats runtime"):
647             return
648
649         # Temporary solution
650         if self._sh_run_counter > 1:
651             return
652
653         if u"show-run" not in self._data[u"tests"][self._test_id].keys():
654             self._data[u"tests"][self._test_id][u"show-run"] = dict()
655
656         groups = re.search(self.REGEX_TC_PAPI_CLI, msg.message)
657         if not groups:
658             return
659         try:
660             host = groups.group(1)
661         except (AttributeError, IndexError):
662             host = u""
663         try:
664             sock = groups.group(2)
665         except (AttributeError, IndexError):
666             sock = u""
667
668         dut = u"dut{nr}".format(
669             nr=len(self._data[u'tests'][self._test_id][u'show-run'].keys()) + 1)
670
671         self._data[u'tests'][self._test_id][u'show-run'][dut] = \
672             copy.copy(
673                 {
674                     u"host": host,
675                     u"socket": sock,
676                     u"runtime": str(msg.message).replace(u' ', u'').
677                                 replace(u'\n', u'').replace(u"'", u'"').
678                                 replace(u'b"', u'"').replace(u'u"', u'"').
679                                 split(u":", 1)[1]
680                 }
681             )
682
683     def _get_telemetry(self, msg):
684         """Called when extraction of VPP telemetry data is required.
685
686         :param msg: Message to process.
687         :type msg: Message
688         :returns: Nothing.
689         """
690
691         if self._telemetry_kw_counter > 1:
692             return
693         if not msg.message.count(u"# TYPE vpp_runtime_calls"):
694             return
695
696         if u"telemetry-show-run" not in \
697                 self._data[u"tests"][self._test_id].keys():
698             self._data[u"tests"][self._test_id][u"telemetry-show-run"] = dict()
699
700         self._telemetry_msg_counter += 1
701         groups = re.search(self.REGEX_SH_RUN_HOST, msg.message)
702         if not groups:
703             return
704         try:
705             host = groups.group(1)
706         except (AttributeError, IndexError):
707             host = u""
708         try:
709             sock = groups.group(2)
710         except (AttributeError, IndexError):
711             sock = u""
712         runtime = {
713             u"source_type": u"node",
714             u"source_id": host,
715             u"msg_type": u"metric",
716             u"log_level": u"INFO",
717             u"timestamp": msg.timestamp,
718             u"msg": u"show_runtime",
719             u"host": host,
720             u"socket": sock,
721             u"data": list()
722         }
723         for line in msg.message.splitlines():
724             if not line.startswith(u"vpp_runtime_"):
725                 continue
726             try:
727                 params, value, timestamp = line.rsplit(u" ", maxsplit=2)
728                 cut = params.index(u"{")
729                 name = params[:cut].split(u"_", maxsplit=2)[-1]
730                 labels = eval(
731                     u"dict" + params[cut:].replace('{', '(').replace('}', ')')
732                 )
733                 labels[u"graph_node"] = labels.pop(u"name")
734                 runtime[u"data"].append(
735                     {
736                         u"name": name,
737                         u"value": value,
738                         u"timestamp": timestamp,
739                         u"labels": labels
740                     }
741                 )
742             except (TypeError, ValueError, IndexError):
743                 continue
744         self._data[u'tests'][self._test_id][u'telemetry-show-run']\
745             [f"dut{self._telemetry_msg_counter}"] = copy.copy(
746                 {
747                     u"host": host,
748                     u"socket": sock,
749                     u"runtime": runtime
750                 }
751             )
752
753     def _get_ndrpdr_throughput(self, msg):
754         """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
755         message.
756
757         :param msg: The test message to be parsed.
758         :type msg: str
759         :returns: Parsed data as a dict and the status (PASS/FAIL).
760         :rtype: tuple(dict, str)
761         """
762
763         throughput = {
764             u"NDR": {u"LOWER": -1.0, u"UPPER": -1.0},
765             u"PDR": {u"LOWER": -1.0, u"UPPER": -1.0}
766         }
767         status = u"FAIL"
768         groups = re.search(self.REGEX_NDRPDR_RATE, msg)
769
770         if groups is not None:
771             try:
772                 throughput[u"NDR"][u"LOWER"] = float(groups.group(1))
773                 throughput[u"NDR"][u"UPPER"] = float(groups.group(2))
774                 throughput[u"PDR"][u"LOWER"] = float(groups.group(3))
775                 throughput[u"PDR"][u"UPPER"] = float(groups.group(4))
776                 status = u"PASS"
777             except (IndexError, ValueError):
778                 pass
779
780         return throughput, status
781
782     def _get_ndrpdr_throughput_gbps(self, msg):
783         """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER in Gbps from the
784         test message.
785
786         :param msg: The test message to be parsed.
787         :type msg: str
788         :returns: Parsed data as a dict and the status (PASS/FAIL).
789         :rtype: tuple(dict, str)
790         """
791
792         gbps = {
793             u"NDR": {u"LOWER": -1.0, u"UPPER": -1.0},
794             u"PDR": {u"LOWER": -1.0, u"UPPER": -1.0}
795         }
796         status = u"FAIL"
797         groups = re.search(self.REGEX_NDRPDR_GBPS, msg)
798
799         if groups is not None:
800             try:
801                 gbps[u"NDR"][u"LOWER"] = float(groups.group(1))
802                 gbps[u"NDR"][u"UPPER"] = float(groups.group(2))
803                 gbps[u"PDR"][u"LOWER"] = float(groups.group(3))
804                 gbps[u"PDR"][u"UPPER"] = float(groups.group(4))
805                 status = u"PASS"
806             except (IndexError, ValueError):
807                 pass
808
809         return gbps, status
810
811     def _get_plr_throughput(self, msg):
812         """Get PLRsearch lower bound and PLRsearch upper bound from the test
813         message.
814
815         :param msg: The test message to be parsed.
816         :type msg: str
817         :returns: Parsed data as a dict and the status (PASS/FAIL).
818         :rtype: tuple(dict, str)
819         """
820
821         throughput = {
822             u"LOWER": -1.0,
823             u"UPPER": -1.0
824         }
825         status = u"FAIL"
826         groups = re.search(self.REGEX_PLR_RATE, msg)
827
828         if groups is not None:
829             try:
830                 throughput[u"LOWER"] = float(groups.group(1))
831                 throughput[u"UPPER"] = float(groups.group(2))
832                 status = u"PASS"
833             except (IndexError, ValueError):
834                 pass
835
836         return throughput, status
837
838     def _get_ndrpdr_latency(self, msg):
839         """Get LATENCY from the test message.
840
841         :param msg: The test message to be parsed.
842         :type msg: str
843         :returns: Parsed data as a dict and the status (PASS/FAIL).
844         :rtype: tuple(dict, str)
845         """
846         latency_default = {
847             u"min": -1.0,
848             u"avg": -1.0,
849             u"max": -1.0,
850             u"hdrh": u""
851         }
852         latency = {
853             u"NDR": {
854                 u"direction1": copy.copy(latency_default),
855                 u"direction2": copy.copy(latency_default)
856             },
857             u"PDR": {
858                 u"direction1": copy.copy(latency_default),
859                 u"direction2": copy.copy(latency_default)
860             },
861             u"LAT0": {
862                 u"direction1": copy.copy(latency_default),
863                 u"direction2": copy.copy(latency_default)
864             },
865             u"PDR10": {
866                 u"direction1": copy.copy(latency_default),
867                 u"direction2": copy.copy(latency_default)
868             },
869             u"PDR50": {
870                 u"direction1": copy.copy(latency_default),
871                 u"direction2": copy.copy(latency_default)
872             },
873             u"PDR90": {
874                 u"direction1": copy.copy(latency_default),
875                 u"direction2": copy.copy(latency_default)
876             },
877         }
878
879         groups = re.search(self.REGEX_NDRPDR_LAT, msg)
880         if groups is None:
881             groups = re.search(self.REGEX_NDRPDR_LAT_BASE, msg)
882         if groups is None:
883             return latency, u"FAIL"
884
885         def process_latency(in_str):
886             """Return object with parsed latency values.
887
888             TODO: Define class for the return type.
889
890             :param in_str: Input string, min/avg/max/hdrh format.
891             :type in_str: str
892             :returns: Dict with corresponding keys, except hdrh float values.
893             :rtype dict:
894             :throws IndexError: If in_str does not have enough substrings.
895             :throws ValueError: If a substring does not convert to float.
896             """
897             in_list = in_str.split('/', 3)
898
899             rval = {
900                 u"min": float(in_list[0]),
901                 u"avg": float(in_list[1]),
902                 u"max": float(in_list[2]),
903                 u"hdrh": u""
904             }
905
906             if len(in_list) == 4:
907                 rval[u"hdrh"] = str(in_list[3])
908
909             return rval
910
911         try:
912             latency[u"NDR"][u"direction1"] = process_latency(groups.group(1))
913             latency[u"NDR"][u"direction2"] = process_latency(groups.group(2))
914             latency[u"PDR"][u"direction1"] = process_latency(groups.group(3))
915             latency[u"PDR"][u"direction2"] = process_latency(groups.group(4))
916             if groups.lastindex == 4:
917                 return latency, u"PASS"
918         except (IndexError, ValueError):
919             pass
920
921         try:
922             latency[u"PDR90"][u"direction1"] = process_latency(groups.group(5))
923             latency[u"PDR90"][u"direction2"] = process_latency(groups.group(6))
924             latency[u"PDR50"][u"direction1"] = process_latency(groups.group(7))
925             latency[u"PDR50"][u"direction2"] = process_latency(groups.group(8))
926             latency[u"PDR10"][u"direction1"] = process_latency(groups.group(9))
927             latency[u"PDR10"][u"direction2"] = process_latency(groups.group(10))
928             latency[u"LAT0"][u"direction1"] = process_latency(groups.group(11))
929             latency[u"LAT0"][u"direction2"] = process_latency(groups.group(12))
930             if groups.lastindex == 12:
931                 return latency, u"PASS"
932         except (IndexError, ValueError):
933             pass
934
935         return latency, u"FAIL"
936
937     @staticmethod
938     def _get_hoststack_data(msg, tags):
939         """Get data from the hoststack test message.
940
941         :param msg: The test message to be parsed.
942         :param tags: Test tags.
943         :type msg: str
944         :type tags: list
945         :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
946         :rtype: tuple(dict, str)
947         """
948         result = dict()
949         status = u"FAIL"
950
951         msg = msg.replace(u"'", u'"').replace(u" ", u"")
952         if u"LDPRELOAD" in tags:
953             try:
954                 result = loads(msg)
955                 status = u"PASS"
956             except JSONDecodeError:
957                 pass
958         elif u"VPPECHO" in tags:
959             try:
960                 msg_lst = msg.replace(u"}{", u"} {").split(u" ")
961                 result = dict(
962                     client=loads(msg_lst[0]),
963                     server=loads(msg_lst[1])
964                 )
965                 status = u"PASS"
966             except (JSONDecodeError, IndexError):
967                 pass
968
969         return result, status
970
971     def _get_vsap_data(self, msg, tags):
972         """Get data from the vsap test message.
973
974         :param msg: The test message to be parsed.
975         :param tags: Test tags.
976         :type msg: str
977         :type tags: list
978         :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
979         :rtype: tuple(dict, str)
980         """
981         result = dict()
982         status = u"FAIL"
983
984         groups = re.search(self.REGEX_VSAP_MSG_INFO, msg)
985         if groups is not None:
986             try:
987                 result[u"transfer-rate"] = float(groups.group(1)) * 1e3
988                 result[u"latency"] = float(groups.group(2))
989                 result[u"completed-requests"] = int(groups.group(3))
990                 result[u"failed-requests"] = int(groups.group(4))
991                 result[u"bytes-transferred"] = int(groups.group(5))
992                 if u"TCP_CPS"in tags:
993                     result[u"cps"] = float(groups.group(6))
994                 elif u"TCP_RPS" in tags:
995                     result[u"rps"] = float(groups.group(6))
996                 else:
997                     return result, status
998                 status = u"PASS"
999             except (IndexError, ValueError):
1000                 pass
1001
1002         return result, status
1003
1004     def visit_suite(self, suite):
1005         """Implements traversing through the suite and its direct children.
1006
1007         :param suite: Suite to process.
1008         :type suite: Suite
1009         :returns: Nothing.
1010         """
1011         if self.start_suite(suite) is not False:
1012             suite.suites.visit(self)
1013             suite.tests.visit(self)
1014             self.end_suite(suite)
1015
1016     def start_suite(self, suite):
1017         """Called when suite starts.
1018
1019         :param suite: Suite to process.
1020         :type suite: Suite
1021         :returns: Nothing.
1022         """
1023
1024         try:
1025             parent_name = suite.parent.name
1026         except AttributeError:
1027             return
1028
1029         self._data[u"suites"][suite.longname.lower().
1030                               replace(u'"', u"'").
1031                               replace(u" ", u"_")] = {
1032                                   u"name": suite.name.lower(),
1033                                   u"doc": suite.doc,
1034                                   u"parent": parent_name,
1035                                   u"level": len(suite.longname.split(u"."))
1036                               }
1037
1038         suite.keywords.visit(self)
1039
1040     def end_suite(self, suite):
1041         """Called when suite ends.
1042
1043         :param suite: Suite to process.
1044         :type suite: Suite
1045         :returns: Nothing.
1046         """
1047
1048     def visit_test(self, test):
1049         """Implements traversing through the test.
1050
1051         :param test: Test to process.
1052         :type test: Test
1053         :returns: Nothing.
1054         """
1055         if self.start_test(test) is not False:
1056             test.keywords.visit(self)
1057             self.end_test(test)
1058
1059     def start_test(self, test):
1060         """Called when test starts.
1061
1062         :param test: Test to process.
1063         :type test: Test
1064         :returns: Nothing.
1065         """
1066
1067         self._sh_run_counter = 0
1068         self._telemetry_kw_counter = 0
1069         self._telemetry_msg_counter = 0
1070
1071         longname_orig = test.longname.lower()
1072
1073         # Check the ignore list
1074         if longname_orig in self._ignore:
1075             return
1076
1077         tags = [str(tag) for tag in test.tags]
1078         test_result = dict()
1079
1080         # Change the TC long name and name if defined in the mapping table
1081         longname = self._mapping.get(longname_orig, None)
1082         if longname is not None:
1083             name = longname.split(u'.')[-1]
1084             logging.debug(
1085                 f"{self._data[u'metadata']}\n{longname_orig}\n{longname}\n"
1086                 f"{name}"
1087             )
1088         else:
1089             longname = longname_orig
1090             name = test.name.lower()
1091
1092         # Remove TC number from the TC long name (backward compatibility):
1093         self._test_id = re.sub(self.REGEX_TC_NUMBER, u"", longname)
1094         # Remove TC number from the TC name (not needed):
1095         test_result[u"name"] = re.sub(self.REGEX_TC_NUMBER, "", name)
1096
1097         test_result[u"parent"] = test.parent.name.lower()
1098         test_result[u"tags"] = tags
1099         test_result["doc"] = test.doc
1100         test_result[u"type"] = u""
1101         test_result[u"status"] = test.status
1102         test_result[u"starttime"] = test.starttime
1103         test_result[u"endtime"] = test.endtime
1104
1105         if test.status == u"PASS":
1106             if u"NDRPDR" in tags:
1107                 if u"TCP_PPS" in tags or u"UDP_PPS" in tags:
1108                     test_result[u"msg"] = self._get_data_from_pps_test_msg(
1109                         test.message)
1110                 elif u"TCP_CPS" in tags or u"UDP_CPS" in tags:
1111                     test_result[u"msg"] = self._get_data_from_cps_test_msg(
1112                         test.message)
1113                 else:
1114                     test_result[u"msg"] = self._get_data_from_perf_test_msg(
1115                         test.message)
1116             elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
1117                 test_result[u"msg"] = self._get_data_from_mrr_test_msg(
1118                     test.message)
1119             else:
1120                 test_result[u"msg"] = test.message
1121         else:
1122             test_result[u"msg"] = test.message
1123
1124         if u"PERFTEST" in tags and u"TREX" not in tags:
1125             # Replace info about cores (e.g. -1c-) with the info about threads
1126             # and cores (e.g. -1t1c-) in the long test case names and in the
1127             # test case names if necessary.
1128             tag_count = 0
1129             tag_tc = str()
1130             for tag in test_result[u"tags"]:
1131                 groups = re.search(self.REGEX_TC_TAG, tag)
1132                 if groups:
1133                     tag_count += 1
1134                     tag_tc = tag
1135
1136             if tag_count == 1:
1137                 self._test_id = re.sub(
1138                     self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
1139                     self._test_id, count=1
1140                 )
1141                 test_result[u"name"] = re.sub(
1142                     self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
1143                     test_result["name"], count=1
1144                 )
1145             else:
1146                 test_result[u"status"] = u"FAIL"
1147                 self._data[u"tests"][self._test_id] = test_result
1148                 logging.debug(
1149                     f"The test {self._test_id} has no or more than one "
1150                     f"multi-threading tags.\n"
1151                     f"Tags: {test_result[u'tags']}"
1152                 )
1153                 return
1154
1155         if u"DEVICETEST" in tags:
1156             test_result[u"type"] = u"DEVICETEST"
1157         elif u"NDRPDR" in tags:
1158             if u"TCP_CPS" in tags or u"UDP_CPS" in tags:
1159                 test_result[u"type"] = u"CPS"
1160             else:
1161                 test_result[u"type"] = u"NDRPDR"
1162             if test.status == u"PASS":
1163                 test_result[u"throughput"], test_result[u"status"] = \
1164                     self._get_ndrpdr_throughput(test.message)
1165                 test_result[u"gbps"], test_result[u"status"] = \
1166                     self._get_ndrpdr_throughput_gbps(test.message)
1167                 test_result[u"latency"], test_result[u"status"] = \
1168                     self._get_ndrpdr_latency(test.message)
1169         elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
1170             if u"MRR" in tags:
1171                 test_result[u"type"] = u"MRR"
1172             else:
1173                 test_result[u"type"] = u"BMRR"
1174             if test.status == u"PASS":
1175                 test_result[u"result"] = dict()
1176                 groups = re.search(self.REGEX_BMRR, test.message)
1177                 if groups is not None:
1178                     items_str = groups.group(1)
1179                     items_float = [
1180                         float(item.strip().replace(u"'", u""))
1181                         for item in items_str.split(",")
1182                     ]
1183                     # Use whole list in CSIT-1180.
1184                     stats = jumpavg.AvgStdevStats.for_runs(items_float)
1185                     test_result[u"result"][u"samples"] = items_float
1186                     test_result[u"result"][u"receive-rate"] = stats.avg
1187                     test_result[u"result"][u"receive-stdev"] = stats.stdev
1188                 else:
1189                     groups = re.search(self.REGEX_MRR, test.message)
1190                     test_result[u"result"][u"receive-rate"] = \
1191                         float(groups.group(3)) / float(groups.group(1))
1192         elif u"SOAK" in tags:
1193             test_result[u"type"] = u"SOAK"
1194             if test.status == u"PASS":
1195                 test_result[u"throughput"], test_result[u"status"] = \
1196                     self._get_plr_throughput(test.message)
1197         elif u"HOSTSTACK" in tags:
1198             test_result[u"type"] = u"HOSTSTACK"
1199             if test.status == u"PASS":
1200                 test_result[u"result"], test_result[u"status"] = \
1201                     self._get_hoststack_data(test.message, tags)
1202         elif u"LDP_NGINX" in tags:
1203             test_result[u"type"] = u"LDP_NGINX"
1204             test_result[u"result"], test_result[u"status"] = \
1205                 self._get_vsap_data(test.message, tags)
1206         # elif u"TCP" in tags:  # This might be not used
1207         #     test_result[u"type"] = u"TCP"
1208         #     if test.status == u"PASS":
1209         #         groups = re.search(self.REGEX_TCP, test.message)
1210         #         test_result[u"result"] = int(groups.group(2))
1211         elif u"RECONF" in tags:
1212             test_result[u"type"] = u"RECONF"
1213             if test.status == u"PASS":
1214                 test_result[u"result"] = None
1215                 try:
1216                     grps_loss = re.search(self.REGEX_RECONF_LOSS, test.message)
1217                     grps_time = re.search(self.REGEX_RECONF_TIME, test.message)
1218                     test_result[u"result"] = {
1219                         u"loss": int(grps_loss.group(1)),
1220                         u"time": float(grps_time.group(1))
1221                     }
1222                 except (AttributeError, IndexError, ValueError, TypeError):
1223                     test_result[u"status"] = u"FAIL"
1224         else:
1225             test_result[u"status"] = u"FAIL"
1226
1227         self._data[u"tests"][self._test_id] = test_result
1228
1229     def end_test(self, test):
1230         """Called when test ends.
1231
1232         :param test: Test to process.
1233         :type test: Test
1234         :returns: Nothing.
1235         """
1236
1237     def visit_keyword(self, keyword):
1238         """Implements traversing through the keyword and its child keywords.
1239
1240         :param keyword: Keyword to process.
1241         :type keyword: Keyword
1242         :returns: Nothing.
1243         """
1244         if self.start_keyword(keyword) is not False:
1245             self.end_keyword(keyword)
1246
1247     def start_keyword(self, keyword):
1248         """Called when keyword starts. Default implementation does nothing.
1249
1250         :param keyword: Keyword to process.
1251         :type keyword: Keyword
1252         :returns: Nothing.
1253         """
1254         try:
1255             if keyword.type == u"setup":
1256                 self.visit_setup_kw(keyword)
1257             elif keyword.type == u"teardown":
1258                 self.visit_teardown_kw(keyword)
1259             else:
1260                 self.visit_test_kw(keyword)
1261         except AttributeError:
1262             pass
1263
1264     def end_keyword(self, keyword):
1265         """Called when keyword ends. Default implementation does nothing.
1266
1267         :param keyword: Keyword to process.
1268         :type keyword: Keyword
1269         :returns: Nothing.
1270         """
1271
1272     def visit_test_kw(self, test_kw):
1273         """Implements traversing through the test keyword and its child
1274         keywords.
1275
1276         :param test_kw: Keyword to process.
1277         :type test_kw: Keyword
1278         :returns: Nothing.
1279         """
1280         for keyword in test_kw.keywords:
1281             if self.start_test_kw(keyword) is not False:
1282                 self.visit_test_kw(keyword)
1283                 self.end_test_kw(keyword)
1284
1285     def start_test_kw(self, test_kw):
1286         """Called when test keyword starts. Default implementation does
1287         nothing.
1288
1289         :param test_kw: Keyword to process.
1290         :type test_kw: Keyword
1291         :returns: Nothing.
1292         """
1293         if self._for_output == u"trending":
1294             return
1295
1296         if test_kw.name.count(u"Run Telemetry On All Duts"):
1297             self._msg_type = u"test-telemetry"
1298             self._telemetry_kw_counter += 1
1299         elif test_kw.name.count(u"Show Runtime On All Duts"):
1300             self._msg_type = u"test-show-runtime"
1301             self._sh_run_counter += 1
1302         else:
1303             return
1304         test_kw.messages.visit(self)
1305
1306     def end_test_kw(self, test_kw):
1307         """Called when keyword ends. Default implementation does nothing.
1308
1309         :param test_kw: Keyword to process.
1310         :type test_kw: Keyword
1311         :returns: Nothing.
1312         """
1313
1314     def visit_setup_kw(self, setup_kw):
1315         """Implements traversing through the teardown keyword and its child
1316         keywords.
1317
1318         :param setup_kw: Keyword to process.
1319         :type setup_kw: Keyword
1320         :returns: Nothing.
1321         """
1322         for keyword in setup_kw.keywords:
1323             if self.start_setup_kw(keyword) is not False:
1324                 self.visit_setup_kw(keyword)
1325                 self.end_setup_kw(keyword)
1326
1327     def start_setup_kw(self, setup_kw):
1328         """Called when teardown keyword starts. Default implementation does
1329         nothing.
1330
1331         :param setup_kw: Keyword to process.
1332         :type setup_kw: Keyword
1333         :returns: Nothing.
1334         """
1335         if setup_kw.name.count(u"Show Vpp Version On All Duts") \
1336                 and not self._version:
1337             self._msg_type = u"vpp-version"
1338         elif setup_kw.name.count(u"Install Dpdk Framework On All Duts") and \
1339                 not self._version:
1340             self._msg_type = u"dpdk-version"
1341         elif setup_kw.name.count(u"Setup Framework") and not self._testbed:
1342             self._msg_type = u"testbed"
1343         else:
1344             return
1345         setup_kw.messages.visit(self)
1346
1347     def end_setup_kw(self, setup_kw):
1348         """Called when keyword ends. Default implementation does nothing.
1349
1350         :param setup_kw: Keyword to process.
1351         :type setup_kw: Keyword
1352         :returns: Nothing.
1353         """
1354
1355     def visit_teardown_kw(self, teardown_kw):
1356         """Implements traversing through the teardown keyword and its child
1357         keywords.
1358
1359         :param teardown_kw: Keyword to process.
1360         :type teardown_kw: Keyword
1361         :returns: Nothing.
1362         """
1363         for keyword in teardown_kw.keywords:
1364             if self.start_teardown_kw(keyword) is not False:
1365                 self.visit_teardown_kw(keyword)
1366                 self.end_teardown_kw(keyword)
1367
1368     def start_teardown_kw(self, teardown_kw):
1369         """Called when teardown keyword starts
1370
1371         :param teardown_kw: Keyword to process.
1372         :type teardown_kw: Keyword
1373         :returns: Nothing.
1374         """
1375         if teardown_kw.name.count(u"Show Papi History On All Duts"):
1376             self._conf_history_lookup_nr = 0
1377             self._msg_type = u"teardown-papi-history"
1378             teardown_kw.messages.visit(self)
1379
1380     def end_teardown_kw(self, teardown_kw):
1381         """Called when keyword ends. Default implementation does nothing.
1382
1383         :param teardown_kw: Keyword to process.
1384         :type teardown_kw: Keyword
1385         :returns: Nothing.
1386         """
1387
1388     def visit_message(self, msg):
1389         """Implements visiting the message.
1390
1391         :param msg: Message to process.
1392         :type msg: Message
1393         :returns: Nothing.
1394         """
1395         if self.start_message(msg) is not False:
1396             self.end_message(msg)
1397
1398     def start_message(self, msg):
1399         """Called when message starts. Get required information from messages:
1400         - VPP version.
1401
1402         :param msg: Message to process.
1403         :type msg: Message
1404         :returns: Nothing.
1405         """
1406         if self._msg_type:
1407             self.parse_msg[self._msg_type](msg)
1408
1409     def end_message(self, msg):
1410         """Called when message ends. Default implementation does nothing.
1411
1412         :param msg: Message to process.
1413         :type msg: Message
1414         :returns: Nothing.
1415         """
1416
1417
1418 class InputData:
1419     """Input data
1420
1421     The data is extracted from output.xml files generated by Jenkins jobs and
1422     stored in pandas' DataFrames.
1423
1424     The data structure:
1425     - job name
1426       - build number
1427         - metadata
1428           (as described in ExecutionChecker documentation)
1429         - suites
1430           (as described in ExecutionChecker documentation)
1431         - tests
1432           (as described in ExecutionChecker documentation)
1433     """
1434
1435     def __init__(self, spec, for_output):
1436         """Initialization.
1437
1438         :param spec: Specification.
1439         :param for_output: Output to be generated from downloaded data.
1440         :type spec: Specification
1441         :type for_output: str
1442         """
1443
1444         # Specification:
1445         self._cfg = spec
1446
1447         self._for_output = for_output
1448
1449         # Data store:
1450         self._input_data = pd.Series()
1451
1452     @property
1453     def data(self):
1454         """Getter - Input data.
1455
1456         :returns: Input data
1457         :rtype: pandas.Series
1458         """
1459         return self._input_data
1460
1461     def metadata(self, job, build):
1462         """Getter - metadata
1463
1464         :param job: Job which metadata we want.
1465         :param build: Build which metadata we want.
1466         :type job: str
1467         :type build: str
1468         :returns: Metadata
1469         :rtype: pandas.Series
1470         """
1471         return self.data[job][build][u"metadata"]
1472
1473     def suites(self, job, build):
1474         """Getter - suites
1475
1476         :param job: Job which suites we want.
1477         :param build: Build which suites we want.
1478         :type job: str
1479         :type build: str
1480         :returns: Suites.
1481         :rtype: pandas.Series
1482         """
1483         return self.data[job][str(build)][u"suites"]
1484
1485     def tests(self, job, build):
1486         """Getter - tests
1487
1488         :param job: Job which tests we want.
1489         :param build: Build which tests we want.
1490         :type job: str
1491         :type build: str
1492         :returns: Tests.
1493         :rtype: pandas.Series
1494         """
1495         return self.data[job][build][u"tests"]
1496
1497     def _parse_tests(self, job, build):
1498         """Process data from robot output.xml file and return JSON structured
1499         data.
1500
1501         :param job: The name of job which build output data will be processed.
1502         :param build: The build which output data will be processed.
1503         :type job: str
1504         :type build: dict
1505         :returns: JSON data structure.
1506         :rtype: dict
1507         """
1508
1509         metadata = {
1510             u"job": job,
1511             u"build": build
1512         }
1513
1514         with open(build[u"file-name"], u'r') as data_file:
1515             try:
1516                 result = ExecutionResult(data_file)
1517             except errors.DataError as err:
1518                 logging.error(
1519                     f"Error occurred while parsing output.xml: {repr(err)}"
1520                 )
1521                 return None
1522         checker = ExecutionChecker(
1523             metadata, self._cfg.mapping, self._cfg.ignore, self._for_output
1524         )
1525         result.visit(checker)
1526
1527         checker.data[u"metadata"][u"tests_total"] = \
1528             result.statistics.total.all.total
1529         checker.data[u"metadata"][u"tests_passed"] = \
1530             result.statistics.total.all.passed
1531         checker.data[u"metadata"][u"tests_failed"] = \
1532             result.statistics.total.all.failed
1533         checker.data[u"metadata"][u"elapsedtime"] = result.suite.elapsedtime
1534         checker.data[u"metadata"][u"generated"] = result.suite.endtime[:14]
1535
1536         return checker.data
1537
1538     def _download_and_parse_build(self, job, build, repeat, pid=10000):
1539         """Download and parse the input data file.
1540
1541         :param pid: PID of the process executing this method.
1542         :param job: Name of the Jenkins job which generated the processed input
1543             file.
1544         :param build: Information about the Jenkins build which generated the
1545             processed input file.
1546         :param repeat: Repeat the download specified number of times if not
1547             successful.
1548         :type pid: int
1549         :type job: str
1550         :type build: dict
1551         :type repeat: int
1552         """
1553
1554         logging.info(f"Processing the job/build: {job}: {build[u'build']}")
1555
1556         state = u"failed"
1557         success = False
1558         data = None
1559         do_repeat = repeat
1560         while do_repeat:
1561             success = download_and_unzip_data_file(self._cfg, job, build, pid)
1562             if success:
1563                 break
1564             do_repeat -= 1
1565         if not success:
1566             logging.error(
1567                 f"It is not possible to download the input data file from the "
1568                 f"job {job}, build {build[u'build']}, or it is damaged. "
1569                 f"Skipped."
1570             )
1571         if success:
1572             logging.info(f"  Processing data from build {build[u'build']}")
1573             data = self._parse_tests(job, build)
1574             if data is None:
1575                 logging.error(
1576                     f"Input data file from the job {job}, build "
1577                     f"{build[u'build']} is damaged. Skipped."
1578                 )
1579             else:
1580                 state = u"processed"
1581
1582             try:
1583                 remove(build[u"file-name"])
1584             except OSError as err:
1585                 logging.error(
1586                     f"Cannot remove the file {build[u'file-name']}: {repr(err)}"
1587                 )
1588
1589         # If the time-period is defined in the specification file, remove all
1590         # files which are outside the time period.
1591         is_last = False
1592         timeperiod = self._cfg.environment.get(u"time-period", None)
1593         if timeperiod and data:
1594             now = dt.utcnow()
1595             timeperiod = timedelta(int(timeperiod))
1596             metadata = data.get(u"metadata", None)
1597             if metadata:
1598                 generated = metadata.get(u"generated", None)
1599                 if generated:
1600                     generated = dt.strptime(generated, u"%Y%m%d %H:%M")
1601                     if (now - generated) > timeperiod:
1602                         # Remove the data and the file:
1603                         state = u"removed"
1604                         data = None
1605                         is_last = True
1606                         logging.info(
1607                             f"  The build {job}/{build[u'build']} is "
1608                             f"outdated, will be removed."
1609                         )
1610         return {
1611             u"data": data,
1612             u"state": state,
1613             u"job": job,
1614             u"build": build,
1615             u"last": is_last
1616         }
1617
1618     def download_and_parse_data(self, repeat=1):
1619         """Download the input data files, parse input data from input files and
1620         store in pandas' Series.
1621
1622         :param repeat: Repeat the download specified number of times if not
1623             successful.
1624         :type repeat: int
1625         """
1626
1627         logging.info(u"Downloading and parsing input files ...")
1628
1629         for job, builds in self._cfg.input.items():
1630             for build in builds:
1631
1632                 result = self._download_and_parse_build(job, build, repeat)
1633                 if result[u"last"]:
1634                     break
1635                 build_nr = result[u"build"][u"build"]
1636
1637                 if result[u"data"]:
1638                     data = result[u"data"]
1639                     build_data = pd.Series({
1640                         u"metadata": pd.Series(
1641                             list(data[u"metadata"].values()),
1642                             index=list(data[u"metadata"].keys())
1643                         ),
1644                         u"suites": pd.Series(
1645                             list(data[u"suites"].values()),
1646                             index=list(data[u"suites"].keys())
1647                         ),
1648                         u"tests": pd.Series(
1649                             list(data[u"tests"].values()),
1650                             index=list(data[u"tests"].keys())
1651                         )
1652                     })
1653
1654                     if self._input_data.get(job, None) is None:
1655                         self._input_data[job] = pd.Series()
1656                     self._input_data[job][str(build_nr)] = build_data
1657                     self._cfg.set_input_file_name(
1658                         job, build_nr, result[u"build"][u"file-name"]
1659                     )
1660                 self._cfg.set_input_state(job, build_nr, result[u"state"])
1661
1662                 mem_alloc = \
1663                     resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000
1664                 logging.info(f"Memory allocation: {mem_alloc:.0f}MB")
1665
1666         logging.info(u"Done.")
1667
1668         msg = f"Successful downloads from the sources:\n"
1669         for source in self._cfg.environment[u"data-sources"]:
1670             if source[u"successful-downloads"]:
1671                 msg += (
1672                     f"{source[u'url']}/{source[u'path']}/"
1673                     f"{source[u'file-name']}: "
1674                     f"{source[u'successful-downloads']}\n"
1675                 )
1676         logging.info(msg)
1677
1678     def process_local_file(self, local_file, job=u"local", build_nr=1,
1679                            replace=True):
1680         """Process local XML file given as a command-line parameter.
1681
1682         :param local_file: The file to process.
1683         :param job: Job name.
1684         :param build_nr: Build number.
1685         :param replace: If True, the information about jobs and builds is
1686             replaced by the new one, otherwise the new jobs and builds are
1687             added.
1688         :type local_file: str
1689         :type job: str
1690         :type build_nr: int
1691         :type replace: bool
1692         :raises: PresentationError if an error occurs.
1693         """
1694         if not isfile(local_file):
1695             raise PresentationError(f"The file {local_file} does not exist.")
1696
1697         try:
1698             build_nr = int(local_file.split(u"/")[-1].split(u".")[0])
1699         except (IndexError, ValueError):
1700             pass
1701
1702         build = {
1703             u"build": build_nr,
1704             u"status": u"failed",
1705             u"file-name": local_file
1706         }
1707         if replace:
1708             self._cfg.input = dict()
1709         self._cfg.add_build(job, build)
1710
1711         logging.info(f"Processing {job}: {build_nr:2d}: {local_file}")
1712         data = self._parse_tests(job, build)
1713         if data is None:
1714             raise PresentationError(
1715                 f"Error occurred while parsing the file {local_file}"
1716             )
1717
1718         build_data = pd.Series({
1719             u"metadata": pd.Series(
1720                 list(data[u"metadata"].values()),
1721                 index=list(data[u"metadata"].keys())
1722             ),
1723             u"suites": pd.Series(
1724                 list(data[u"suites"].values()),
1725                 index=list(data[u"suites"].keys())
1726             ),
1727             u"tests": pd.Series(
1728                 list(data[u"tests"].values()),
1729                 index=list(data[u"tests"].keys())
1730             )
1731         })
1732
1733         if self._input_data.get(job, None) is None:
1734             self._input_data[job] = pd.Series()
1735         self._input_data[job][str(build_nr)] = build_data
1736
1737         self._cfg.set_input_state(job, build_nr, u"processed")
1738
1739     def process_local_directory(self, local_dir, replace=True):
1740         """Process local directory with XML file(s). The directory is processed
1741         as a 'job' and the XML files in it as builds.
1742         If the given directory contains only sub-directories, these
1743         sub-directories processed as jobs and corresponding XML files as builds
1744         of their job.
1745
1746         :param local_dir: Local directory to process.
1747         :param replace: If True, the information about jobs and builds is
1748             replaced by the new one, otherwise the new jobs and builds are
1749             added.
1750         :type local_dir: str
1751         :type replace: bool
1752         """
1753         if not isdir(local_dir):
1754             raise PresentationError(
1755                 f"The directory {local_dir} does not exist."
1756             )
1757
1758         # Check if the given directory includes only files, or only directories
1759         _, dirnames, filenames = next(walk(local_dir))
1760
1761         if filenames and not dirnames:
1762             filenames.sort()
1763             # local_builds:
1764             # key: dir (job) name, value: list of file names (builds)
1765             local_builds = {
1766                 local_dir: [join(local_dir, name) for name in filenames]
1767             }
1768
1769         elif dirnames and not filenames:
1770             dirnames.sort()
1771             # local_builds:
1772             # key: dir (job) name, value: list of file names (builds)
1773             local_builds = dict()
1774             for dirname in dirnames:
1775                 builds = [
1776                     join(local_dir, dirname, name)
1777                     for name in listdir(join(local_dir, dirname))
1778                     if isfile(join(local_dir, dirname, name))
1779                 ]
1780                 if builds:
1781                     local_builds[dirname] = sorted(builds)
1782
1783         elif not filenames and not dirnames:
1784             raise PresentationError(f"The directory {local_dir} is empty.")
1785         else:
1786             raise PresentationError(
1787                 f"The directory {local_dir} can include only files or only "
1788                 f"directories, not both.\nThe directory {local_dir} includes "
1789                 f"file(s):\n{filenames}\nand directories:\n{dirnames}"
1790             )
1791
1792         if replace:
1793             self._cfg.input = dict()
1794
1795         for job, files in local_builds.items():
1796             for idx, local_file in enumerate(files):
1797                 self.process_local_file(local_file, job, idx + 1, replace=False)
1798
1799     @staticmethod
1800     def _end_of_tag(tag_filter, start=0, closer=u"'"):
1801         """Return the index of character in the string which is the end of tag.
1802
1803         :param tag_filter: The string where the end of tag is being searched.
1804         :param start: The index where the searching is stated.
1805         :param closer: The character which is the tag closer.
1806         :type tag_filter: str
1807         :type start: int
1808         :type closer: str
1809         :returns: The index of the tag closer.
1810         :rtype: int
1811         """
1812         try:
1813             idx_opener = tag_filter.index(closer, start)
1814             return tag_filter.index(closer, idx_opener + 1)
1815         except ValueError:
1816             return None
1817
1818     @staticmethod
1819     def _condition(tag_filter):
1820         """Create a conditional statement from the given tag filter.
1821
1822         :param tag_filter: Filter based on tags from the element specification.
1823         :type tag_filter: str
1824         :returns: Conditional statement which can be evaluated.
1825         :rtype: str
1826         """
1827         index = 0
1828         while True:
1829             index = InputData._end_of_tag(tag_filter, index)
1830             if index is None:
1831                 return tag_filter
1832             index += 1
1833             tag_filter = tag_filter[:index] + u" in tags" + tag_filter[index:]
1834
1835     def filter_data(self, element, params=None, data=None, data_set=u"tests",
1836                     continue_on_error=False):
1837         """Filter required data from the given jobs and builds.
1838
1839         The output data structure is:
1840         - job 1
1841           - build 1
1842             - test (or suite) 1 ID:
1843               - param 1
1844               - param 2
1845               ...
1846               - param n
1847             ...
1848             - test (or suite) n ID:
1849             ...
1850           ...
1851           - build n
1852         ...
1853         - job n
1854
1855         :param element: Element which will use the filtered data.
1856         :param params: Parameters which will be included in the output. If None,
1857             all parameters are included.
1858         :param data: If not None, this data is used instead of data specified
1859             in the element.
1860         :param data_set: The set of data to be filtered: tests, suites,
1861             metadata.
1862         :param continue_on_error: Continue if there is error while reading the
1863             data. The Item will be empty then
1864         :type element: pandas.Series
1865         :type params: list
1866         :type data: dict
1867         :type data_set: str
1868         :type continue_on_error: bool
1869         :returns: Filtered data.
1870         :rtype pandas.Series
1871         """
1872
1873         try:
1874             if data_set == "suites":
1875                 cond = u"True"
1876             elif element[u"filter"] in (u"all", u"template"):
1877                 cond = u"True"
1878             else:
1879                 cond = InputData._condition(element[u"filter"])
1880             logging.debug(f"   Filter: {cond}")
1881         except KeyError:
1882             logging.error(u"  No filter defined.")
1883             return None
1884
1885         if params is None:
1886             params = element.get(u"parameters", None)
1887             if params:
1888                 params.extend((u"type", u"status"))
1889
1890         data_to_filter = data if data else element[u"data"]
1891         data = pd.Series()
1892         try:
1893             for job, builds in data_to_filter.items():
1894                 data[job] = pd.Series()
1895                 for build in builds:
1896                     data[job][str(build)] = pd.Series()
1897                     try:
1898                         data_dict = dict(
1899                             self.data[job][str(build)][data_set].items())
1900                     except KeyError:
1901                         if continue_on_error:
1902                             continue
1903                         return None
1904
1905                     for test_id, test_data in data_dict.items():
1906                         if eval(cond, {u"tags": test_data.get(u"tags", u"")}):
1907                             data[job][str(build)][test_id] = pd.Series()
1908                             if params is None:
1909                                 for param, val in test_data.items():
1910                                     data[job][str(build)][test_id][param] = val
1911                             else:
1912                                 for param in params:
1913                                     try:
1914                                         data[job][str(build)][test_id][param] =\
1915                                             test_data[param]
1916                                     except KeyError:
1917                                         data[job][str(build)][test_id][param] =\
1918                                             u"No Data"
1919             return data
1920
1921         except (KeyError, IndexError, ValueError) as err:
1922             logging.error(
1923                 f"Missing mandatory parameter in the element specification: "
1924                 f"{repr(err)}"
1925             )
1926             return None
1927         except AttributeError as err:
1928             logging.error(repr(err))
1929             return None
1930         except SyntaxError as err:
1931             logging.error(
1932                 f"The filter {cond} is not correct. Check if all tags are "
1933                 f"enclosed by apostrophes.\n{repr(err)}"
1934             )
1935             return None
1936
1937     def filter_tests_by_name(self, element, params=None, data_set=u"tests",
1938                              continue_on_error=False):
1939         """Filter required data from the given jobs and builds.
1940
1941         The output data structure is:
1942         - job 1
1943           - build 1
1944             - test (or suite) 1 ID:
1945               - param 1
1946               - param 2
1947               ...
1948               - param n
1949             ...
1950             - test (or suite) n ID:
1951             ...
1952           ...
1953           - build n
1954         ...
1955         - job n
1956
1957         :param element: Element which will use the filtered data.
1958         :param params: Parameters which will be included in the output. If None,
1959         all parameters are included.
1960         :param data_set: The set of data to be filtered: tests, suites,
1961         metadata.
1962         :param continue_on_error: Continue if there is error while reading the
1963         data. The Item will be empty then
1964         :type element: pandas.Series
1965         :type params: list
1966         :type data_set: str
1967         :type continue_on_error: bool
1968         :returns: Filtered data.
1969         :rtype pandas.Series
1970         """
1971
1972         include = element.get(u"include", None)
1973         if not include:
1974             logging.warning(u"No tests to include, skipping the element.")
1975             return None
1976
1977         if params is None:
1978             params = element.get(u"parameters", None)
1979             if params and u"type" not in params:
1980                 params.append(u"type")
1981
1982         cores = element.get(u"core", None)
1983         if cores:
1984             tests = list()
1985             for core in cores:
1986                 for test in include:
1987                     tests.append(test.format(core=core))
1988         else:
1989             tests = include
1990
1991         data = pd.Series()
1992         try:
1993             for job, builds in element[u"data"].items():
1994                 data[job] = pd.Series()
1995                 for build in builds:
1996                     data[job][str(build)] = pd.Series()
1997                     for test in tests:
1998                         try:
1999                             reg_ex = re.compile(str(test).lower())
2000                             for test_id in self.data[job][
2001                                     str(build)][data_set].keys():
2002                                 if re.match(reg_ex, str(test_id).lower()):
2003                                     test_data = self.data[job][
2004                                         str(build)][data_set][test_id]
2005                                     data[job][str(build)][test_id] = pd.Series()
2006                                     if params is None:
2007                                         for param, val in test_data.items():
2008                                             data[job][str(build)][test_id]\
2009                                                 [param] = val
2010                                     else:
2011                                         for param in params:
2012                                             try:
2013                                                 data[job][str(build)][
2014                                                     test_id][param] = \
2015                                                     test_data[param]
2016                                             except KeyError:
2017                                                 data[job][str(build)][
2018                                                     test_id][param] = u"No Data"
2019                         except KeyError as err:
2020                             if continue_on_error:
2021                                 logging.debug(repr(err))
2022                                 continue
2023                             logging.error(repr(err))
2024                             return None
2025             return data
2026
2027         except (KeyError, IndexError, ValueError) as err:
2028             logging.error(
2029                 f"Missing mandatory parameter in the element "
2030                 f"specification: {repr(err)}"
2031             )
2032             return None
2033         except AttributeError as err:
2034             logging.error(repr(err))
2035             return None
2036
2037     @staticmethod
2038     def merge_data(data):
2039         """Merge data from more jobs and builds to a simple data structure.
2040
2041         The output data structure is:
2042
2043         - test (suite) 1 ID:
2044           - param 1
2045           - param 2
2046           ...
2047           - param n
2048         ...
2049         - test (suite) n ID:
2050         ...
2051
2052         :param data: Data to merge.
2053         :type data: pandas.Series
2054         :returns: Merged data.
2055         :rtype: pandas.Series
2056         """
2057
2058         logging.info(u"    Merging data ...")
2059
2060         merged_data = pd.Series()
2061         for builds in data.values:
2062             for item in builds.values:
2063                 for item_id, item_data in item.items():
2064                     merged_data[item_id] = item_data
2065         return merged_data
2066
2067     def print_all_oper_data(self):
2068         """Print all operational data to console.
2069         """
2070
2071         for job in self._input_data.values:
2072             for build in job.values:
2073                 for test_id, test_data in build[u"tests"].items():
2074                     print(f"{test_id}")
2075                     if test_data.get(u"show-run", None) is None:
2076                         continue
2077                     for dut_name, data in test_data[u"show-run"].items():
2078                         if data.get(u"runtime", None) is None:
2079                             continue
2080                         runtime = loads(data[u"runtime"])
2081                         try:
2082                             threads_nr = len(runtime[0][u"clocks"])
2083                         except (IndexError, KeyError):
2084                             continue
2085                         threads = OrderedDict(
2086                             {idx: list() for idx in range(threads_nr)})
2087                         for item in runtime:
2088                             for idx in range(threads_nr):
2089                                 if item[u"vectors"][idx] > 0:
2090                                     clocks = item[u"clocks"][idx] / \
2091                                              item[u"vectors"][idx]
2092                                 elif item[u"calls"][idx] > 0:
2093                                     clocks = item[u"clocks"][idx] / \
2094                                              item[u"calls"][idx]
2095                                 elif item[u"suspends"][idx] > 0:
2096                                     clocks = item[u"clocks"][idx] / \
2097                                              item[u"suspends"][idx]
2098                                 else:
2099                                     clocks = 0.0
2100
2101                                 if item[u"calls"][idx] > 0:
2102                                     vectors_call = item[u"vectors"][idx] / \
2103                                                    item[u"calls"][idx]
2104                                 else:
2105                                     vectors_call = 0.0
2106
2107                                 if int(item[u"calls"][idx]) + int(
2108                                         item[u"vectors"][idx]) + \
2109                                         int(item[u"suspends"][idx]):
2110                                     threads[idx].append([
2111                                         item[u"name"],
2112                                         item[u"calls"][idx],
2113                                         item[u"vectors"][idx],
2114                                         item[u"suspends"][idx],
2115                                         clocks,
2116                                         vectors_call
2117                                     ])
2118
2119                         print(f"Host IP: {data.get(u'host', '')}, "
2120                               f"Socket: {data.get(u'socket', '')}")
2121                         for thread_nr, thread in threads.items():
2122                             txt_table = prettytable.PrettyTable(
2123                                 (
2124                                     u"Name",
2125                                     u"Nr of Vectors",
2126                                     u"Nr of Packets",
2127                                     u"Suspends",
2128                                     u"Cycles per Packet",
2129                                     u"Average Vector Size"
2130                                 )
2131                             )
2132                             avg = 0.0
2133                             for row in thread:
2134                                 txt_table.add_row(row)
2135                                 avg += row[-1]
2136                             if len(thread) == 0:
2137                                 avg = u""
2138                             else:
2139                                 avg = f", Average Vector Size per Node: " \
2140                                       f"{(avg / len(thread)):.2f}"
2141                             th_name = u"main" if thread_nr == 0 \
2142                                 else f"worker_{thread_nr}"
2143                             print(f"{dut_name}, {th_name}{avg}")
2144                             txt_table.float_format = u".2"
2145                             txt_table.align = u"r"
2146                             txt_table.align[u"Name"] = u"l"
2147                             print(f"{txt_table.get_string()}\n")