Report: Add mrr stdev to comp tables
[csit.git] / resources / tools / presentation / input_data_parser.py
index f7869d8..987b996 100644 (file)
@@ -25,10 +25,12 @@ import resource
 import logging
 
 from collections import OrderedDict
 import logging
 
 from collections import OrderedDict
-from os import remove
+from os import remove, walk, listdir
+from os.path import isfile, isdir, join
 from datetime import datetime as dt
 from datetime import timedelta
 from json import loads
 from datetime import datetime as dt
 from datetime import timedelta
 from json import loads
+from json.decoder import JSONDecodeError
 
 import hdrh.histogram
 import hdrh.codec
 
 import hdrh.histogram
 import hdrh.codec
@@ -40,6 +42,7 @@ from robot import errors
 
 from resources.libraries.python import jumpavg
 from input_data_files import download_and_unzip_data_file
 
 from resources.libraries.python import jumpavg
 from input_data_files import download_and_unzip_data_file
+from pal_errors import PresentationError
 
 
 # Separator used in file names
 
 
 # Separator used in file names
@@ -871,6 +874,40 @@ class ExecutionChecker(ResultVisitor):
 
         return latency, u"FAIL"
 
 
         return latency, u"FAIL"
 
+    @staticmethod
+    def _get_hoststack_data(msg, tags):
+        """Get data from the hoststack test message.
+
+        :param msg: The test message to be parsed.
+        :param tags: Test tags.
+        :type msg: str
+        :type tags: list
+        :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
+        :rtype: tuple(dict, str)
+        """
+        result = dict()
+        status = u"FAIL"
+
+        msg = msg.replace(u"'", u'"').replace(u" ", u"")
+        if u"LDPRELOAD" in tags:
+            try:
+                result = loads(msg)
+                status = u"PASS"
+            except JSONDecodeError:
+                pass
+        elif u"VPPECHO" in tags:
+            try:
+                msg_lst = msg.replace(u"}{", u"} {").split(u" ")
+                result = dict(
+                    client=loads(msg_lst[0]),
+                    server=loads(msg_lst[1])
+                )
+                status = u"PASS"
+            except (JSONDecodeError, IndexError):
+                pass
+
+        return result, status
+
     def visit_suite(self, suite):
         """Implements traversing through the suite and its direct children.
 
     def visit_suite(self, suite):
         """Implements traversing through the suite and its direct children.
 
@@ -1040,6 +1077,10 @@ class ExecutionChecker(ResultVisitor):
                 test_result[u"type"] = u"SOAK"
                 test_result[u"throughput"], test_result[u"status"] = \
                     self._get_plr_throughput(test.message)
                 test_result[u"type"] = u"SOAK"
                 test_result[u"throughput"], test_result[u"status"] = \
                     self._get_plr_throughput(test.message)
+            elif u"HOSTSTACK" in tags:
+                test_result[u"type"] = u"HOSTSTACK"
+                test_result[u"result"], test_result[u"status"] = \
+                    self._get_hoststack_data(test.message, tags)
             elif u"TCP" in tags:
                 test_result[u"type"] = u"TCP"
                 groups = re.search(self.REGEX_TCP, test.message)
             elif u"TCP" in tags:
                 test_result[u"type"] = u"TCP"
                 groups = re.search(self.REGEX_TCP, test.message)
@@ -1059,6 +1100,7 @@ class ExecutionChecker(ResultVisitor):
                     # Use whole list in CSIT-1180.
                     stats = jumpavg.AvgStdevStats.for_runs(items_float)
                     test_result[u"result"][u"receive-rate"] = stats.avg
                     # Use whole list in CSIT-1180.
                     stats = jumpavg.AvgStdevStats.for_runs(items_float)
                     test_result[u"result"][u"receive-rate"] = stats.avg
+                    test_result[u"result"][u"receive-stdev"] = stats.stdev
                 else:
                     groups = re.search(self.REGEX_MRR, test.message)
                     test_result[u"result"][u"receive-rate"] = \
                 else:
                     groups = re.search(self.REGEX_MRR, test.message)
                     test_result[u"result"][u"receive-rate"] = \
