#!/usr/bin/env python # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import os import os.path import re import base64 import argparse from xml.dom import Node from xml.dom.minidom import parseString from mako.template import Template report_dir = os.path.dirname(os.path.realpath(__file__)) one_file = False class TemplateRenderer(): def __init__(self, template): self.__template = template def render(self, file_name = None): if file_name is None: file_name = self.__template if not file_name.startswith('/'): current_dir = os.path.dirname(os.path.realpath(__file__)) file_name = os.path.join(current_dir, file_name) return Template(filename = file_name).render(this = self) def render_link(self, link_name, file_name): if one_file: return 'data:text/html;charset=utf-8;base64,' + \ base64.b64encode(self.render(file_name)) else: part = self.render(file_name) with open(report_dir + '/' + link_name, 'w') as f: f.write(part) return link_name def render_files(self): report = self.render() with open(report_dir + '/report.html', 'w') as f: f.write(report) class TestCase(TemplateRenderer): def __init__(self, element): TemplateRenderer.__init__(self, 'templates/TestCase.html') self.__class_name = element.getAttribute('classname') self.__name = element.getAttribute('name') self.__time = float(element.getAttribute('time')) self.__failure = False self.__error = False self.__log = None for child in element.childNodes: if child.nodeType == Node.ELEMENT_NODE and child.tagName == 'failure': self.__failure = True self.__log = child.firstChild.nodeValue elif child.nodeType == Node.ELEMENT_NODE and child.tagName == 'error': self.__error = True self.__log = child.firstChild.nodeValue def success(self): return not (self.failure() or self.error()) def failure(self): return self.__failure def error(self): return self.__error def get_log(self): return self.__log def get_name(self): return self.__name def get_time(self): return self.__time class TestSuite(TemplateRenderer): def __init__(self, text): TemplateRenderer.__init__(self, 'templates/TestSuite.html') self.properties = {} self.test_cases = [] xml = parseString(text) self.__populate_properties(xml) self.__populate_test_cases(xml) top = xml.getElementsByTagName('testsuite')[0] self.__errors = int(top.getAttribute('errors')) self.__failures = int(top.getAttribute('failures')) self.__tests = int(top.getAttribute('tests')) self.__host_name = top.getAttribute('hostname') dist_dir = self.properties['dist.dir'] build_number = re.findall(self.__host_name + '-([0-9]+)$', dist_dir) if build_number: # Multiple builds per host. self.__host_name += '-' + build_number[0] self.__name = top.getAttribute('name').split('.')[-1] self.__time = float(top.getAttribute('time')) def __populate_properties(self, xml): properties = xml.getElementsByTagName('property') for prop in properties: self.properties[prop.getAttribute('name')] = prop.getAttribute('value') def __populate_test_cases(self, xml): test_cases = xml.getElementsByTagName('testcase') for test in test_cases: self.test_cases.append(TestCase(test)) def tests(self): return self.__tests def failures(self): return self.__failures def errors(self): return self.__errors def passes(self): return self.tests() - self.failures() - self.errors() def time(self): return self.__time def host_name(self): return self.__host_name def name(self): return self.__name def label(self): return self.host_name() + '-' + self.name() class TestRun(TemplateRenderer): def __init__(self, pwd): TemplateRenderer.__init__(self, 'templates/TestRun.html') self.test_suites = [] files = os.listdir(pwd) pattern = re.compile('^TEST-.*\.xml$') for f in files: if pattern.search(f) is not None: with open(os.path.join(pwd, f)) as handle: self.test_suites.append(TestSuite(handle.read())) def passes(self): return reduce(lambda acc, x: acc + x.passes(), self.test_suites, 0) def failures(self): return reduce(lambda acc, x: acc + x.failures(), self.test_suites, 0) def errors(self): return reduce(lambda acc, x: acc + x.errors(), self.test_suites, 0) def tests(self): return reduce(lambda acc, x: acc + x.tests(), self.test_suites, 0) def time(self): return reduce(lambda acc, x: acc + x.time(), self.test_suites, 0) def success_rate(self): if self.tests(): return 100.0 * self.passes() / self.tests() else: return 100.0 def make_report(args): global report_dir, one_file report_dir = args.report_dir one_file = args.one_file test_run = TestRun(args.log_dir) test_run.render_files() print('Summary:') print(' tests run:', test_run.tests()) print(' failures:', test_run.failures()) print(' errors:', test_run.errors()) failed_results = {} files = os.listdir(args.log_dir) pattern = re.compile('^([^-]+-[^-]+)-(.*)\.fail$') for f in files: match = pattern.findall(f) if match: (host, test, ) = match[0] if host not in failed_results: failed_results[host] = [] failed_results[host].append(test) if failed_results: print() print('Some tests faled to produce a log and are not included in the report:') for host in failed_results: print(' {0}:'.format(host)) for test in failed_results[host]: print(' {0}'.format(test)) if __name__ == '__main__': parser = argparse.ArgumentParser( description = 'Create HTML report from JUnit logs.') parser.add_argument(dest = 'log_dir', help = 'Path to directory containing JUnit logs') parser.add_argument(dest = 'report_dir', help = 'Where should the report be generated') parser.add_argument('--one-file', action = 'store_true', dest = 'one_file', help = 'Inline everything and generate only one file') args = parser.parse_args() make_report(args)