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