@@ -1152,7 +1194,8 @@ class ExecutionChecker(ResultVisitor):
                 test_kw.name.count(u"Show Runtime Counters On All Duts"):
             self._msg_type = u"test-show-runtime"
             self._sh_run_counter += 1
                 test_kw.name.count(u"Show Runtime Counters On All Duts"):
             self._msg_type = u"test-show-runtime"
             self._sh_run_counter += 1
-        elif test_kw.name.count(u"Install Dpdk Test") and not self._version:
+        elif test_kw.name.count(u"Install Dpdk Test On All Duts") and \
+                not self._version:
             self._msg_type = u"dpdk-version"
         else:
             return
             self._msg_type = u"dpdk-version"
         else:
             return
@@ -1264,7 +1307,6 @@ class ExecutionChecker(ResultVisitor):
         :type msg: Message
         :returns: Nothing.
         """
         :type msg: Message
         :returns: Nothing.
         """
-
         if self._msg_type:
             self.parse_msg[self._msg_type](msg)
 
         if self._msg_type:
             self.parse_msg[self._msg_type](msg)
 
@@ -1326,7 +1368,6 @@ class InputData:
         :returns: Metadata
         :rtype: pandas.Series
         """
         :returns: Metadata
         :rtype: pandas.Series
         """
-
         return self.data[job][build][u"metadata"]
 
     def suites(self, job, build):
         return self.data[job][build][u"metadata"]
 
     def suites(self, job, build):
@@ -1339,7 +1380,6 @@ class InputData:
         :returns: Suites.
         :rtype: pandas.Series
         """
         :returns: Suites.
         :rtype: pandas.Series
         """
-
         return self.data[job][str(build)][u"suites"]
 
     def tests(self, job, build):
         return self.data[job][str(build)][u"suites"]
 
     def tests(self, job, build):
@@ -1352,7 +1392,6 @@ class InputData:
         :returns: Tests.
         :rtype: pandas.Series
         """
         :returns: Tests.
         :rtype: pandas.Series
         """
-
         return self.data[job][build][u"tests"]
 
     def _parse_tests(self, job, build, log):
         return self.data[job][build][u"tests"]
 
     def _parse_tests(self, job, build, log):
@@ -1536,6 +1575,122 @@ class InputData:
 
         logging.info(u"Done.")
 
 
         logging.info(u"Done.")
 
