enb,epc,ms: refactor KPI API

we previously mixed component specific and component agnostic APIs
(stdout vs. log file for example) for setting and retrieving KPI.

This patch propose to use a single abstract get_kpis() method for
all components that can be enriched with component-specific
stuff as desired.

In the case of srsLTE blocks, the main implementation will
remain in srslte_common() and is shared among srsENB/srsUE/srsEPC.

The KPI analyzer in srslte_common() extract and also manages
all three KPI sources (log, csv and stdout) independently.

In addition to the get_kpis() method that always returns a flat
dictionary, it also exposes get_kpi_tree() that return
a dict of KPI dicts that will be used for the Junit.xml generation.

Change-Id: I4bacc6b8a0cb92a581edfb947100b57022265265
diff --git a/src/osmo_gsm_tester/obj/enb.py b/src/osmo_gsm_tester/obj/enb.py
index 32ead69..15a0033 100644
--- a/src/osmo_gsm_tester/obj/enb.py
+++ b/src/osmo_gsm_tester/obj/enb.py
@@ -356,4 +356,8 @@
     def get_counter(self, counter_name):
         pass
 
-# vim: expandtab tabstop=4 shiftwidth=4
+    @abstractmethod
+    def get_kpis(self):
+        pass
+
+# vim: expandtab tabstop=4 shiftwidth=4
\ No newline at end of file
diff --git a/src/osmo_gsm_tester/obj/enb_amarisoft.py b/src/osmo_gsm_tester/obj/enb_amarisoft.py
index e97bb90..01aed18 100644
--- a/src/osmo_gsm_tester/obj/enb_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/enb_amarisoft.py
@@ -260,6 +260,9 @@
             return self.process.get_counter_stdout('PRACH:')
         raise log.Error('counter %s not implemented!' % counter_name)
 
+    def get_kpis(self):
+        return {}
+
     def get_rfemu(self, cell=0, dl=True):
         cell_list = self.gen_conf['enb'].get('cell_list', None)
         if cell_list is None or len(cell_list) < cell + 1:
diff --git a/src/osmo_gsm_tester/obj/enb_srs.py b/src/osmo_gsm_tester/obj/enb_srs.py
index aee3f61..83df5ed 100644
--- a/src/osmo_gsm_tester/obj/enb_srs.py
+++ b/src/osmo_gsm_tester/obj/enb_srs.py
@@ -101,7 +101,7 @@
                 self.log(repr(e))
 
         # Collect KPIs for each TC
-        self.testenv.test().set_kpis(self.get_kpis())
+        self.testenv.test().set_kpis(self.get_kpi_tree())
         # Clean up for parent class:
         super().cleanup()
 
@@ -267,6 +267,9 @@
             return self.process.get_counter_stdout('RACH:')
         raise log.Error('counter %s not implemented!' % counter_name)
 
+    def get_kpis(self):
+        return srslte_common.get_kpis(self)
+
     def get_rfemu(self, cell=0, dl=True):
         cell_list = self.gen_conf['enb'].get('cell_list', None)
         if cell_list is None or len(cell_list) < cell + 1:
diff --git a/src/osmo_gsm_tester/obj/epc.py b/src/osmo_gsm_tester/obj/epc.py
index 6f056fc..aaa96b7 100644
--- a/src/osmo_gsm_tester/obj/epc.py
+++ b/src/osmo_gsm_tester/obj/epc.py
@@ -116,4 +116,8 @@
     def run_node(self):
         return self._run_node
 
+    @abstractmethod
+    def get_kpis(self):
+        pass
+
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/obj/epc_amarisoft.py b/src/osmo_gsm_tester/obj/epc_amarisoft.py
index 1291891..4c3bf07 100644
--- a/src/osmo_gsm_tester/obj/epc_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/epc_amarisoft.py
@@ -199,4 +199,7 @@
         # TODO: set proper addr
         return '192.168.4.1'
 
+    def get_kpis(self):
+        return {}
+
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/obj/epc_srs.py b/src/osmo_gsm_tester/obj/epc_srs.py
index 6a7a20e..6a0a7bb 100644
--- a/src/osmo_gsm_tester/obj/epc_srs.py
+++ b/src/osmo_gsm_tester/obj/epc_srs.py
@@ -219,4 +219,7 @@
     def tun_addr(self):
         return '172.16.0.1'
 
+    def get_kpis(self):
+        return srslte_common.get_kpis(self)
+
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/obj/ms_srs.py b/src/osmo_gsm_tester/obj/ms_srs.py
index 2f19f3f..aaeeca5 100644
--- a/src/osmo_gsm_tester/obj/ms_srs.py
+++ b/src/osmo_gsm_tester/obj/ms_srs.py
@@ -126,7 +126,7 @@
                 self.log(repr(e))
 
         # Collect KPIs for each TC
