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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Data pre-processing
16 - extract data from output.xml files generated by Jenkins jobs and store in
18 - provide access to the data.
19 - filter the data using tags,
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
40 from robot.api import ExecutionResult, ResultVisitor
41 from robot import errors
43 from resources.libraries.python import jumpavg
44 from input_data_files import download_and_unzip_data_file
45 from pal_errors import PresentationError
48 # Separator used in file names
52 class ExecutionChecker(ResultVisitor):
53 """Class to traverse through the test suite structure.
55 The functionality implemented in this class generates a json structure:
61 "generated": "Timestamp",
62 "version": "SUT version",
63 "job": "Jenkins job name",
64 "build": "Information about the build"
67 "Suite long name 1": {
69 "doc": "Suite 1 documentation",
70 "parent": "Suite 1 parent",
71 "level": "Level of the suite in the suite hierarchy"
73 "Suite long name N": {
75 "doc": "Suite N documentation",
76 "parent": "Suite 2 parent",
77 "level": "Level of the suite in the suite hierarchy"
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"],
91 "status": "PASS" | "FAIL",
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"],
142 "status": "PASS" | "FAIL",
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",
156 "receive-rate": float,
157 # Average of a list, computed using AvgStdevStats.
158 # In CSIT-1180, replace with List[float].
172 "metadata": { # Optional
173 "version": "VPP version",
174 "job": "Jenkins job name",
175 "build": "Information about the build"
179 "doc": "Suite 1 documentation",
180 "parent": "Suite 1 parent",
181 "level": "Level of the suite in the suite hierarchy"
184 "doc": "Suite N documentation",
185 "parent": "Suite 2 parent",
186 "level": "Level of the suite in the suite hierarchy"
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"
206 .. note:: ID is the lowercase full path to the test.
209 REGEX_PLR_RATE = re.compile(
210 r'PLRsearch lower bound::?\s(\d+.\d+).*\n'
211 r'PLRsearch upper bound::?\s(\d+.\d+)'
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+)'
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+)'
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'
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.*'
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.*'
240 REGEX_MRR_MSG_INFO = re.compile(r'.*\[(.*)\]')
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*)'
251 # Needed for CPS and PPS tests
252 REGEX_NDRPDR_LAT_BASE = re.compile(
253 r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
254 r'LATENCY.*\[\'(.*)\', \'(.*)\'\]'
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.*\[\'(.*)\', \'(.*)\'\]'
265 REGEX_VERSION_VPP = re.compile(
266 r"(VPP Version:\s*|VPP version:\s*)(.*)"
268 REGEX_VERSION_DPDK = re.compile(
269 r"(DPDK version:\s*|DPDK Version:\s*)(.*)"
271 REGEX_TCP = re.compile(
272 r'Total\s(rps|cps|throughput):\s(\d*).*$'
274 REGEX_MRR = re.compile(
275 r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
276 r'tx\s(\d*),\srx\s(\d*)'
278 REGEX_BMRR = re.compile(
279 r'.*trial results.*: \[(.*)\]'
281 REGEX_RECONF_LOSS = re.compile(
282 r'Packets lost due to reconfig: (\d*)'
284 REGEX_RECONF_TIME = re.compile(
285 r'Implied time lost: (\d*.[\de-]*)'
287 REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
289 REGEX_TC_NAME_NEW = re.compile(r'-\d+[cC]-')
291 REGEX_TC_NUMBER = re.compile(r'tc\d{2}-')
293 REGEX_TC_PAPI_CLI = re.compile(r'.*\((\d+.\d+.\d+.\d+.) - (.*)\)')
295 REGEX_SH_RUN_HOST = re.compile(
296 r'hostname=\"(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\",hook=\"(.*)\"'
299 def __init__(self, metadata, mapping, ignore, process_oper):
302 :param metadata: Key-value pairs to be included in "metadata" part of
304 :param mapping: Mapping of the old names of test cases to the new
306 :param ignore: List of TCs to be ignored.
307 :param process_oper: If True, operational data (show run, telemetry) is
312 :type process_oper: bool
315 # Type of message to parse out from the test messages
316 self._msg_type = None
322 self._timestamp = None
324 # Testbed. The testbed is identified by TG node IP address.
327 # Mapping of TCs long names
328 self._mapping = mapping
331 self._ignore = ignore
333 self._process_oper = process_oper
335 # Number of PAPI History messages found:
337 # 1 - PAPI History of DUT1
338 # 2 - PAPI History of DUT2
339 self._conf_history_lookup_nr = 0
341 self._sh_run_counter = 0
342 self._telemetry_kw_counter = 0
343 self._telemetry_msg_counter = 0
345 # Test ID of currently processed test- the lowercase full path to the
349 # The main data structure
351 u"metadata": OrderedDict(),
352 u"suites": OrderedDict(),
353 u"tests": OrderedDict()
356 # Save the provided metadata
357 for key, val in metadata.items():
358 self._data[u"metadata"][key] = val
360 # Dictionary defining the methods used to parse different types of
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
373 """Getter - Data parsed from the XML file.
375 :returns: Data parsed from the XML file.
380 def _get_data_from_mrr_test_msg(self, msg):
381 """Get info from message of MRR performance tests.
383 :param msg: Message to be processed.
385 :returns: Processed message or original message if a problem occurs.
389 groups = re.search(self.REGEX_MRR_MSG_INFO, msg)
390 if not groups or groups.lastindex != 1:
391 return u"Test Failed."
394 data = groups.group(1).split(u", ")
395 except (AttributeError, IndexError, ValueError, KeyError):
396 return u"Test Failed."
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."
406 def _get_data_from_cps_test_msg(self, msg):
407 """Get info from message of NDRPDR CPS tests.
409 :param msg: Message to be processed.
411 :returns: Processed message or "Test Failed." if a problem occurs.
415 groups = re.search(self.REGEX_CPS_MSG_INFO, msg)
416 if not groups or groups.lastindex != 2:
417 return u"Test Failed."
421 f"1. {(float(groups.group(1)) / 1e6):5.2f}\n"
422 f"2. {(float(groups.group(2)) / 1e6):5.2f}"
424 except (AttributeError, IndexError, ValueError, KeyError):
425 return u"Test Failed."
427 def _get_data_from_pps_test_msg(self, msg):
428 """Get info from message of NDRPDR PPS tests.
430 :param msg: Message to be processed.
432 :returns: Processed message or "Test Failed." if a problem occurs.
436 groups = re.search(self.REGEX_PPS_MSG_INFO, msg)
437 if not groups or groups.lastindex != 4:
438 return u"Test Failed."
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}"
447 except (AttributeError, IndexError, ValueError, KeyError):
448 return u"Test Failed."
450 def _get_data_from_perf_test_msg(self, msg):
451 """Get info from message of NDRPDR performance tests.
453 :param msg: Message to be processed.
455 :returns: Processed message or "Test Failed." if a problem occurs.
459 groups = re.search(self.REGEX_PERF_MSG_INFO, msg)
460 if not groups or groups.lastindex != 10:
461 return u"Test Failed."
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),
476 except (AttributeError, IndexError, ValueError, KeyError):
477 return u"Test Failed."
479 def _process_lat(in_str_1, in_str_2):
480 """Extract P50, P90 and P99 latencies or min, avg, max values from
483 :param in_str_1: Latency string for one direction produced by robot
485 :param in_str_2: Latency string for second direction produced by
489 :returns: Processed latency string or None if a problem occurs.
492 in_list_1 = in_str_1.split('/', 3)
493 in_list_2 = in_str_2.split('/', 3)
495 if len(in_list_1) != 4 and len(in_list_2) != 4:
498 in_list_1[3] += u"=" * (len(in_list_1[3]) % 4)
500 hdr_lat_1 = hdrh.histogram.HdrHistogram.decode(in_list_1[3])
501 except hdrh.codec.HdrLengthException:
504 in_list_2[3] += u"=" * (len(in_list_2[3]) % 4)
506 hdr_lat_2 = hdrh.histogram.HdrHistogram.decode(in_list_2[3])
507 except hdrh.codec.HdrLengthException:
510 if hdr_lat_1 and hdr_lat_2:
512 hdr_lat_1.get_value_at_percentile(50.0),
513 hdr_lat_1.get_value_at_percentile(90.0),
514 hdr_lat_1.get_value_at_percentile(99.0),
515 hdr_lat_2.get_value_at_percentile(50.0),
516 hdr_lat_2.get_value_at_percentile(90.0),
517 hdr_lat_2.get_value_at_percentile(99.0)
523 int(in_list_1[0]), int(in_list_1[1]), int(in_list_1[2]),
524 int(in_list_2[0]), int(in_list_2[1]), int(in_list_2[2])
527 if item in (-1, 4294967295, 0):
533 f"1. {(data[u'ndr_low'] / 1e6):5.2f} "
534 f"{data[u'ndr_low_b']:5.2f}"
535 f"\n2. {(data[u'pdr_low'] / 1e6):5.2f} "
536 f"{data[u'pdr_low_b']:5.2f}"
539 _process_lat(data[u'pdr_lat_10_1'], data[u'pdr_lat_10_2']),
540 _process_lat(data[u'pdr_lat_50_1'], data[u'pdr_lat_50_2']),
541 _process_lat(data[u'pdr_lat_90_1'], data[u'pdr_lat_90_2'])
544 max_len = len(str(max((max(item) for item in latency))))
545 max_len = 4 if max_len < 4 else max_len
547 for idx, lat in enumerate(latency):
552 f"{lat[0]:{max_len}d} "
553 f"{lat[1]:{max_len}d} "
554 f"{lat[2]:{max_len}d} "
555 f"{lat[3]:{max_len}d} "
556 f"{lat[4]:{max_len}d} "
557 f"{lat[5]:{max_len}d} "
562 except (AttributeError, IndexError, ValueError, KeyError):
563 return u"Test Failed."
565 def _get_testbed(self, msg):
566 """Called when extraction of testbed IP is required.
567 The testbed is identified by TG node IP address.
569 :param msg: Message to process.
574 if msg.message.count(u"Setup of TG node") or \
575 msg.message.count(u"Setup of node TG host"):
576 reg_tg_ip = re.compile(
577 r'.*TG .* (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*')
579 self._testbed = str(re.search(reg_tg_ip, msg.message).group(1))
580 except (KeyError, ValueError, IndexError, AttributeError):
583 self._data[u"metadata"][u"testbed"] = self._testbed
584 self._msg_type = None
586 def _get_vpp_version(self, msg):
587 """Called when extraction of VPP version is required.
589 :param msg: Message to process.
594 if msg.message.count(u"VPP version:") or \
595 msg.message.count(u"VPP Version:"):
597 re.search(self.REGEX_VERSION_VPP, msg.message).group(2)
599 self._data[u"metadata"][u"version"] = self._version
600 self._msg_type = None
601 logging.info(self._version)
603 def _get_dpdk_version(self, msg):
604 """Called when extraction of DPDK version is required.
606 :param msg: Message to process.
611 if msg.message.count(u"DPDK Version:"):
613 self._version = str(re.search(
614 self.REGEX_VERSION_DPDK, msg.message).group(2))
615 self._data[u"metadata"][u"version"] = self._version
619 self._msg_type = None
621 def _get_papi_history(self, msg):
622 """Called when extraction of PAPI command history is required.
624 :param msg: Message to process.
628 if msg.message.count(u"PAPI command history:"):
629 self._conf_history_lookup_nr += 1
630 if self._conf_history_lookup_nr == 1:
631 self._data[u"tests"][self._test_id][u"conf-history"] = str()
633 self._msg_type = None
635 r"\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} PAPI command history:",
639 ).replace(u'"', u"'")
640 self._data[u"tests"][self._test_id][u"conf-history"] += (
641 f"**DUT{str(self._conf_history_lookup_nr)}:** {text}"
644 def _get_show_run(self, msg):
645 """Called when extraction of VPP operational data (output of CLI command
646 Show Runtime) is required.
648 :param msg: Message to process.
653 if not msg.message.count(u"stats runtime"):
657 if self._sh_run_counter > 1:
660 if u"show-run" not in self._data[u"tests"][self._test_id].keys():
661 self._data[u"tests"][self._test_id][u"show-run"] = dict()
663 groups = re.search(self.REGEX_TC_PAPI_CLI, msg.message)
667 host = groups.group(1)
668 except (AttributeError, IndexError):
671 sock = groups.group(2)
672 except (AttributeError, IndexError):
675 dut = u"dut{nr}".format(
676 nr=len(self._data[u'tests'][self._test_id][u'show-run'].keys()) + 1)
678 self._data[u'tests'][self._test_id][u'show-run'][dut] = \
683 u"runtime": str(msg.message).replace(u' ', u'').
684 replace(u'\n', u'').replace(u"'", u'"').
685 replace(u'b"', u'"').replace(u'u"', u'"').
690 def _get_telemetry(self, msg):
691 """Called when extraction of VPP telemetry data is required.
693 :param msg: Message to process.
698 if self._telemetry_kw_counter > 1:
700 if not msg.message.count(u"# TYPE vpp_runtime_calls"):
703 if u"telemetry-show-run" not in \
704 self._data[u"tests"][self._test_id].keys():
705 self._data[u"tests"][self._test_id][u"telemetry-show-run"] = dict()
707 self._telemetry_msg_counter += 1
708 groups = re.search(self.REGEX_SH_RUN_HOST, msg.message)
712 host = groups.group(1)
713 except (AttributeError, IndexError):
716 sock = groups.group(2)
717 except (AttributeError, IndexError):
720 u"source_type": u"node",
722 u"msg_type": u"metric",
723 u"log_level": u"INFO",
724 u"timestamp": msg.timestamp,
725 u"msg": u"show_runtime",
730 for line in msg.message.splitlines():
731 if not line.startswith(u"vpp_runtime_"):
734 params, value, timestamp = line.rsplit(u" ", maxsplit=2)
735 cut = params.index(u"{")
736 name = params[:cut].split(u"_", maxsplit=2)[-1]
738 u"dict" + params[cut:].replace('{', '(').replace('}', ')')
740 labels[u"graph_node"] = labels.pop(u"name")
741 runtime[u"data"].append(
745 u"timestamp": timestamp,
749 except (TypeError, ValueError, IndexError):
751 self._data[u'tests'][self._test_id][u'telemetry-show-run']\
752 [f"dut{self._telemetry_msg_counter}"] = copy.copy(
760 def _get_ndrpdr_throughput(self, msg):
761 """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
764 :param msg: The test message to be parsed.
766 :returns: Parsed data as a dict and the status (PASS/FAIL).
767 :rtype: tuple(dict, str)
771 u"NDR": {u"LOWER": -1.0, u"UPPER": -1.0},
772 u"PDR": {u"LOWER": -1.0, u"UPPER": -1.0}
775 groups = re.search(self.REGEX_NDRPDR_RATE, msg)
777 if groups is not None:
779 throughput[u"NDR"][u"LOWER"] = float(groups.group(1))
780 throughput[u"NDR"][u"UPPER"] = float(groups.group(2))
781 throughput[u"PDR"][u"LOWER"] = float(groups.group(3))
782 throughput[u"PDR"][u"UPPER"] = float(groups.group(4))
784 except (IndexError, ValueError):
787 return throughput, status
789 def _get_ndrpdr_throughput_gbps(self, msg):
790 """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER in Gbps from the
793 :param msg: The test message to be parsed.
795 :returns: Parsed data as a dict and the status (PASS/FAIL).
796 :rtype: tuple(dict, str)
800 u"NDR": {u"LOWER": -1.0, u"UPPER": -1.0},
801 u"PDR": {u"LOWER": -1.0, u"UPPER": -1.0}
804 groups = re.search(self.REGEX_NDRPDR_GBPS, msg)
806 if groups is not None:
808 gbps[u"NDR"][u"LOWER"] = float(groups.group(1))
809 gbps[u"NDR"][u"UPPER"] = float(groups.group(2))
810 gbps[u"PDR"][u"LOWER"] = float(groups.group(3))
811 gbps[u"PDR"][u"UPPER"] = float(groups.group(4))
813 except (IndexError, ValueError):
818 def _get_plr_throughput(self, msg):
819 """Get PLRsearch lower bound and PLRsearch upper bound from the test
822 :param msg: The test message to be parsed.
824 :returns: Parsed data as a dict and the status (PASS/FAIL).
825 :rtype: tuple(dict, str)
833 groups = re.search(self.REGEX_PLR_RATE, msg)
835 if groups is not None:
837 throughput[u"LOWER"] = float(groups.group(1))
838 throughput[u"UPPER"] = float(groups.group(2))
840 except (IndexError, ValueError):
843 return throughput, status
845 def _get_ndrpdr_latency(self, msg):
846 """Get LATENCY from the test message.
848 :param msg: The test message to be parsed.
850 :returns: Parsed data as a dict and the status (PASS/FAIL).
851 :rtype: tuple(dict, str)
861 u"direction1": copy.copy(latency_default),
862 u"direction2": copy.copy(latency_default)
865 u"direction1": copy.copy(latency_default),
866 u"direction2": copy.copy(latency_default)
869 u"direction1": copy.copy(latency_default),
870 u"direction2": copy.copy(latency_default)
873 u"direction1": copy.copy(latency_default),
874 u"direction2": copy.copy(latency_default)
877 u"direction1": copy.copy(latency_default),
878 u"direction2": copy.copy(latency_default)
881 u"direction1": copy.copy(latency_default),
882 u"direction2": copy.copy(latency_default)
886 groups = re.search(self.REGEX_NDRPDR_LAT, msg)
888 groups = re.search(self.REGEX_NDRPDR_LAT_BASE, msg)
890 return latency, u"FAIL"
892 def process_latency(in_str):
893 """Return object with parsed latency values.
895 TODO: Define class for the return type.
897 :param in_str: Input string, min/avg/max/hdrh format.
899 :returns: Dict with corresponding keys, except hdrh float values.
901 :throws IndexError: If in_str does not have enough substrings.
902 :throws ValueError: If a substring does not convert to float.
904 in_list = in_str.split('/', 3)
907 u"min": float(in_list[0]),
908 u"avg": float(in_list[1]),
909 u"max": float(in_list[2]),
913 if len(in_list) == 4:
914 rval[u"hdrh"] = str(in_list[3])
919 latency[u"NDR"][u"direction1"] = process_latency(groups.group(1))
920 latency[u"NDR"][u"direction2"] = process_latency(groups.group(2))
921 latency[u"PDR"][u"direction1"] = process_latency(groups.group(3))
922 latency[u"PDR"][u"direction2"] = process_latency(groups.group(4))
923 if groups.lastindex == 4:
924 return latency, u"PASS"
925 except (IndexError, ValueError):
929 latency[u"PDR90"][u"direction1"] = process_latency(groups.group(5))
930 latency[u"PDR90"][u"direction2"] = process_latency(groups.group(6))
931 latency[u"PDR50"][u"direction1"] = process_latency(groups.group(7))
932 latency[u"PDR50"][u"direction2"] = process_latency(groups.group(8))
933 latency[u"PDR10"][u"direction1"] = process_latency(groups.group(9))
934 latency[u"PDR10"][u"direction2"] = process_latency(groups.group(10))
935 latency[u"LAT0"][u"direction1"] = process_latency(groups.group(11))
936 latency[u"LAT0"][u"direction2"] = process_latency(groups.group(12))
937 if groups.lastindex == 12:
938 return latency, u"PASS"
939 except (IndexError, ValueError):
942 return latency, u"FAIL"
945 def _get_hoststack_data(msg, tags):
946 """Get data from the hoststack test message.
948 :param msg: The test message to be parsed.
949 :param tags: Test tags.
952 :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
953 :rtype: tuple(dict, str)
958 msg = msg.replace(u"'", u'"').replace(u" ", u"")
959 if u"LDPRELOAD" in tags:
963 except JSONDecodeError:
965 elif u"VPPECHO" in tags:
967 msg_lst = msg.replace(u"}{", u"} {").split(u" ")
969 client=loads(msg_lst[0]),
970 server=loads(msg_lst[1])
973 except (JSONDecodeError, IndexError):
976 return result, status
978 def _get_vsap_data(self, msg, tags):
979 """Get data from the vsap test message.
981 :param msg: The test message to be parsed.
982 :param tags: Test tags.
985 :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
986 :rtype: tuple(dict, str)
991 groups = re.search(self.REGEX_VSAP_MSG_INFO, msg)
992 if groups is not None:
994 result[u"transfer-rate"] = float(groups.group(1)) * 1e3
995 result[u"latency"] = float(groups.group(2))
996 result[u"completed-requests"] = int(groups.group(3))
997 result[u"failed-requests"] = int(groups.group(4))
998 result[u"bytes-transferred"] = int(groups.group(5))
999 if u"TCP_CPS"in tags:
1000 result[u"cps"] = float(groups.group(6))
1001 elif u"TCP_RPS" in tags:
1002 result[u"rps"] = float(groups.group(6))
1004 return result, status
1006 except (IndexError, ValueError):
1009 return result, status
1011 def visit_suite(self, suite):
1012 """Implements traversing through the suite and its direct children.
1014 :param suite: Suite to process.
1018 if self.start_suite(suite) is not False:
1019 suite.suites.visit(self)
1020 suite.tests.visit(self)
1021 self.end_suite(suite)
1023 def start_suite(self, suite):
1024 """Called when suite starts.
1026 :param suite: Suite to process.
1032 parent_name = suite.parent.name
1033 except AttributeError:
1036 self._data[u"suites"][suite.longname.lower().
1037 replace(u'"', u"'").
1038 replace(u" ", u"_")] = {
1039 u"name": suite.name.lower(),
1041 u"parent": parent_name,
1042 u"level": len(suite.longname.split(u"."))
1045 suite.setup.visit(self)
1046 suite.teardown.visit(self)
1048 def end_suite(self, suite):
1049 """Called when suite ends.
1051 :param suite: Suite to process.
1056 def visit_test(self, test):
1057 """Implements traversing through the test.
1059 :param test: Test to process.
1063 if self.start_test(test) is not False:
1064 test.setup.visit(self)
1065 test.body.visit(self)
1066 test.teardown.visit(self)
1069 def start_test(self, test):
1070 """Called when test starts.
1072 :param test: Test to process.
1077 self._sh_run_counter = 0
1078 self._telemetry_kw_counter = 0
1079 self._telemetry_msg_counter = 0
1081 longname_orig = test.longname.lower()
1083 # Check the ignore list
1084 if longname_orig in self._ignore:
1087 tags = [str(tag) for tag in test.tags]
1088 test_result = dict()
1090 # Change the TC long name and name if defined in the mapping table
1091 longname = self._mapping.get(longname_orig, None)
1092 if longname is not None:
1093 name = longname.split(u'.')[-1]
1095 f"{self._data[u'metadata']}\n{longname_orig}\n{longname}\n"
1099 longname = longname_orig
1100 name = test.name.lower()
1102 # Remove TC number from the TC long name (backward compatibility):
1103 self._test_id = re.sub(self.REGEX_TC_NUMBER, u"", longname)
1104 # Remove TC number from the TC name (not needed):
1105 test_result[u"name"] = re.sub(self.REGEX_TC_NUMBER, "", name)
1107 test_result[u"parent"] = test.parent.name.lower()
1108 test_result[u"tags"] = tags
1109 test_result["doc"] = test.doc
1110 test_result[u"type"] = u""
1111 test_result[u"status"] = test.status
1112 test_result[u"starttime"] = test.starttime
1113 test_result[u"endtime"] = test.endtime
1115 if test.status == u"PASS":
1116 if u"NDRPDR" in tags:
1117 if u"TCP_PPS" in tags or u"UDP_PPS" in tags:
1118 test_result[u"msg"] = self._get_data_from_pps_test_msg(
1120 elif u"TCP_CPS" in tags or u"UDP_CPS" in tags:
1121 test_result[u"msg"] = self._get_data_from_cps_test_msg(
1124 test_result[u"msg"] = self._get_data_from_perf_test_msg(
1126 elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
1127 test_result[u"msg"] = self._get_data_from_mrr_test_msg(
1130 test_result[u"msg"] = test.message
1132 test_result[u"msg"] = test.message
1134 if u"PERFTEST" in tags and u"TREX" not in tags:
1135 # Replace info about cores (e.g. -1c-) with the info about threads
1136 # and cores (e.g. -1t1c-) in the long test case names and in the
1137 # test case names if necessary.
1140 for tag in test_result[u"tags"]:
1141 groups = re.search(self.REGEX_TC_TAG, tag)
1147 self._test_id = re.sub(
1148 self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
1149 self._test_id, count=1
1151 test_result[u"name"] = re.sub(
1152 self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
1153 test_result["name"], count=1
1156 test_result[u"status"] = u"FAIL"
1157 self._data[u"tests"][self._test_id] = test_result
1159 f"The test {self._test_id} has no or more than one "
1160 f"multi-threading tags.\n"
1161 f"Tags: {test_result[u'tags']}"
1165 if u"DEVICETEST" in tags:
1166 test_result[u"type"] = u"DEVICETEST"
1167 elif u"NDRPDR" in tags:
1168 if u"TCP_CPS" in tags or u"UDP_CPS" in tags:
1169 test_result[u"type"] = u"CPS"
1171 test_result[u"type"] = u"NDRPDR"
1172 if test.status == u"PASS":
1173 test_result[u"throughput"], test_result[u"status"] = \
1174 self._get_ndrpdr_throughput(test.message)
1175 test_result[u"gbps"], test_result[u"status"] = \
1176 self._get_ndrpdr_throughput_gbps(test.message)
1177 test_result[u"latency"], test_result[u"status"] = \
1178 self._get_ndrpdr_latency(test.message)
1179 elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
1181 test_result[u"type"] = u"MRR"
1183 test_result[u"type"] = u"BMRR"
1184 if test.status == u"PASS":
1185 test_result[u"result"] = dict()
1186 groups = re.search(self.REGEX_BMRR, test.message)
1187 if groups is not None:
1188 items_str = groups.group(1)
1190 float(item.strip().replace(u"'", u""))
1191 for item in items_str.split(",")
1193 # Use whole list in CSIT-1180.
1194 stats = jumpavg.AvgStdevStats.for_runs(items_float)
1195 test_result[u"result"][u"samples"] = items_float
1196 test_result[u"result"][u"receive-rate"] = stats.avg
1197 test_result[u"result"][u"receive-stdev"] = stats.stdev
1199 groups = re.search(self.REGEX_MRR, test.message)
1200 test_result[u"result"][u"receive-rate"] = \
1201 float(groups.group(3)) / float(groups.group(1))
1202 elif u"SOAK" in tags:
1203 test_result[u"type"] = u"SOAK"
1204 if test.status == u"PASS":
1205 test_result[u"throughput"], test_result[u"status"] = \
1206 self._get_plr_throughput(test.message)
1207 elif u"LDP_NGINX" in tags:
1208 test_result[u"type"] = u"LDP_NGINX"
1209 test_result[u"result"], test_result[u"status"] = \
1210 self._get_vsap_data(test.message, tags)
1211 elif u"HOSTSTACK" in tags:
1212 test_result[u"type"] = u"HOSTSTACK"
1213 if test.status == u"PASS":
1214 test_result[u"result"], test_result[u"status"] = \
1215 self._get_hoststack_data(test.message, tags)
1216 elif u"RECONF" in tags:
1217 test_result[u"type"] = u"RECONF"
1218 if test.status == u"PASS":
1219 test_result[u"result"] = None
1221 grps_loss = re.search(self.REGEX_RECONF_LOSS, test.message)
1222 grps_time = re.search(self.REGEX_RECONF_TIME, test.message)
1223 test_result[u"result"] = {
1224 u"loss": int(grps_loss.group(1)),
1225 u"time": float(grps_time.group(1))
1227 except (AttributeError, IndexError, ValueError, TypeError):
1228 test_result[u"status"] = u"FAIL"
1230 test_result[u"status"] = u"FAIL"
1232 self._data[u"tests"][self._test_id] = test_result
1234 def end_test(self, test):
1235 """Called when test ends.
1237 :param test: Test to process.
1242 def visit_keyword(self, keyword):
1243 """Implements traversing through the keyword and its child keywords.
1245 :param keyword: Keyword to process.
1246 :type keyword: Keyword
1249 if self.start_keyword(keyword) is not False:
1250 self.end_keyword(keyword)
1252 def start_keyword(self, keyword):
1253 """Called when keyword starts. Default implementation does nothing.
1255 :param keyword: Keyword to process.
1256 :type keyword: Keyword
1260 if keyword.type in ("setup", "SETUP"):
1261 self.visit_setup_kw(keyword)
1262 elif keyword.type in ("teardown", "TEARDOWN"):
1263 self.visit_teardown_kw(keyword)
1265 self.visit_test_kw(keyword)
1266 except AttributeError:
1269 def end_keyword(self, keyword):
1270 """Called when keyword ends. Default implementation does nothing.
1272 :param keyword: Keyword to process.
1273 :type keyword: Keyword
1277 def visit_test_kw(self, test_kw):
1278 """Implements traversing through the test keyword and its child
1281 :param test_kw: Keyword to process.
1282 :type test_kw: Keyword
1285 for keyword in test_kw.body:
1286 if self.start_test_kw(keyword) is not False:
1287 self.visit_test_kw(keyword)
1288 self.end_test_kw(keyword)
1290 def start_test_kw(self, test_kw):
1291 """Called when test keyword starts. Default implementation does
1294 :param test_kw: Keyword to process.
1295 :type test_kw: Keyword
1298 if not self._process_oper:
1301 if test_kw.name.count(u"Run Telemetry On All Duts"):
1302 self._msg_type = u"test-telemetry"
1303 self._telemetry_kw_counter += 1
1304 elif test_kw.name.count(u"Show Runtime On All Duts"):
1305 self._msg_type = u"test-show-runtime"
1306 self._sh_run_counter += 1
1309 test_kw.messages.visit(self)
1311 def end_test_kw(self, test_kw):
1312 """Called when keyword ends. Default implementation does nothing.
1314 :param test_kw: Keyword to process.
1315 :type test_kw: Keyword
1319 def visit_setup_kw(self, setup_kw):
1320 """Implements traversing through the teardown keyword and its child
1323 :param setup_kw: Keyword to process.
1324 :type setup_kw: Keyword
1327 for keyword in setup_kw.setup:
1328 if self.start_setup_kw(keyword) is not False:
1329 self.visit_setup_kw(keyword)
1330 self.end_setup_kw(keyword)
1331 for keyword in setup_kw.body:
1332 if self.start_setup_kw(keyword) is not False:
1333 self.visit_setup_kw(keyword)
1334 self.end_setup_kw(keyword)
1336 def start_setup_kw(self, setup_kw):
1337 """Called when teardown keyword starts. Default implementation does
1340 :param setup_kw: Keyword to process.
1341 :type setup_kw: Keyword
1344 if setup_kw.name.count(u"Show Vpp Version On All Duts") \
1345 and not self._version:
1346 self._msg_type = u"vpp-version"
1347 elif setup_kw.name.count(u"Install Dpdk Framework On All Duts") and \
1349 self._msg_type = u"dpdk-version"
1350 elif setup_kw.name.count(u"Setup Framework") and not self._testbed:
1351 self._msg_type = u"testbed"
1354 setup_kw.messages.visit(self)
1356 def end_setup_kw(self, setup_kw):
1357 """Called when keyword ends. Default implementation does nothing.
1359 :param setup_kw: Keyword to process.
1360 :type setup_kw: Keyword
1364 def visit_teardown_kw(self, teardown_kw):
1365 """Implements traversing through the teardown keyword and its child
1368 :param teardown_kw: Keyword to process.
1369 :type teardown_kw: Keyword
1372 for keyword in teardown_kw.body:
1373 if self.start_teardown_kw(keyword) is not False:
1374 self.visit_teardown_kw(keyword)
1375 self.end_teardown_kw(keyword)
1377 def start_teardown_kw(self, teardown_kw):
1378 """Called when teardown keyword starts
1380 :param teardown_kw: Keyword to process.
1381 :type teardown_kw: Keyword
1384 if teardown_kw.name.count(u"Show Papi History On All Duts"):
1385 self._conf_history_lookup_nr = 0
1386 self._msg_type = u"teardown-papi-history"
1387 teardown_kw.messages.visit(self)
1389 def end_teardown_kw(self, teardown_kw):
1390 """Called when keyword ends. Default implementation does nothing.
1392 :param teardown_kw: Keyword to process.
1393 :type teardown_kw: Keyword
1397 def visit_message(self, msg):
1398 """Implements visiting the message.
1400 :param msg: Message to process.
1404 if self.start_message(msg) is not False:
1405 self.end_message(msg)
1407 def start_message(self, msg):
1408 """Called when message starts. Get required information from messages:
1411 :param msg: Message to process.
1416 self.parse_msg[self._msg_type](msg)
1418 def end_message(self, msg):
1419 """Called when message ends. Default implementation does nothing.
1421 :param msg: Message to process.
1430 The data is extracted from output.xml files generated by Jenkins jobs and
1431 stored in pandas' DataFrames.
1437 (as described in ExecutionChecker documentation)
1439 (as described in ExecutionChecker documentation)
1441 (as described in ExecutionChecker documentation)
1444 def __init__(self, spec, for_output):
1447 :param spec: Specification.
1448 :param for_output: Output to be generated from downloaded data.
1449 :type spec: Specification
1450 :type for_output: str
1456 self._for_output = for_output
1459 self._input_data = pd.Series(dtype="float64")
1463 """Getter - Input data.
1465 :returns: Input data
1466 :rtype: pandas.Series
1468 return self._input_data
1470 def metadata(self, job, build):
1471 """Getter - metadata
1473 :param job: Job which metadata we want.
1474 :param build: Build which metadata we want.
1478 :rtype: pandas.Series
1480 return self.data[job][build][u"metadata"]
1482 def suites(self, job, build):
1485 :param job: Job which suites we want.
1486 :param build: Build which suites we want.
1490 :rtype: pandas.Series
1492 return self.data[job][str(build)][u"suites"]
1494 def tests(self, job, build):
1497 :param job: Job which tests we want.
1498 :param build: Build which tests we want.
1502 :rtype: pandas.Series
1504 return self.data[job][build][u"tests"]
1506 def _parse_tests(self, job, build):
1507 """Process data from robot output.xml file and return JSON structured
1510 :param job: The name of job which build output data will be processed.
1511 :param build: The build which output data will be processed.
1514 :returns: JSON data structure.
1523 with open(build[u"file-name"], u'r') as data_file:
1525 result = ExecutionResult(data_file)
1526 except errors.DataError as err:
1528 f"Error occurred while parsing output.xml: {repr(err)}"
1532 process_oper = False
1533 if u"-vpp-perf-report-coverage-" in job:
1535 # elif u"-vpp-perf-report-iterative-" in job:
1536 # # Exceptions for TBs where we do not have coverage data:
1537 # for item in (u"-2n-icx", ):
1539 # process_oper = True
1541 checker = ExecutionChecker(
1542 metadata, self._cfg.mapping, self._cfg.ignore, process_oper
1544 result.visit(checker)
1546 checker.data[u"metadata"][u"tests_total"] = \
1547 result.statistics.total.total
1548 checker.data[u"metadata"][u"tests_passed"] = \
1549 result.statistics.total.passed
1550 checker.data[u"metadata"][u"tests_failed"] = \
1551 result.statistics.total.failed
1552 checker.data[u"metadata"][u"elapsedtime"] = result.suite.elapsedtime
1553 checker.data[u"metadata"][u"generated"] = result.suite.endtime[:14]
1557 def _download_and_parse_build(self, job, build, repeat, pid=10000):
1558 """Download and parse the input data file.
1560 :param pid: PID of the process executing this method.
1561 :param job: Name of the Jenkins job which generated the processed input
1563 :param build: Information about the Jenkins build which generated the
1564 processed input file.
1565 :param repeat: Repeat the download specified number of times if not
1573 logging.info(f"Processing the job/build: {job}: {build[u'build']}")
1580 success = download_and_unzip_data_file(self._cfg, job, build, pid)
1586 f"It is not possible to download the input data file from the "
1587 f"job {job}, build {build[u'build']}, or it is damaged. "
1591 logging.info(f" Processing data from build {build[u'build']}")
1592 data = self._parse_tests(job, build)
1595 f"Input data file from the job {job}, build "
1596 f"{build[u'build']} is damaged. Skipped."
1599 state = u"processed"
1602 remove(build[u"file-name"])
1603 except OSError as err:
1605 f"Cannot remove the file {build[u'file-name']}: {repr(err)}"
1608 # If the time-period is defined in the specification file, remove all
1609 # files which are outside the time period.
1611 timeperiod = self._cfg.environment.get(u"time-period", None)
1612 if timeperiod and data:
1614 timeperiod = timedelta(int(timeperiod))
1615 metadata = data.get(u"metadata", None)
1617 generated = metadata.get(u"generated", None)
1619 generated = dt.strptime(generated, u"%Y%m%d %H:%M")
1620 if (now - generated) > timeperiod:
1621 # Remove the data and the file:
1626 f" The build {job}/{build[u'build']} is "
1627 f"outdated, will be removed."
1637 def download_and_parse_data(self, repeat=1):
1638 """Download the input data files, parse input data from input files and
1639 store in pandas' Series.
1641 :param repeat: Repeat the download specified number of times if not
1646 logging.info(u"Downloading and parsing input files ...")
1648 for job, builds in self._cfg.input.items():
1649 for build in builds:
1651 result = self._download_and_parse_build(job, build, repeat)
1654 build_nr = result[u"build"][u"build"]
1657 data = result[u"data"]
1658 build_data = pd.Series({
1659 u"metadata": pd.Series(
1660 list(data[u"metadata"].values()),
1661 index=list(data[u"metadata"].keys())
1663 u"suites": pd.Series(
1664 list(data[u"suites"].values()),
1665 index=list(data[u"suites"].keys())
1667 u"tests": pd.Series(
1668 list(data[u"tests"].values()),
1669 index=list(data[u"tests"].keys())
1673 if self._input_data.get(job, None) is None:
1674 self._input_data[job] = pd.Series(dtype="float64")
1675 self._input_data[job][str(build_nr)] = build_data
1676 self._cfg.set_input_file_name(
1677 job, build_nr, result[u"build"][u"file-name"]
1679 self._cfg.set_input_state(job, build_nr, result[u"state"])
1682 resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000
1683 logging.info(f"Memory allocation: {mem_alloc:.0f}MB")
1685 logging.info(u"Done.")
1687 msg = f"Successful downloads from the sources:\n"
1688 for source in self._cfg.environment[u"data-sources"]:
1689 if source[u"successful-downloads"]:
1691 f"{source[u'url']}/{source[u'path']}/"
1692 f"{source[u'file-name']}: "
1693 f"{source[u'successful-downloads']}\n"
1697 def process_local_file(self, local_file, job=u"local", build_nr=1,
1699 """Process local XML file given as a command-line parameter.
1701 :param local_file: The file to process.
1702 :param job: Job name.
1703 :param build_nr: Build number.
1704 :param replace: If True, the information about jobs and builds is
1705 replaced by the new one, otherwise the new jobs and builds are
1707 :type local_file: str
1711 :raises: PresentationError if an error occurs.
1713 if not isfile(local_file):
1714 raise PresentationError(f"The file {local_file} does not exist.")
1717 build_nr = int(local_file.split(u"/")[-1].split(u".")[0])
1718 except (IndexError, ValueError):
1723 u"status": u"failed",
1724 u"file-name": local_file
1727 self._cfg.input = dict()
1728 self._cfg.add_build(job, build)
1730 logging.info(f"Processing {job}: {build_nr:2d}: {local_file}")
1731 data = self._parse_tests(job, build)
1733 raise PresentationError(
1734 f"Error occurred while parsing the file {local_file}"
1737 build_data = pd.Series({
1738 u"metadata": pd.Series(
1739 list(data[u"metadata"].values()),
1740 index=list(data[u"metadata"].keys())
1742 u"suites": pd.Series(
1743 list(data[u"suites"].values()),
1744 index=list(data[u"suites"].keys())
1746 u"tests": pd.Series(
1747 list(data[u"tests"].values()),
1748 index=list(data[u"tests"].keys())
1752 if self._input_data.get(job, None) is None:
1753 self._input_data[job] = pd.Series(dtype="float64")
1754 self._input_data[job][str(build_nr)] = build_data
1756 self._cfg.set_input_state(job, build_nr, u"processed")
1758 def process_local_directory(self, local_dir, replace=True):
1759 """Process local directory with XML file(s). The directory is processed
1760 as a 'job' and the XML files in it as builds.
1761 If the given directory contains only sub-directories, these
1762 sub-directories processed as jobs and corresponding XML files as builds
1765 :param local_dir: Local directory to process.
1766 :param replace: If True, the information about jobs and builds is
1767 replaced by the new one, otherwise the new jobs and builds are
1769 :type local_dir: str
1772 if not isdir(local_dir):
1773 raise PresentationError(
1774 f"The directory {local_dir} does not exist."
1777 # Check if the given directory includes only files, or only directories
1778 _, dirnames, filenames = next(walk(local_dir))
1780 if filenames and not dirnames:
1783 # key: dir (job) name, value: list of file names (builds)
1785 local_dir: [join(local_dir, name) for name in filenames]
1788 elif dirnames and not filenames:
1791 # key: dir (job) name, value: list of file names (builds)
1792 local_builds = dict()
1793 for dirname in dirnames:
1795 join(local_dir, dirname, name)
1796 for name in listdir(join(local_dir, dirname))
1797 if isfile(join(local_dir, dirname, name))
1800 local_builds[dirname] = sorted(builds)
1802 elif not filenames and not dirnames:
1803 raise PresentationError(f"The directory {local_dir} is empty.")
1805 raise PresentationError(
1806 f"The directory {local_dir} can include only files or only "
1807 f"directories, not both.\nThe directory {local_dir} includes "
1808 f"file(s):\n{filenames}\nand directories:\n{dirnames}"
1812 self._cfg.input = dict()
1814 for job, files in local_builds.items():
1815 for idx, local_file in enumerate(files):
1816 self.process_local_file(local_file, job, idx + 1, replace=False)
1819 def _end_of_tag(tag_filter, start=0, closer=u"'"):
1820 """Return the index of character in the string which is the end of tag.
1822 :param tag_filter: The string where the end of tag is being searched.
1823 :param start: The index where the searching is stated.
1824 :param closer: The character which is the tag closer.
1825 :type tag_filter: str
1828 :returns: The index of the tag closer.
1832 idx_opener = tag_filter.index(closer, start)
1833 return tag_filter.index(closer, idx_opener + 1)
1838 def _condition(tag_filter):
1839 """Create a conditional statement from the given tag filter.
1841 :param tag_filter: Filter based on tags from the element specification.
1842 :type tag_filter: str
1843 :returns: Conditional statement which can be evaluated.
1848 index = InputData._end_of_tag(tag_filter, index)
1852 tag_filter = tag_filter[:index] + u" in tags" + tag_filter[index:]
1854 def filter_data(self, element, params=None, data=None, data_set=u"tests",
1855 continue_on_error=False):
1856 """Filter required data from the given jobs and builds.
1858 The output data structure is:
1861 - test (or suite) 1 ID:
1867 - test (or suite) n ID:
1874 :param element: Element which will use the filtered data.
1875 :param params: Parameters which will be included in the output. If None,
1876 all parameters are included.
1877 :param data: If not None, this data is used instead of data specified
1879 :param data_set: The set of data to be filtered: tests, suites,
1881 :param continue_on_error: Continue if there is error while reading the
1882 data. The Item will be empty then
1883 :type element: pandas.Series
1887 :type continue_on_error: bool
1888 :returns: Filtered data.
1889 :rtype pandas.Series
1893 if data_set == "suites":
1895 elif element[u"filter"] in (u"all", u"template"):
1898 cond = InputData._condition(element[u"filter"])
1899 logging.debug(f" Filter: {cond}")
1901 logging.error(u" No filter defined.")
1905 params = element.get(u"parameters", None)
1907 params.extend((u"type", u"status"))
1909 data_to_filter = data if data else element[u"data"]
1910 data = pd.Series(dtype="float64")
1912 for job, builds in data_to_filter.items():
1913 data[job] = pd.Series(dtype="float64")
1914 for build in builds:
1915 data[job][str(build)] = pd.Series(dtype="float64")
1918 self.data[job][str(build)][data_set].items())
1920 if continue_on_error:
1924 for test_id, test_data in data_dict.items():
1925 if eval(cond, {u"tags": test_data.get(u"tags", u"")}):
1926 data[job][str(build)][test_id] = \
1927 pd.Series(dtype="float64")
1929 for param, val in test_data.items():
1930 data[job][str(build)][test_id][param] = val
1932 for param in params:
1934 data[job][str(build)][test_id][param] =\
1937 data[job][str(build)][test_id][param] =\
1941 except (KeyError, IndexError, ValueError) as err:
1943 f"Missing mandatory parameter in the element specification: "
1947 except AttributeError as err:
1948 logging.error(repr(err))
1950 except SyntaxError as err:
1952 f"The filter {cond} is not correct. Check if all tags are "
1953 f"enclosed by apostrophes.\n{repr(err)}"
1957 def filter_tests_by_name(self, element, params=None, data_set=u"tests",
1958 continue_on_error=False):
1959 """Filter required data from the given jobs and builds.
1961 The output data structure is:
1964 - test (or suite) 1 ID:
1970 - test (or suite) n ID:
1977 :param element: Element which will use the filtered data.
1978 :param params: Parameters which will be included in the output. If None,
1979 all parameters are included.
1980 :param data_set: The set of data to be filtered: tests, suites,
1982 :param continue_on_error: Continue if there is error while reading the
1983 data. The Item will be empty then
1984 :type element: pandas.Series
1987 :type continue_on_error: bool
1988 :returns: Filtered data.
1989 :rtype pandas.Series
1992 include = element.get(u"include", None)
1994 logging.warning(u"No tests to include, skipping the element.")
1998 params = element.get(u"parameters", None)
1999 if params and u"type" not in params:
2000 params.append(u"type")
2002 cores = element.get(u"core", None)
2006 for test in include:
2007 tests.append(test.format(core=core))
2011 data = pd.Series(dtype="float64")
2013 for job, builds in element[u"data"].items():
2014 data[job] = pd.Series(dtype="float64")
2015 for build in builds:
2016 data[job][str(build)] = pd.Series(dtype="float64")
2019 reg_ex = re.compile(str(test).lower())
2020 for test_id in self.data[job][
2021 str(build)][data_set].keys():
2022 if re.match(reg_ex, str(test_id).lower()):
2023 test_data = self.data[job][
2024 str(build)][data_set][test_id]
2025 data[job][str(build)][test_id] = \
2026 pd.Series(dtype="float64")
2028 for param, val in test_data.items():
2029 data[job][str(build)][test_id]\
2032 for param in params:
2034 data[job][str(build)][
2038 data[job][str(build)][
2039 test_id][param] = u"No Data"
2040 except KeyError as err:
2041 if continue_on_error:
2042 logging.debug(repr(err))
2044 logging.error(repr(err))
2048 except (KeyError, IndexError, ValueError) as err:
2050 f"Missing mandatory parameter in the element "
2051 f"specification: {repr(err)}"
2054 except AttributeError as err:
2055 logging.error(repr(err))
2059 def merge_data(data):
2060 """Merge data from more jobs and builds to a simple data structure.
2062 The output data structure is:
2064 - test (suite) 1 ID:
2070 - test (suite) n ID:
2073 :param data: Data to merge.
2074 :type data: pandas.Series
2075 :returns: Merged data.
2076 :rtype: pandas.Series
2079 logging.info(u" Merging data ...")
2081 merged_data = pd.Series(dtype="float64")
2082 for builds in data.values:
2083 for item in builds.values:
2084 for item_id, item_data in item.items():
2085 merged_data[item_id] = item_data
2088 def print_all_oper_data(self):
2089 """Print all operational data to console.
2092 for job in self._input_data.values:
2093 for build in job.values:
2094 for test_id, test_data in build[u"tests"].items():
2096 if test_data.get(u"show-run", None) is None:
2098 for dut_name, data in test_data[u"show-run"].items():
2099 if data.get(u"runtime", None) is None:
2101 runtime = loads(data[u"runtime"])
2103 threads_nr = len(runtime[0][u"clocks"])
2104 except (IndexError, KeyError):
2106 threads = OrderedDict(
2107 {idx: list() for idx in range(threads_nr)})
2108 for item in runtime:
2109 for idx in range(threads_nr):
2110 if item[u"vectors"][idx] > 0:
2111 clocks = item[u"clocks"][idx] / \
2112 item[u"vectors"][idx]
2113 elif item[u"calls"][idx] > 0:
2114 clocks = item[u"clocks"][idx] / \
2116 elif item[u"suspends"][idx] > 0:
2117 clocks = item[u"clocks"][idx] / \
2118 item[u"suspends"][idx]
2122 if item[u"calls"][idx] > 0:
2123 vectors_call = item[u"vectors"][idx] / \
2128 if int(item[u"calls"][idx]) + int(
2129 item[u"vectors"][idx]) + \
2130 int(item[u"suspends"][idx]):
2131 threads[idx].append([
2133 item[u"calls"][idx],
2134 item[u"vectors"][idx],
2135 item[u"suspends"][idx],
2140 print(f"Host IP: {data.get(u'host', '')}, "
2141 f"Socket: {data.get(u'socket', '')}")
2142 for thread_nr, thread in threads.items():
2143 txt_table = prettytable.PrettyTable(
2149 u"Cycles per Packet",
2150 u"Average Vector Size"
2155 txt_table.add_row(row)
2157 if len(thread) == 0:
2160 avg = f", Average Vector Size per Node: " \
2161 f"{(avg / len(thread)):.2f}"
2162 th_name = u"main" if thread_nr == 0 \
2163 else f"worker_{thread_nr}"
2164 print(f"{dut_name}, {th_name}{avg}")
2165 txt_table.float_format = u".2"
2166 txt_table.align = u"r"
2167 txt_table.align[u"Name"] = u"l"
2168 print(f"{txt_table.get_string()}\n")