diff --git a/ceph-pr-clang-tidy/build/LICENSE.clang-tidy-to-junit.py b/ceph-pr-clang-tidy/build/LICENSE.clang-tidy-to-junit.py new file mode 100644 index 000000000..e4605d415 --- /dev/null +++ b/ceph-pr-clang-tidy/build/LICENSE.clang-tidy-to-junit.py @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 PSPDFKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ceph-pr-clang-tidy/build/build b/ceph-pr-clang-tidy/build/build new file mode 100644 index 000000000..4f1ca22fa --- /dev/null +++ b/ceph-pr-clang-tidy/build/build @@ -0,0 +1,45 @@ +#!/bin/bash + +set -ex + +BUILD_DIR="$WORKSPACE/ceph/build" +TAR_FILE_CEPH_DIRS="$WORKSPACE/ceph_directories.tar" +TAR_FILE_SOURCE="$WORKSPACE/source.tar" + +echo "Installing dependencies..." +sudo apt-get install -y curl python3-routes clang-tidy + +echo "Cloning ceph repository..." +git clone https://github.com/ceph/ceph --depth 10 + +echo "Extracting ceph_directories.tar to $BUILD_DIR..." +sudo mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" +sudo rm -rf boost include +sudo tar -xf "../../$TAR_FILE_CEPH_DIRS" +cd - + +echo "Extracting source.tar to replace src..." +cd ceph +sudo rm -rf src +sudo tar -xf "../$TAR_FILE_SOURCE" +cd .. + +echo "Configuring build with CMake..." +cd "$BUILD_DIR" +sudo cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. +cd - + +echo "Running clang-tidy on modified files in src/rgw/..." +MODIFIED_FILES=$(cd ceph && git diff --name-only HEAD HEAD~1 | grep '^src/rgw/' || true) +if [[ -n "$MODIFIED_FILES" ]]; then + echo "Analyzing files: $MODIFIED_FILES" + cd "$BUILD_DIR" + MODIFIED_PATHS=$(echo "$MODIFIED_FILES" | sed 's|^|../|') + run-clang-tidy -checks="-*,bugprone-use-after-move" $MODIFIED_PATHS || true + cd - +else + echo "No modified files in src/rgw/ to analyze." +fi + +echo "Script completed successfully." diff --git a/ceph-pr-clang-tidy/build/clang-tidy-to-junit.py b/ceph-pr-clang-tidy/build/clang-tidy-to-junit.py new file mode 100755 index 000000000..7a80ae2f3 --- /dev/null +++ b/ceph-pr-clang-tidy/build/clang-tidy-to-junit.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +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(""" +""".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 \n""" + .format(error_count=error_count, file=file)) + for error in errors: + # Write each error as a test case. + output_file.write(""" + + +{htmldata} + + """.format(id="[{}/{}] {}".format(error.line, error.column, error.error_identifier), + message=escape(error.error, entities={"\"": """}), + htmldata=escape(error.description))) + output_file.write("\n \n") + output_file.write("\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) diff --git a/ceph-pr-clang-tidy/config/definitions/ceph-pr-clang-tidy.yml b/ceph-pr-clang-tidy/config/definitions/ceph-pr-clang-tidy.yml new file mode 100644 index 000000000..5bece94b2 --- /dev/null +++ b/ceph-pr-clang-tidy/config/definitions/ceph-pr-clang-tidy.yml @@ -0,0 +1,55 @@ +- job: + name: ceph-pr-clang-tidy + node: jammy && small + project-type: freestyle + defaults: global + display-name: 'ceph: Clang-tidy checks' + concurrent: true + quiet-period: 5 + block-downstream: false + block-upstream: false + retry-count: 3 + properties: + - build-discarder: + days-to-keep: 15 + artifact-days-to-keep: 15 + + parameters: + - string: + name: BRANCH + default: main + triggers: + - github-pull-request: + allow-whitelist-orgs-as-admins: true + org-list: + - ceph + only-trigger-phrase: false + status-context: "Clang-tidy lint check" + started-status: "checking if bugs exist" + success-status: "no bugs found" + failure-status: "bugs found" + + scm: + - git: + url: https://github.com/ceph/ceph.git + branches: + - $BRANCH + browser: auto + timeout: 20 + shallow-clone: true + wipe-workspace: true + + builders: + - copyartifact: + project: ceph-pull-requests + + - shell: + !include-raw: + - ../../../scripts/build_utils.sh + - ../../build/build + - ../../build/clang-tidy-to-junit.py + + publishers: + - junit: + results: report.xml + allow-empty-results: true