Add per-test KPI support

tests can now use 'tenv.test().set_kpis(some_dict)' to set any kind of
data as KPIs, which will be presented in the junit report.

The representation of KPIs in the xml file doesn't follow the junit
format, mainly because it has no support for per-test properties.

Change-Id: I00e976f65a202e82d440bf33708f06c8ce2643e2
diff --git a/src/osmo_gsm_tester/core/report.py b/src/osmo_gsm_tester/core/report.py
index 5014bf5..d2c68c5 100644
--- a/src/osmo_gsm_tester/core/report.py
+++ b/src/osmo_gsm_tester/core/report.py
@@ -53,6 +53,46 @@
         prop.set('name', 'ref:' + key)
         prop.set('value', val)
 
+def dict_to_junit(parent, d):
+    for key, val in d.items():
+        if isinstance(val, dict):
+            node = et.SubElement(parent, 'kpi_node')
+            node.set('name', key)
+            dict_to_junit(node, val)
+            continue
+        if isinstance(val, (tuple, list)):
+            node = et.SubElement(parent, 'kpi_node')
+            node.set('name', key)
+            list_to_junit(node, val)
+            continue
+        # scalar:
+        node = et.SubElement(parent, 'property')
+        node.set('name', key)
+        node.set('value', str(val))
+
+def list_to_junit(parent, li):
+    for i in range(len(li)):
+        if isinstance(li[i], dict):
+            node = et.SubElement(parent, 'kpi_node')
+            node.set('name', str(i))
+            dict_to_junit(node, li[i])
+            continue
+        if isinstance(val, (tuple, list)):
+            node = et.SubElement(parent, 'kpi_node')
+            node.set('name', str(i))
+            list_to_junit(node, li[i])
+            continue
+        # scalar:
+        node = et.SubElement(parent, 'property')
+        node.set('name', str(i))
+        node.set('value', str(li[i]))
+
+def kpis_to_junit(parent, kpis):
+    if not kpis:
+        return
+    assert isinstance(kpis, dict)
+    knode = et.SubElement(parent, 'kpis')
+    dict_to_junit(knode, kpis)
 
 def trial_to_junit_write(trial, junit_path):
     elements = et.ElementTree(element=trial_to_junit(trial))
@@ -118,6 +158,7 @@
     elif t.status != test.Test.PASS:
         error = et.SubElement(testcase, 'error')
         error.text = 'could not run'
+    kpis_to_junit(testcase, t.kpis())
     sout = et.SubElement(testcase, 'system-out')
     sout.text = escape_xml_invalid_characters(t.report_stdout())
     return testcase