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