Hendrik Langer
5 years ago
3 changed files with 129 additions and 5 deletions
@ -0,0 +1,119 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
|
||||
|
# https://github.com/PSPDFKit-labs/clang-tidy-to-junit/ |
||||
|
|
||||
|
import sys |
||||
|
import collections |
||||
|
import re |
||||
|
import logging |
||||
|
import itertools |
||||
|
from xml.sax.saxutils import escape |
||||
|
|
||||
|
# Create a `ErrorDescription` tuple with all the information we want to keep. |
||||
|
ErrorDescription = collections.namedtuple( |
||||
|
'ErrorDescription', 'file line column error error_identifier description') |
||||
|
|
||||
|
|
||||
|
class ClangTidyConverter: |
||||
|
# All the errors encountered. |
||||
|
errors = [] |
||||
|
|
||||
|
# Parses the error. |
||||
|
# Group 1: file path |
||||
|
# Group 2: line |
||||
|
# Group 3: column |
||||
|
# Group 4: error message |
||||
|
# Group 5: error identifier |
||||
|
error_regex = re.compile( |
||||
|
r"^([\w\/\.\-\ ]+):(\d+):(\d+): (.+) (\[[\w\-,\.]+\])$") |
||||
|
|
||||
|
# This identifies the main error line (it has a [the-warning-type] at the end) |
||||
|
# We only create a new error when we encounter one of those. |
||||
|
main_error_identifier = re.compile(r'\[[\w\-,\.]+\]$') |
||||
|
|
||||
|
def __init__(self, basename): |
||||
|
self.basename = basename |
||||
|
|
||||
|
def print_junit_file(self, output_file): |
||||
|
# Write the header. |
||||
|
output_file.write("""<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<testsuites id="1" name="Clang-Tidy" tests="{error_count}" errors="{error_count}" failures="0" time="0">""".format(error_count=len(self.errors))) |
||||
|
|
||||
|
sorted_errors = sorted(self.errors, key=lambda x: x.file) |
||||
|
|
||||
|
# Iterate through the errors, grouped by file. |
||||
|
for file, errorIterator in itertools.groupby(sorted_errors, key=lambda x: x.file): |
||||
|
errors = list(errorIterator) |
||||
|
error_count = len(errors) |
||||
|
|
||||
|
# Each file gets a test-suite |
||||
|
output_file.write("""\n <testsuite errors="{error_count}" name="{file}" tests="{error_count}" failures="0" time="0">\n""" |
||||
|
.format(error_count=error_count, file=file)) |
||||
|
for error in errors: |
||||
|
# Write each error as a test case. |
||||
|
output_file.write(""" |
||||
|
<testcase id="{id}" name="{id}" time="0"> |
||||
|
<failure message="{message}"> |
||||
|
{htmldata} |
||||
|
</failure> |
||||
|
</testcase>""".format(id="[{}/{}] {}".format(error.line, error.column, error.error_identifier), message=escape(error.error), |
||||
|
htmldata=escape(error.description))) |
||||
|
output_file.write("\n </testsuite>\n") |
||||
|
output_file.write("</testsuites>\n") |
||||
|
|
||||
|
def process_error(self, error_array): |
||||
|
if len(error_array) == 0: |
||||
|
return |
||||
|
|
||||
|
result = self.error_regex.match(error_array[0]) |
||||
|
if result is None: |
||||
|
logging.warning( |
||||
|
'Could not match error_array to regex: %s', error_array) |
||||
|
return |
||||
|
|
||||
|
# We remove the `basename` from the `file_path` to make prettier filenames in the JUnit file. |
||||
|
file_path = result.group(1).replace(self.basename, "") |
||||
|
error = ErrorDescription(file_path, int(result.group(2)), int( |
||||
|
result.group(3)), result.group(4), result.group(5), "\n".join(error_array[1:])) |
||||
|
self.errors.append(error) |
||||
|
|
||||
|
def convert(self, input_file, output_file): |
||||
|
# Collect all lines related to one error. |
||||
|
current_error = [] |
||||
|
for line in input_file: |
||||
|
# If the line starts with a `/`, it is a line about a file. |
||||
|
if line[0] == '/': |
||||
|
# Look if it is the start of a error |
||||
|
if self.main_error_identifier.search(line, re.M): |
||||
|
# If so, process any `current_error` we might have |
||||
|
self.process_error(current_error) |
||||
|
# Initialize `current_error` with the first line of the error. |
||||
|
current_error = [line] |
||||
|
else: |
||||
|
# Otherwise, append the line to the error. |
||||
|
current_error.append(line) |
||||
|
elif len(current_error) > 0: |
||||
|
# If the line didn't start with a `/` and we have a `current_error`, we simply append |
||||
|
# the line as additional information. |
||||
|
current_error.append(line) |
||||
|
else: |
||||
|
pass |
||||
|
|
||||
|
# If we still have any current_error after we read all the lines, |
||||
|
# process it. |
||||
|
if len(current_error) > 0: |
||||
|
self.process_error(current_error) |
||||
|
|
||||
|
# Print the junit file. |
||||
|
self.print_junit_file(output_file) |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
if len(sys.argv) < 2: |
||||
|
logging.error("Usage: %s base-filename-path", sys.argv[0]) |
||||
|
logging.error( |
||||
|
" base-filename-path: Removed from the filenames to make nicer paths.") |
||||
|
sys.exit(1) |
||||
|
converter = ClangTidyConverter(sys.argv[1]) |
||||
|
converter.convert(sys.stdin, sys.stdout) |
||||
|
|
Loading…
Reference in new issue