From 5dc000323d27c175182597ea45149218e361dc83 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 11 May 2021 14:26:27 +1000 Subject: [PATCH] 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 --- .gitlab-ci.yml | 12 +-- .gitlab-ci/ci.template | 12 +-- .gitlab-ci/scanbuild-plist-to-junit.py | 131 +++++++++++++++++++++++++++++++++ .gitlab-ci/scanbuild-wrapper.sh | 2 + 4 files changed, 145 insertions(+), 12 deletions(-) create mode 100755 .gitlab-ci/scanbuild-plist-to-junit.py create mode 100755 .gitlab-ci/scanbuild-wrapper.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b29a8ab..7665a01 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/.gitlab-ci/ci.template b/.gitlab-ci/ci.template index 9970c02..8c2fe1d 100644 --- a/.gitlab-ci/ci.template +++ b/.gitlab-ci/ci.template @@ -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 diff --git a/.gitlab-ci/scanbuild-plist-to-junit.py b/.gitlab-ci/scanbuild-plist-to-junit.py new file mode 100755 index 0000000..987148a --- /dev/null +++ b/.gitlab-ci/scanbuild-plist-to-junit.py @@ -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('') +print("") +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'') + for error in errs: + print( + f"""\ + + + + +""" + ) + counter += 1 + print("") +else: + # In case of success, add one test case so that registers in the UI + # properly + print('') + print('') + print("") +print("") diff --git a/.gitlab-ci/scanbuild-wrapper.sh b/.gitlab-ci/scanbuild-wrapper.sh new file mode 100755 index 0000000..58243b0 --- /dev/null +++ b/.gitlab-ci/scanbuild-wrapper.sh @@ -0,0 +1,2 @@ +#!/bin/sh +scan-build -v --status-bugs -plist-html "$@" -- 2.7.4