Changeset View
Changeset View
Standalone View
Standalone View
cmake/utils/junit-reports-merge.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2020 The Bitcoin developers | # Copyright (c) 2020 The Bitcoin developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
import datetime | import datetime | ||||
import fcntl | import fcntl | ||||
import os | import os | ||||
import sys | import sys | ||||
import xml.etree.ElementTree as ET | import xml.etree.ElementTree as ET | ||||
class TestSuite: | class TestSuite: | ||||
def __init__(self, name, report_dir): | def __init__(self, name, report_dir): | ||||
self.name = name | self.name = name | ||||
self.test_cases = {} | self.test_cases = {} | ||||
self.report_file = os.path.join(report_dir, f'{self.name}.xml') | self.report_file = os.path.join(report_dir, f"{self.name}.xml") | ||||
def add_test_case(self, test_case): | def add_test_case(self, test_case): | ||||
self.test_cases[test_case.test_id] = test_case | self.test_cases[test_case.test_id] = test_case | ||||
def get_failed_tests(self): | def get_failed_tests(self): | ||||
return [t for t in self.test_cases.values() if not t.test_success] | return [t for t in self.test_cases.values() if not t.test_success] | ||||
def dump(self): | def dump(self): | ||||
# Calculate test suite duration as the sum of all test case duraration | # Calculate test suite duration as the sum of all test case duraration | ||||
duration = round(sum([ | duration = round( | ||||
float(t.node.get('time', 0.0)) for t in self.test_cases.values() | sum([float(t.node.get("time", 0.0)) for t in self.test_cases.values()]), 3 | ||||
]), 3) | ) | ||||
test_suite = ET.Element( | test_suite = ET.Element( | ||||
'testsuite', | "testsuite", | ||||
{ | { | ||||
'name': self.name, | "name": self.name, | ||||
'id': '0', | "id": "0", | ||||
'timestamp': datetime.datetime.now().isoformat('T'), | "timestamp": datetime.datetime.now().isoformat("T"), | ||||
'time': str(duration), | "time": str(duration), | ||||
'tests': str(len(self.test_cases)), | "tests": str(len(self.test_cases)), | ||||
'failures': str(len(self.get_failed_tests())), | "failures": str(len(self.get_failed_tests())), | ||||
} | }, | ||||
) | ) | ||||
for test_case in self.test_cases.values(): | for test_case in self.test_cases.values(): | ||||
test_suite.append(test_case.node) | test_suite.append(test_case.node) | ||||
report_dir = os.path.dirname(self.report_file) | report_dir = os.path.dirname(self.report_file) | ||||
os.makedirs(report_dir, exist_ok=True) | os.makedirs(report_dir, exist_ok=True) | ||||
ET.ElementTree(test_suite).write( | ET.ElementTree(test_suite).write( | ||||
self.report_file, | self.report_file, | ||||
'UTF-8', | "UTF-8", | ||||
xml_declaration=True, | xml_declaration=True, | ||||
) | ) | ||||
def load(self): | def load(self): | ||||
tree = ET.parse(self.report_file) | tree = ET.parse(self.report_file) | ||||
xml_root = tree.getroot() | xml_root = tree.getroot() | ||||
assert xml_root.tag == 'testsuite' | assert xml_root.tag == "testsuite" | ||||
assert self.name == xml_root.get('name') | assert self.name == xml_root.get("name") | ||||
for test_case in xml_root.findall('testcase'): | for test_case in xml_root.findall("testcase"): | ||||
self.add_test_case(TestCase(test_case)) | self.add_test_case(TestCase(test_case)) | ||||
class TestCase: | class TestCase: | ||||
def __init__(self, node): | def __init__(self, node): | ||||
self.node = node | self.node = node | ||||
self.test_success = self.node.find('failure') is None | self.test_success = self.node.find("failure") is None | ||||
def __getattr__(self, attribute): | def __getattr__(self, attribute): | ||||
if attribute == 'test_id': | if attribute == "test_id": | ||||
return f"{self.classname}/{self.name}" | return f"{self.classname}/{self.name}" | ||||
return self.node.attrib[attribute] | return self.node.attrib[attribute] | ||||
class Lock: | class Lock: | ||||
def __init__(self, suite, lock_dir): | def __init__(self, suite, lock_dir): | ||||
self.lock_file = os.path.join(lock_dir, f'{suite}.lock') | self.lock_file = os.path.join(lock_dir, f"{suite}.lock") | ||||
def __enter__(self): | def __enter__(self): | ||||
os.makedirs(os.path.dirname(self.lock_file), exist_ok=True) | os.makedirs(os.path.dirname(self.lock_file), exist_ok=True) | ||||
self.fd = open(self.lock_file, 'w', encoding='utf-8') | self.fd = open(self.lock_file, "w", encoding="utf-8") | ||||
fcntl.lockf(self.fd, fcntl.LOCK_EX) | fcntl.lockf(self.fd, fcntl.LOCK_EX) | ||||
def __exit__(self, exception_type, exception_value, traceback): | def __exit__(self, exception_type, exception_value, traceback): | ||||
fcntl.lockf(self.fd, fcntl.LOCK_UN) | fcntl.lockf(self.fd, fcntl.LOCK_UN) | ||||
self.fd.close() | self.fd.close() | ||||
def main(report_dir, lock_dir, suite, test): | def main(report_dir, lock_dir, suite, test): | ||||
junit = f'{suite}-{test}.xml' | junit = f"{suite}-{test}.xml" | ||||
if not os.path.isfile(junit): | if not os.path.isfile(junit): | ||||
return 0 | return 0 | ||||
tree = ET.parse(junit) | tree = ET.parse(junit) | ||||
# Junit root can be a single test suite or multiple test suites. The | # Junit root can be a single test suite or multiple test suites. The | ||||
# later case is unsupported. | # later case is unsupported. | ||||
xml_root = tree.getroot() | xml_root = tree.getroot() | ||||
if xml_root.tag != 'testsuite': | if xml_root.tag != "testsuite": | ||||
raise AssertionError( | raise AssertionError("The parser only supports a single test suite per report") | ||||
"The parser only supports a single test suite per report") | |||||
test_suite_name = xml_root.get('name') | test_suite_name = xml_root.get("name") | ||||
lock = Lock(suite, lock_dir) | lock = Lock(suite, lock_dir) | ||||
with lock: | with lock: | ||||
test_suite = TestSuite(test_suite_name, report_dir) | test_suite = TestSuite(test_suite_name, report_dir) | ||||
if os.path.isfile(test_suite.report_file): | if os.path.isfile(test_suite.report_file): | ||||
test_suite.load() | test_suite.load() | ||||
for child in xml_root: | for child in xml_root: | ||||
if child.tag != 'testcase' or (child.find('skipped') is not None): | if child.tag != "testcase" or (child.find("skipped") is not None): | ||||
continue | continue | ||||
test_suite.add_test_case(TestCase(child)) | test_suite.add_test_case(TestCase(child)) | ||||
test_suite.dump() | test_suite.dump() | ||||
sys.exit( | sys.exit( | ||||
1 if test in [case.classname for case in test_suite.get_failed_tests()] | 1 if test in [case.classname for case in test_suite.get_failed_tests()] else 0 | ||||
else 0 | |||||
) | ) | ||||
main( | main( | ||||
report_dir=sys.argv[1], | report_dir=sys.argv[1], | ||||
lock_dir=sys.argv[2], | lock_dir=sys.argv[2], | ||||
suite=sys.argv[3], | suite=sys.argv[3], | ||||
test=sys.argv[4], | test=sys.argv[4], | ||||
) | ) |