File indexing completed on 2024-11-10 04:41:11

0001 #!/usr/bin/env python3
0002 #
0003 # SPDX-FileCopyrightText: 2018 PSPDFKit
0004 #
0005 # SPDX-License-Identifier: MIT
0006 #
0007 # Originally taken from https://github.com/PSPDFKit-labs/clang-tidy-to-junit
0008 
0009 import sys
0010 import collections
0011 import re
0012 import logging
0013 import itertools
0014 from xml.sax.saxutils import escape
0015 
0016 # Create a `ErrorDescription` tuple with all the information we want to keep.
0017 ErrorDescription = collections.namedtuple(
0018     'ErrorDescription', 'file line column error error_identifier description')
0019 
0020 
0021 class ClangTidyConverter:
0022     # All the errors encountered.
0023     errors = []
0024 
0025     # Parses the error.
0026     # Group 1: file path
0027     # Group 2: line
0028     # Group 3: column
0029     # Group 4: error message
0030     # Group 5: error identifier
0031     error_regex = re.compile(
0032         r"^([\w\/\.\-\ ]+):(\d+):(\d+): (.+) (\[[\w\-,\.]+\])$")
0033 
0034     # This identifies the main error line (it has a [the-warning-type] at the end)
0035     # We only create a new error when we encounter one of those.
0036     main_error_identifier = re.compile(r'\[[\w\-,\.]+\]$')
0037 
0038     def __init__(self, basename):
0039         self.basename = basename
0040 
0041     def print_junit_file(self, output_file):
0042         # Write the header.
0043         output_file.write("""<?xml version="1.0" encoding="UTF-8" ?>
0044 <testsuites id="1" name="Clang-Tidy" tests="{error_count}" errors="{error_count}" failures="0" time="0">""".format(error_count=len(self.errors)))
0045 
0046         sorted_errors = sorted(self.errors, key=lambda x: x.file)
0047 
0048         # Iterate through the errors, grouped by file.
0049         for file, errorIterator in itertools.groupby(sorted_errors, key=lambda x: x.file):
0050             errors = list(errorIterator)
0051             error_count = len(errors)
0052 
0053             # Each file gets a test-suite
0054             output_file.write("""\n    <testsuite errors="{error_count}" name="{file}" tests="{error_count}" failures="0" time="0">\n"""
0055                               .format(error_count=error_count, file=file))
0056             for error in errors:
0057                 # Write each error as a test case.
0058                 output_file.write("""
0059         <testcase id="{id}" name="{id}" time="0">
0060             <failure message="{message}">
0061 {htmldata}
0062             </failure>
0063         </testcase>""".format(id="[{}/{}] {}".format(error.line, error.column, error.error_identifier),
0064                               message=escape(error.error, entities={"\"": "&quot;"}),
0065                               htmldata=escape(error.description)))
0066             output_file.write("\n    </testsuite>\n")
0067         output_file.write("</testsuites>\n")
0068 
0069     def process_error(self, error_array):
0070         if len(error_array) == 0:
0071             return
0072 
0073         result = self.error_regex.match(error_array[0])
0074         if result is None:
0075             logging.warning(
0076                 'Could not match error_array to regex: %s', error_array)
0077             return
0078 
0079         # We remove the `basename` from the `file_path` to make prettier filenames in the JUnit file.
0080         file_path = result.group(1).replace(self.basename, "")
0081         error = ErrorDescription(file_path, int(result.group(2)), int(
0082             result.group(3)), result.group(4), result.group(5), "\n".join(error_array[1:]))
0083         self.errors.append(error)
0084 
0085     def convert(self, input_file, output_file):
0086         # Collect all lines related to one error.
0087         current_error = []
0088         for line in input_file:
0089             # If the line starts with a `/`, it is a line about a file.
0090             if line[0] == '/':
0091                 # Look if it is the start of a error
0092                 if self.main_error_identifier.search(line, re.M):
0093                     # If so, process any `current_error` we might have
0094                     self.process_error(current_error)
0095                     # Initialize `current_error` with the first line of the error.
0096                     current_error = [line]
0097                 else:
0098                     # Otherwise, append the line to the error.
0099                     current_error.append(line)
0100             elif len(current_error) > 0:
0101                 # If the line didn't start with a `/` and we have a `current_error`, we simply append
0102                 # the line as additional information.
0103                 current_error.append(line)
0104             else:
0105                 pass
0106 
0107         # If we still have any current_error after we read all the lines,
0108         # process it.
0109         if len(current_error) > 0:
0110             self.process_error(current_error)
0111 
0112         # Print the junit file.
0113         self.print_junit_file(output_file)
0114 
0115 
0116 if __name__ == "__main__":
0117     if len(sys.argv) < 2:
0118         logging.error("Usage: %s base-filename-path", sys.argv[0])
0119         logging.error(
0120             "  base-filename-path: Removed from the filenames to make nicer paths.")
0121         sys.exit(1)
0122     converter = ClangTidyConverter(sys.argv[1])
0123     converter.convert(sys.stdin, sys.stdout)