-        self.testenv.test().set_kpis(self.get_kpis())
+        self.testenv.test().set_kpis(self.get_kpi_tree())
 
     def features(self):
         return self._conf.get('features', [])
diff --git a/src/osmo_gsm_tester/obj/srslte_common.py b/src/osmo_gsm_tester/obj/srslte_common.py
index 21001b7..cbc360f 100644
--- a/src/osmo_gsm_tester/obj/srslte_common.py
+++ b/src/osmo_gsm_tester/obj/srslte_common.py
@@ -27,7 +27,9 @@
         self.process = None
         self.metrics_file = None
         self.stop_sleep_time = 6 # We require at most 5s to stop
-        self.kpis = None
+        self.log_kpi = None
+        self.stdout_kpi = None
+        self.csv_kpi = None
 
     def sleep_after_stop(self):
         # Only sleep once
@@ -42,61 +44,50 @@
         self.sleep_after_stop()
 
     def get_kpis(self):
-        ''' Return all KPI '''
-        if self.kpis is None:
-            self.extract_kpis()
-        return self.kpis
+        ''' Merge all KPI and return as flat dict '''
+        self.extract_kpis()
+        kpi_flat = {}
+        kpi_flat.update(self.log_kpi)
+        kpi_flat.update(self.stdout_kpi)
+        kpi_flat.update(self.csv_kpi)
+        return kpi_flat
 
-    def get_log_kpis(self):
-        ''' Return KPIs extracted from log '''
-        if self.kpis is None:
-            self.extract_kpis()
-
-        # Use log KPIs if they exist for this node
-        if "log_" + self.name() in self.kpis:
-            log_kpi = self.kpis["log_" + self.name()]
-        else:
-            log_kpi = {}
-
-        # Make sure we have the errors and warnings counter in the dict
-        if 'total_errors' not in log_kpi:
-            log_kpi['total_errors'] = 0
-        if 'total_warnings' not in log_kpi:
-            log_kpi['total_warnings'] = 0
-        return log_kpi
+    def get_kpi_tree(self):
+        ''' Return all KPI as dict of dict in which the source (e.g. stdout_srsue1) is the key of the first dict '''
+        self.extract_kpis()
+        kpi_tree = {}
+        kpi_tree["log_" + self.name()] = self.log_kpi
+        kpi_tree["csv_" + self.name()] = self.csv_kpi
+        kpi_tree["stdout_" + self.name()] = self.stdout_kpi
+        return kpi_tree
 
     def extract_kpis(self):
         ''' Use the srsLTE KPI analyzer module (part of srsLTE.git) if available to collect KPIs '''
 
+        # Make sure this only runs once
+        if self.csv_kpi is not None or self.log_kpi is not None or self.stdout_kpi is not None:
+            return
+
+        # Start with empty KPIs
+        self.log_kpi = {}
+        self.stdout_kpi = {}
+        self.csv_kpi = {}
+
         # Stop application, copy back logs and process them
         if self.running():
             self.stop()
             self.cleanup()
-
-        self.kpis = {}
         try:
             # Please make sure the srsLTE scripts folder is included in your PYTHONPATH env variable
             from kpi_analyzer import kpi_analyzer
             analyzer = kpi_analyzer(self.name())
             if self.log_file is not None:
-                self.kpis["log_" + self.name()] = analyzer.get_kpi_from_logfile(self.log_file)
+                self.log_kpi = analyzer.get_kpi_from_logfile(self.log_file)
             if self.process.get_output_file('stdout') is not None:
-                self.kpis["stdout_" + self.name()] = analyzer.get_kpi_from_stdout(self.process.get_output_file('stdout'))
+                self.stdout_kpi = analyzer.get_kpi_from_stdout(self.process.get_output_file('stdout'))
             if self.metrics_file is not None:
-                self.kpis["csv_" + self.name()] = analyzer.get_kpi_from_csv(self.metrics_file)
+                self.csv_kpi = analyzer.get_kpi_from_csv(self.metrics_file)
+            # PHY errors for either UE or eNB components from parsed KPI vector as extra entry in dict
+            self.log_kpi["num_phy_errors"] = analyzer.get_num_phy_errors(self.log_kpi)
         except ImportError:
-            self.log("Can't load KPI analyzer module.")
-            self.kpis = {}
-
-        return self.kpis
-
-    def get_num_phy_errors(self, kpi):
-        """ Use KPI analyzer to calculate the number PHY errors for either UE or eNB components from parsed KPI vector """
-        try:
-            # Same as above, make sure the srsLTE scripts folder is included in your PYTHONPATH env variable
-            from kpi_analyzer import kpi_analyzer
-            analyzer = kpi_analyzer(self.name())
-            return analyzer.get_num_phy_errors(kpi)
-        except ImportError:
-            self.log("Can't load KPI analyzer module.")
-            return 0
+            self.log("Can't load KPI analyzer module.")
\ No newline at end of file