Home | History | Annotate | Line # | Download | only in tests
      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