+    def process_local_file(self, local_file, job=u"local", build_nr=1,
+                           replace=True):
+        """Process local XML file given as a command-line parameter.
+
+        :param local_file: The file to process.
+        :param job: Job name.
+        :param build_nr: Build number.
+        :param replace: If True, the information about jobs and builds is
+            replaced by the new one, otherwise the new jobs and builds are
+            added.
+        :type local_file: str
+        :type job: str
+        :type build_nr: int
+        :type replace: bool
+        :raises: PresentationError if an error occurs.
+        """
+        if not isfile(local_file):
+            raise PresentationError(f"The file {local_file} does not exist.")
+
+        build = {
+            u"build": build_nr,
+            u"status": u"failed",
+            u"file-name": local_file
+        }
+        if replace:
+            self._cfg.builds = dict()
+        self._cfg.add_build(job, build)
+
+        logging.info(f"Processing {job}: {build_nr:2d}: {local_file}")
+        data = self._parse_tests(job, build, list())
+        if data is None:
+            raise PresentationError(
+                f"Error occurred while parsing the file {local_file}"
+            )
+
+        build_data = pd.Series({
+            u"metadata": pd.Series(
+                list(data[u"metadata"].values()),
+                index=list(data[u"metadata"].keys())
+            ),
+            u"suites": pd.Series(
+                list(data[u"suites"].values()),
+                index=list(data[u"suites"].keys())
+            ),
+            u"tests": pd.Series(
+                list(data[u"tests"].values()),
+                index=list(data[u"tests"].keys())
+            )
+        })
+
+        if self._input_data.get(job, None) is None:
+            self._input_data[job] = pd.Series()
+        self._input_data[job][str(build_nr)] = build_data
+
+        self._cfg.set_input_state(job, build_nr, u"processed")
+
+    def process_local_directory(self, local_dir, replace=True):
+        """Process local directory with XML file(s). The directory is processed
+        as a 'job' and the XML files in it as builds.
+        If the given directory contains only sub-directories, these
+        sub-directories processed as jobs and corresponding XML files as builds
+        of their job.
+
+        :param local_dir: Local directory to process.
+        :param replace: If True, the information about jobs and builds is
+            replaced by the new one, otherwise the new jobs and builds are
+            added.
+        :type local_dir: str
+        :type replace: bool
+        """
+        if not isdir(local_dir):
+            raise PresentationError(
+                f"The directory {local_dir} does not exist."
+            )
+
+        # Check if the given directory includes only files, or only directories
+        _, dirnames, filenames = next(walk(local_dir))
+
+        if filenames and not dirnames:
+            filenames.sort()
+            # local_builds:
+            # key: dir (job) name, value: list of file names (builds)
+            local_builds = {
+                local_dir: [join(local_dir, name) for name in filenames]
+            }
+
+        elif dirnames and not filenames:
+            dirnames.sort()
+            # local_builds:
+            # key: dir (job) name, value: list of file names (builds)
+            local_builds = dict()
+            for dirname in dirnames:
+                builds = [
+                    join(local_dir, dirname, name)
+                    for name in listdir(join(local_dir, dirname))
+                    if isfile(join(local_dir, dirname, name))
+                ]
+                if builds:
+                    local_builds[dirname] = sorted(builds)
+
+        elif not filenames and not dirnames:
+            raise PresentationError(f"The directory {local_dir} is empty.")
+        else:
+            raise PresentationError(
+                f"The directory {local_dir} can include only files or only "
+                f"directories, not both.\nThe directory {local_dir} includes "
+                f"file(s):\n{filenames}\nand directories:\n{dirnames}"
+            )
+
+        if replace:
+            self._cfg.builds = dict()
+
+        for job, files in local_builds.items():
+            for idx, local_file in enumerate(files):
+                self.process_local_file(local_file, job, idx + 1, replace=False)
+
     @staticmethod
     def _end_of_tag(tag_filter, start=0, closer=u"'"):
         """Return the index of character in the string which is the end of tag.
     @staticmethod
     def _end_of_tag(tag_filter, start=0, closer=u"'"):
         """Return the index of character in the string which is the end of tag.
@@ -1549,7 +1704,6 @@ class InputData:
         :returns: The index of the tag closer.
         :rtype: int
         """
         :returns: The index of the tag closer.
         :rtype: int
         """
-
         try:
             idx_opener = tag_filter.index(closer, start)
             return tag_filter.index(closer, idx_opener + 1)
         try:
             idx_opener = tag_filter.index(closer, start)
             return tag_filter.index(closer, idx_opener + 1)
@@ -1565,7 +1719,6 @@ class InputData:
         :returns: Conditional statement which can be evaluated.
         :rtype: str
         """
         :returns: Conditional statement which can be evaluated.
         :rtype: str
         """
-
         index = 0
         while True:
             index = InputData._end_of_tag(tag_filter, index)
         index = 0
         while True:
             index = InputData._end_of_tag(tag_filter, index)
@@ -1579,7 +1732,6 @@ class InputData:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
-
         - job 1
           - build 1
             - test (or suite) 1 ID:
         - job 1
           - build 1
             - test (or suite) 1 ID:
@@ -1682,7 +1834,6 @@ class InputData:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
-
         - job 1
           - build 1
             - test (or suite) 1 ID:
         - job 1
           - build 1
             - test (or suite) 1 ID:
@@ -1796,7 +1947,6 @@ class InputData:
             for item in builds.values:
                 for item_id, item_data in item.items():
                     merged_data[item_id] = item_data
             for item in builds.values:
                 for item_id, item_data in item.items():
                     merged_data[item_id] = item_data
-
         return merged_data
 
     def print_all_oper_data(self):
         return merged_data
 
     def print_all_oper_data(self):