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={"\"": """}), 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)