summaryrefslogtreecommitdiffstats
path: root/bin/tests/convert-trs-to-junit.py
blob: 85b37dd62936816ecb0c8fc5a869109ebd649cb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python
#
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# Convert automake .trs files into JUnit format suitable for Gitlab

import argparse
import os
import sys
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from xml.etree.ElementTree import SubElement


# getting explicit encoding specification right for Python 2/3 would be messy,
# so let's hope for the best
def read_whole_text(filename):
    with open(filename) as inf:  # pylint: disable-msg=unspecified-encoding
        return inf.read().strip()


def read_trs_result(filename):
    result = None
    with open(filename, "r") as trs:  # pylint: disable-msg=unspecified-encoding
        for line in trs:
            items = line.split()
            if len(items) < 2:
                raise ValueError("unsupported line in trs file", filename, line)
            if items[0] != (":test-result:"):
                continue
            if result is not None:
                raise NotImplementedError("double :test-result:", filename)
            result = items[1].upper()

    if result is None:
        raise ValueError(":test-result: not found", filename)

    return result


def find_test_relative_path(source_dir, in_path):
    """Return {in_path}.c if it exists, with fallback to {in_path}"""
    candidates_relative = [in_path + ".c", in_path]
    for relative in candidates_relative:
        absolute = os.path.join(source_dir, relative)
        if os.path.exists(absolute):
            return relative
    raise KeyError


def err_out(exception):
    raise exception


def walk_trss(source_dir):
    for cur_dir, _dirs, files in os.walk(source_dir, onerror=err_out):
        for filename in files:
            if not filename.endswith(".trs"):
                continue

            filename_prefix = filename[: -len(".trs")]
            log_name = filename_prefix + ".log"
            full_trs_path = os.path.join(cur_dir, filename)
            full_log_path = os.path.join(cur_dir, log_name)
            sub_dir = os.path.relpath(cur_dir, source_dir)
            test_name = os.path.join(sub_dir, filename_prefix)

            t = {
                "name": test_name,
                "full_log_path": full_log_path,
                "rel_log_path": os.path.relpath(full_log_path, source_dir),
            }
            t["result"] = read_trs_result(full_trs_path)

            # try to find dir/file path for a clickable link
            try:
                t["rel_file_path"] = find_test_relative_path(source_dir, test_name)
            except KeyError:
                pass  # no existing path found

            yield t


def append_testcase(testsuite, t):
    # attributes taken from
    # https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/ci/parsers/test/junit.rb
    attrs = {"name": t["name"]}
    if "rel_file_path" in t:
        attrs["file"] = t["rel_file_path"]

    testcase = SubElement(testsuite, "testcase", attrs)

    # Gitlab accepts only [[ATTACHMENT| links for system-out, not raw text
    s = SubElement(testcase, "system-out")
    s.text = "[[ATTACHMENT|" + t["rel_log_path"] + "]]"
    if t["result"].lower() == "pass":
        return

    # Gitlab shows output only for failed or skipped tests
    if t["result"].lower() == "skip":
        err = SubElement(testcase, "skipped")
    else:
        err = SubElement(testcase, "failure")
    err.text = read_whole_text(t["full_log_path"])


def gen_junit(results):
    testsuites = Element("testsuites")
    testsuite = SubElement(testsuites, "testsuite")
    for test in results:
        append_testcase(testsuite, test)
    return testsuites


def check_directory(path):
    try:
        os.listdir(path)
        return path
    except OSError as ex:
        msg = "Path {} cannot be listed as a directory: {}".format(path, ex)
        raise argparse.ArgumentTypeError(msg)


def main():
    parser = argparse.ArgumentParser(
        description="Recursively search for .trs + .log files and compile "
        "them into JUnit XML suitable for Gitlab. Paths in the "
        "XML are relative to the specified top directory."
    )
    parser.add_argument(
        "top_directory",
        type=check_directory,
        help="root directory where to start scanning for .trs files",
    )
    args = parser.parse_args()
    junit = gen_junit(walk_trss(args.top_directory))

    # encode results into file format, on Python 3 it produces bytes
    xml = ElementTree.tostring(junit, "utf-8")
    # use stdout as a binary file object, Python2/3 compatibility
    output = getattr(sys.stdout, "buffer", sys.stdout)
    output.write(xml)


if __name__ == "__main__":
    main()