libinput 1.22.0
[platform/upstream/libinput.git] / .gitlab-ci / scanbuild-plist-to-junit.py
1 #!/usr/bin/env python3
2 #
3 # SPDX-License-Identifier: MIT
4 #
5 # Usage:
6 #   $ scanbuild-plist-to-junit.py /path/to/meson-logs/scanbuild/ > junit-report.xml
7 #
8 # Converts the plist output from scan-build into a JUnit-compatible XML.
9 #
10 # For use with meson, use a wrapper script with this content:
11 #   scan-build -v --status-bugs -plist-html "$@"
12 # then build with
13 #  SCANBUILD="/abs/path/to/wrapper.sh" ninja -C builddir scan-build
14 #
15 # For file context, $PWD has to be the root source directory.
16 #
17 # Note that the XML format is tailored towards being useful in the gitlab
18 # CI, the JUnit format supports more features.
19 #
20 # This file is formatted with Python Black
21
22 import argparse
23 import plistlib
24 import re
25 import sys
26 from pathlib import Path
27
28 errors = []
29
30
31 class Error(object):
32     pass
33
34
35 parser = argparse.ArgumentParser(
36     description="This tool convers scan-build's plist format to JUnit XML"
37 )
38 parser.add_argument(
39     "directory", help="Path to a scan-build output directory", type=Path
40 )
41 args = parser.parse_args()
42
43 if not args.directory.exists():
44     print(f"Invalid directory: {args.directory}", file=sys.stderr)
45     sys.exit(1)
46
47 # Meson places scan-build runs into a timestamped directory. To make it
48 # easier to invoke this script, we just glob everything on the assumption
49 # that there's only one scanbuild/$timestamp/ directory anyway.
50 for file in Path(args.directory).glob("**/*.plist"):
51     with open(file, "rb") as fd:
52         plist = plistlib.load(fd, fmt=plistlib.FMT_XML)
53         try:
54             sources = plist["files"]
55             for elem in plist["diagnostics"]:
56                 e = Error()
57                 e.type = elem["type"]  # Human-readable error type
58                 e.description = elem["description"]  # Longer description
59                 e.func = elem["issue_context"]  # function name
60                 e.lineno = elem["location"]["line"]
61                 filename = sources[elem["location"]["file"]]
62                 # Remove the ../../../ prefix from the file
63                 e.file = re.sub(r"^(\.\./)*", "", filename)
64                 errors.append(e)
65         except KeyError:
66             print(
67                 "Failed to access plist content, incompatible format?", file=sys.stderr
68             )
69             sys.exit(1)
70
71
72 # Add a few lines of context for each error that we can print in the xml
73 # output. Note that e.lineno is 1-indexed.
74 #
75 # If one of the files fail, we stop doing this, we're probably in the wrong
76 # directory.
77 try:
78     current_file = None
79     lines = []
80     for e in sorted(errors, key=lambda x: x.file):
81         if current_file != e.file:
82             current_file = e.file
83             lines = open(current_file).readlines()
84
85         # e.lineno is 1-indexed, lineno is our 0-indexed line number
86         lineno = e.lineno - 1
87         start = max(0, lineno - 4)
88         end = min(len(lines), lineno + 5)  # end is exclusive
89         e.context = [
90             f"{'>' if line == e.lineno else ' '} {line}: {content}"
91             for line, content in zip(range(start + 1, end), lines[start:end])
92         ]
93 except FileNotFoundError:
94     pass
95
96 print('<?xml version="1.0" encoding="utf-8"?>')
97 print("<testsuites>")
98 if errors:
99     suites = sorted(set([s.type for s in errors]))
100     # Use a counter to ensure test names are unique, otherwise the CI
101     # display ignores duplicates.
102     counter = 0
103     for suite in suites:
104         errs = [e for e in errors if e.type == suite]
105         # Note: the grouping by suites doesn't actually do anything in gitlab. Oh well
106         print(f'<testsuite name="{suite}" failures="{len(errs)}" tests="{len(errs)}">')
107         for error in errs:
108             print(
109                 f"""\
110 <testcase name="{counter}. {error.type} - {error.file}:{error.lineno}" classname="{error.file}">
111 <failure message="{error.description}">
112 <![CDATA[
113 In function {error.func}(),
114 {error.description}
115
116 {error.file}:{error.lineno}
117 ---
118 {"".join(error.context)}
119 ]]>
120 </failure>
121 </testcase>"""
122             )
123             counter += 1
124         print("</testsuite>")
125 else:
126     # In case of success, add one test case so that registers in the UI
127     # properly
128     print('<testsuite name="scanbuild" failures="0" tests="1">')
129     print('<testcase name="scanbuild" classname="scanbuild"/>')
130     print("</testsuite>")
131 print("</testsuites>")