text: #!/usr/bin/env python
  
  from __future__ import print_function
  import collections
  import re
  import os.path
  import sys
  from xml.dom.minidom import parse
  
  if sys.version_info > (3,):
      long = int
      def cmp(a, b): return (a>b)-(a<b)
  
  class TestInfo(object):
  
      def __init__(self, xmlnode):
          self.fixture = xmlnode.getAttribute("classname")
          self.name = xmlnode.getAttribute("name")
          self.value_param = xmlnode.getAttribute("value_param")
          self.type_param = xmlnode.getAttribute("type_param")
  
          custom_status = xmlnode.getAttribute("custom_status")
          failures = xmlnode.getElementsByTagName("failure")
  
          if len(custom_status) > 0:
              self.status = custom_status
          elif len(failures) > 0:
              self.status = "failed"
          else:
              self.status = xmlnode.getAttribute("status")
  
          if self.name.startswith("DISABLED_"):
              if self.status == 'notrun':
                  self.status = "disabled"
              self.fixture = self.fixture.replace("DISABLED_", "")
              self.name = self.name.replace("DISABLED_", "")
          self.properties = {
              prop.getAttribute("name") : prop.getAttribute("value")
              for prop in xmlnode.getElementsByTagName("property")
              if prop.hasAttribute("name") and prop.hasAttribute("value")
          }
          self.metrix = {}
          self.parseLongMetric(xmlnode, "bytesIn");
          self.parseLongMetric(xmlnode, "bytesOut");
          self.parseIntMetric(xmlnode, "samples");
          self.parseIntMetric(xmlnode, "outliers");
          self.parseFloatMetric(xmlnode, "frequency", 1);
          self.parseLongMetric(xmlnode, "min");
          self.parseLongMetric(xmlnode, "median");
          self.parseLongMetric(xmlnode, "gmean");
          self.parseLongMetric(xmlnode, "mean");
          self.parseLongMetric(xmlnode, "stddev");
          self.parseFloatMetric(xmlnode, "gstddev");
          self.parseFloatMetric(xmlnode, "time");
          self.parseLongMetric(xmlnode, "total_memory_usage");
  
      def parseLongMetric(self, xmlnode, name, default = 0):
          if name in self.properties:
              self.metrix[name] = long(self.properties[name])
          elif xmlnode.hasAttribute(name):
              self.metrix[name] = long(xmlnode.getAttribute(name))
          else:
              self.metrix[name] = default
  
      def parseIntMetric(self, xmlnode, name, default = 0):
          if name in self.properties:
              self.metrix[name] = int(self.properties[name])
          elif xmlnode.hasAttribute(name):
              self.metrix[name] = int(xmlnode.getAttribute(name))
          else:
              self.metrix[name] = default
  
      def parseFloatMetric(self, xmlnode, name, default = 0):
          if name in self.properties:
              self.metrix[name] = float(self.properties[name])
          elif xmlnode.hasAttribute(name):
              self.metrix[name] = float(xmlnode.getAttribute(name))
          else:
              self.metrix[name] = default
  
      def parseStringMetric(self, xmlnode, name, default = None):
          if name in self.properties:
              self.metrix[name] = self.properties[name].strip()
          elif xmlnode.hasAttribute(name):
              self.metrix[name] = xmlnode.getAttribute(name).strip()
          else:
              self.metrix[name] = default
  
      def get(self, name, units="ms"):
          if name == "classname":
              return self.fixture
          if name == "name":
              return self.name
          if name == "fullname":
              return self.__str__()
          if name == "value_param":
              return self.value_param
          if name == "type_param":
              return self.type_param
          if name == "status":
              return self.status
          val = self.metrix.get(name, None)
          if not val:
              return val
          if name == "time":
              return self.metrix.get("time")
          if name in ["gmean", "min", "mean", "median", "stddev"]:
              scale = 1.0
              frequency = self.metrix.get("frequency", 1.0) or 1.0
              if units == "ms":
                  scale = 1000.0
              if units == "us" or units == "mks":  # mks is typo error for microsecond (<= OpenCV 3.4)
                  scale = 1000000.0
              if units == "ns":
                  scale = 1000000000.0
              if units == "ticks":
                  frequency = long(1)
                  scale = long(1)
              return val * scale / frequency
          return val
  
  
      def dump(self, units="ms"):
          print("%s ->\t\033[1;31m%s\033[0m = \t%.2f%s" % (str(self), self.status, self.get("gmean", units), units))
  
  
      def getName(self):
          pos = self.name.find("/")
          if pos > 0:
              return self.name[:pos]
          return self.name
  
  
      def getFixture(self):
          if self.fixture.endswith(self.getName()):
              fixture = self.fixture[:-len(self.getName())]
          else:
              fixture = self.fixture
          if fixture.endswith("_"):
              fixture = fixture[:-1]
          return fixture
  
  
      def param(self):
          return '::'.join(filter(None, [self.type_param, self.value_param]))
  
      def shortName(self):
          name = self.getName()
          fixture = self.getFixture()
          return '::'.join(filter(None, [name, fixture]))
  
  
      def __str__(self):
          name = self.getName()
          fixture = self.getFixture()
          return '::'.join(filter(None, [name, fixture, self.type_param, self.value_param]))
  
  
      def __cmp__(self, other):
          r = cmp(self.fixture, other.fixture);
          if r != 0:
              return r
          if self.type_param:
              if other.type_param:
                  r = cmp(self.type_param, other.type_param);
                  if r != 0:
                       return r
              else:
                  return -1
          else:
              if other.type_param:
                  return 1
          if self.value_param:
              if other.value_param:
                  r = cmp(self.value_param, other.value_param);
                  if r != 0:
                       return r
              else:
                  return -1
          else:
              if other.value_param:
                  return 1
          return 0
  
  # This is a Sequence for compatibility with old scripts,
  # which treat parseLogFile's return value as a list.
  class TestRunInfo(collections.Sequence):
      def __init__(self, properties, tests):
          self.properties = properties
          self.tests = tests
  
      def __len__(self):
          return len(self.tests)
  
      def __getitem__(self, key):
          return self.tests[key]
  
  def parseLogFile(filename):
      log = parse(filename)
  
      properties = {
          attr_name[3:]: attr_value
          for (attr_name, attr_value) in log.documentElement.attributes.items()
          if attr_name.startswith('cv_')
      }
  
      tests = list(map(TestInfo, log.getElementsByTagName("testcase")))
  
      return TestRunInfo(properties, tests)
  
  
  if __name__ == "__main__":
      if len(sys.argv) < 2:
          print("Usage:\n", os.path.basename(sys.argv[0]), "<log_name>.xml")
          exit(0)
  
      for arg in sys.argv[1:]:
          print("Processing {}...".format(arg))
  
          run = parseLogFile(arg)
  
          print("Properties:")
  
          for (prop_name, prop_value) in run.properties.items():
            print("\t{} = {}".format(prop_name, prop_value))
  
          print("Tests:")
  
          for t in sorted(run.tests):
              t.dump()
  
          print()
  