gitlab CI: add a JUnit XML report for scan-build

Use a scan-build wrapper to generate plist files, then parse those into a
JUnit xml format. This makes the errors appear on the main MR page as opposed
to being hidden in the artifacts somewhere.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
This commit is contained in:
Peter Hutterer 2021-05-11 14:26:27 +10:00
parent b1c9667aca
commit 5dc000323d
4 changed files with 145 additions and 12 deletions

View file

@ -724,16 +724,16 @@ scan-build@fedora:34:
extends:
- .fedora-build@template
variables:
NINJA_ARGS: scan-build
NINJA_ARGS: ''
MESON_TEST_ARGS: ''
before_script:
- dnf install -y clang-analyzer findutils
- dnf install -y clang-analyzer
script:
- .gitlab-ci/meson-build.sh
- test ! -d "$MESON_BUILDDIR"/meson-logs/scanbuild && exit 0
- test $(find "$MESON_BUILDDIR"/meson-logs/scanbuild -maxdepth 0 ! -empty -exec echo "not empty" \; | wc -l) -eq 0 && exit 0
- echo "Check scan-build results"
- /bin/false
- export SCANBUILD="$PWD/.gitlab-ci/scanbuild-wrapper.sh"
- ninja -C "$MESON_BUILDDIR" scan-build
after_script:
- .gitlab-ci/scanbuild-plist-to-junit.py "$MESON_BUILDDIR"/meson-logs/scanbuild/ > "$MESON_BUILDDIR"/junit-scan-build.xml
# Below jobs are build option combinations. We only
# run them on one image, they shouldn't fail on one distro

View file

@ -454,16 +454,16 @@ scan-build@{{distro.name}}:{{version}}:
extends:
- .{{distro.name}}-build@template
variables:
NINJA_ARGS: scan-build
NINJA_ARGS: ''
MESON_TEST_ARGS: ''
before_script:
- dnf install -y clang-analyzer findutils
- dnf install -y clang-analyzer
script:
- .gitlab-ci/meson-build.sh
- test ! -d "$MESON_BUILDDIR"/meson-logs/scanbuild && exit 0
- test $(find "$MESON_BUILDDIR"/meson-logs/scanbuild -maxdepth 0 ! -empty -exec echo "not empty" \; | wc -l) -eq 0 && exit 0
- echo "Check scan-build results"
- /bin/false
- export SCANBUILD="$PWD/.gitlab-ci/scanbuild-wrapper.sh"
- ninja -C "$MESON_BUILDDIR" scan-build
after_script:
- .gitlab-ci/scanbuild-plist-to-junit.py "$MESON_BUILDDIR"/meson-logs/scanbuild/ > "$MESON_BUILDDIR"/junit-scan-build.xml
# Below jobs are build option combinations. We only
# run them on one image, they shouldn't fail on one distro

View file

@ -0,0 +1,131 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: MIT
#
# Usage:
# $ scanbuild-plist-to-junit.py /path/to/meson-logs/scanbuild/ > junit-report.xml
#
# Converts the plist output from scan-build into a JUnit-compatible XML.
#
# For use with meson, use a wrapper script with this content:
# scan-build -v --status-bugs -plist-html "$@"
# then build with
# SCANBUILD="/abs/path/to/wrapper.sh" ninja -C builddir scan-build
#
# For file context, $PWD has to be the root source directory.
#
# Note that the XML format is tailored towards being useful in the gitlab
# CI, the JUnit format supports more features.
#
# This file is formatted with Python Black
import argparse
import plistlib
import re
import sys
from pathlib import Path
errors = []
class Error(object):
pass
parser = argparse.ArgumentParser(
description="This tool convers scan-build's plist format to JUnit XML"
)
parser.add_argument(
"directory", help="Path to a scan-build output directory", type=Path
)
args = parser.parse_args()
if not args.directory.exists():
print(f"Invalid directory: {args.directory}", file=sys.stderr)
sys.exit(1)
# Meson places scan-build runs into a timestamped directory. To make it
# easier to invoke this script, we just glob everything on the assumption
# that there's only one scanbuild/$timestamp/ directory anyway.
for file in Path(args.directory).glob("**/*.plist"):
with open(file, "rb") as fd:
plist = plistlib.load(fd, fmt=plistlib.FMT_XML)
try:
sources = plist["files"]
for elem in plist["diagnostics"]:
e = Error()
e.type = elem["type"] # Human-readable error type
e.description = elem["description"] # Longer description
e.func = elem["issue_context"] # function name
e.lineno = elem["location"]["line"]
filename = sources[elem["location"]["file"]]
# Remove the ../../../ prefix from the file
e.file = re.sub(r"^(\.\./)*", "", filename)
errors.append(e)
except KeyError:
print(
"Failed to access plist content, incompatible format?", file=sys.stderr
)
sys.exit(1)
# Add a few lines of context for each error that we can print in the xml
# output. Note that e.lineno is 1-indexed.
#
# If one of the files fail, we stop doing this, we're probably in the wrong
# directory.
try:
current_file = None
lines = []
for e in sorted(errors, key=lambda x: x.file):
if current_file != e.file:
current_file = e.file
lines = open(current_file).readlines()
# e.lineno is 1-indexed, lineno is our 0-indexed line number
lineno = e.lineno - 1
start = max(0, lineno - 4)
end = min(len(lines), lineno + 5) # end is exclusive
e.context = [
f"{'>' if line == e.lineno else ' '} {line}: {content}"
for line, content in zip(range(start + 1, end), lines[start:end])
]
except FileNotFoundError:
pass
print('<?xml version="1.0" encoding="utf-8"?>')
print("<testsuites>")
if errors:
suites = sorted(set([s.type for s in errors]))
# Use a counter to ensure test names are unique, otherwise the CI
# display ignores duplicates.
counter = 0
for suite in suites:
errs = [e for e in errors if e.type == suite]
# Note: the grouping by suites doesn't actually do anything in gitlab. Oh well
print(f'<testsuite name="{suite}" failures="{len(errs)}" tests="{len(errs)}">')
for error in errs:
print(
f"""\
<testcase name="{counter}. {error.type} - {error.file}:{error.lineno}" classname="{error.file}">
<failure message="{error.description}">
<![CDATA[
In function {error.func}(),
{error.description}
{error.file}:{error.lineno}
---
{"".join(error.context)}
]]>
</failure>
</testcase>"""
)
counter += 1
print("</testsuite>")
else:
# In case of success, add one test case so that registers in the UI
# properly
print('<testsuite name="scanbuild" failures="0" tests="1">')
print('<testcase name="scanbuild" classname="scanbuild"/>')
print("</testsuite>")
print("</testsuites>")

View file

@ -0,0 +1,2 @@
#!/bin/sh
scan-build -v --status-bugs -plist-html "$@"