1 #!/usr/bin/env python3 2 # 3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC") 4 # 5 # SPDX-License-Identifier: MPL-2.0 6 # 7 # Convert automake .trs files into JUnit format suitable for Gitlab 8 9 from xml.etree import ElementTree 10 from xml.etree.ElementTree import Element, SubElement 11 12 import argparse 13 import os 14 import sys 15 16 17 def read_whole_text(filename): 18 with open(filename, encoding="utf-8") as inf: 19 return inf.read().strip() 20 21 22 def read_trs_result(filename): 23 result = None 24 with open(filename, "r", encoding="utf-8") as trs: 25 for line in trs: 26 items = line.split() 27 if len(items) < 2: 28 raise ValueError("unsupported line in trs file", filename, line) 29 if items[0] != (":global-test-result:"): 30 continue 31 if result is not None: 32 raise NotImplementedError("double :global-test-result:", filename) 33 result = items[1].upper() 34 35 if result is None: 36 raise ValueError(":global-test-result: not found", filename) 37 38 return result 39 40 41 def find_test_relative_path(source_dir, in_path): 42 """Return {in_path}.c if it exists, with fallback to {in_path}""" 43 candidates_relative = [in_path + ".c", in_path] 44 for relative in candidates_relative: 45 absolute = os.path.join(source_dir, relative) 46 if os.path.exists(absolute): 47 return relative 48 raise KeyError 49 50 51 def err_out(exception): 52 raise exception 53 54 55 def walk_trss(source_dir): 56 for cur_dir, _dirs, files in os.walk(source_dir, onerror=err_out): 57 for filename in files: 58 if not filename.endswith(".trs"): 59 continue 60 61 filename_prefix = filename[: -len(".trs")] 62 log_name = filename_prefix + ".log" 63 full_trs_path = os.path.join(cur_dir, filename) 64 full_log_path = os.path.join(cur_dir, log_name) 65 sub_dir = os.path.relpath(cur_dir, source_dir) 66 test_dir_path = os.path.join(sub_dir, filename_prefix) 67 68 if sub_dir.startswith("bin/tests/system"): 69 # Match the `pytest` style test names for system tests 70 test_name = f"test_{filename_prefix}" 71 else: 72 test_name = test_dir_path 73 74 t = { 75 "name": test_name, 76 "full_log_path": full_log_path, 77 "rel_log_path": os.path.relpath(full_log_path, source_dir), 78 } 79 t["result"] = read_trs_result(full_trs_path) 80 81 # try to find dir/file path for a clickable link 82 try: 83 t["rel_file_path"] = find_test_relative_path(source_dir, test_dir_path) 84 except KeyError: 85 pass # no existing path found 86 87 yield t 88 89 90 def append_testcase(testsuite, t): 91 # attributes taken from 92 # https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/ci/parsers/test/junit.rb 93 attrs = {"name": t["name"]} 94 if "rel_file_path" in t: 95 attrs["file"] = t["rel_file_path"] 96 97 testcase = SubElement(testsuite, "testcase", attrs) 98 99 # Gitlab accepts only [[ATTACHMENT| links for system-out, not raw text 100 s = SubElement(testcase, "system-out") 101 s.text = "[[ATTACHMENT|" + t["rel_log_path"] + "]]" 102 if t["result"].lower() == "pass": 103 return 104 105 # Gitlab shows output only for failed or skipped tests 106 if t["result"].lower() == "skip": 107 err = SubElement(testcase, "skipped") 108 else: 109 err = SubElement(testcase, "failure") 110 err.text = read_whole_text(t["full_log_path"]) 111 112 113 def gen_junit(results): 114 testsuites = Element("testsuites") 115 testsuite = SubElement(testsuites, "testsuite") 116 for test in results: 117 append_testcase(testsuite, test) 118 return testsuites 119 120 121 def check_directory(path): 122 try: 123 os.listdir(path) 124 return path 125 except OSError as ex: 126 msg = f"Path {path} cannot be listed as a directory: {ex}" 127 raise argparse.ArgumentTypeError(msg) 128 129 130 def main(): 131 parser = argparse.ArgumentParser( 132 description="Recursively search for .trs + .log files and compile " 133 "them into JUnit XML suitable for Gitlab. Paths in the " 134 "XML are relative to the specified top directory." 135 ) 136 parser.add_argument( 137 "top_directory", 138 type=check_directory, 139 help="root directory where to start scanning for .trs files", 140 ) 141 args = parser.parse_args() 142 junit = gen_junit(walk_trss(args.top_directory)) 143 144 # encode results into file format, on Python 3 it produces bytes 145 xml = ElementTree.tostring(junit, "utf-8") 146 # use stdout as a binary file object, Python2/3 compatibility 147 output = getattr(sys.stdout, "buffer", sys.stdout) 148 output.write(xml) 149 150 151 if __name__ == "__main__": 152 main() 153