diff --git a/.github/actions/env_setup/action.yml b/.github/actions/env_setup/action.yml new file mode 100644 index 00000000..6c118ffe --- /dev/null +++ b/.github/actions/env_setup/action.yml @@ -0,0 +1,43 @@ +name: 'Setup environment' +description: 'Sets up analyzers, CodeChecker and Bazel' +runs: + using: "composite" + steps: + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.15.0 + + # Writing out bazel version forces bazelisk to install bazel here + - name: Set bazel version to 6.5.0 + run: | + echo "6.5.0" > .bazelversion + bazel version + shell: bash + + - name: Install python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install CodeChecker analyzers + run: | + sudo apt-get update --quiet + sudo apt-get install --no-install-recommends \ + clang \ + clang-tools \ + clang-tidy + # The default naming of the clang-extdef-mapping, needed for CTU, is + # installed by clang-tools also contains the major version + # (e.g. clang-extdef-mapping-18), but the bazel rules reference it + # without the version number. To this end, we use update-alternatives + # to rename the binary to omit it. + sudo update-alternatives --install \ + /usr/bin/clang-extdef-mapping \ + clang-extdef-mapping \ + /usr/bin/clang-extdef-mapping-$(clang --version | head -n 1 | + sed -E 's/.*version ([0-9]+)\..*/\1/') \ + 100 + shell: bash + + - name: Install CodeChecker + run: pip3 install codechecker + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a8038b91 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: codechecker-bazel-tests + +# Triggers the workflow on push or pull request events. +on: [push, pull_request] + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rules_test: + name: Unit tests + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: ./.github/actions/env_setup + + - name: Print versions + run: | + bazel version + CodeChecker version + echo "[NOTE]: If you are debugging, its possible that " \ + "CodeChecker finds different analyzers when running in " \ + "bazel's sandbox environment!" + CodeChecker analyzers + + - name: Run tests + run: | + cd test + python3 test.py -vvv + + # Prepares matrix used to generate jobs in project_test_runner + # This job assumes that every project patch file is in the .github/workflows/patches directory + # and the name of these scripts follows this rule: patch-project_name.sh + # patches must clone their repository into folder: test-proj + prepare_project_matrix: + runs-on: ubuntu-24.04 + name: Collecting Projects + outputs: + project_configurations: ${{ steps.generate_matrix.outputs.matrix_json }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate dynamic project matrix + id: generate_matrix + run: | + PATCH_DIR="./test/foss/" + TEMP_JSON_FILE=$(mktemp) + find "$PATCH_DIR" -maxdepth 1 -mindepth 1 -type d ! -name "templates" -print0 | while IFS= read -r -d $'\0' PROJECT_FOLDER; do + # Extract project name from folder name + PROJECT_NAME=$(basename "$PROJECT_FOLDER") + + jq -n -c \ + --arg name "$PROJECT_NAME" \ + --arg folder "$PROJECT_FOLDER" \ + '{ name: $name, folder: $folder }' >> "$TEMP_JSON_FILE" + + echo "Added $PROJECT_NAME to matrix." + done + + if [ -s "$TEMP_JSON_FILE" ]; then + FINAL_MATRIX_JSON="[$(paste -s -d ',' "$TEMP_JSON_FILE")]" + else + FINAL_MATRIX_JSON="[]" + fi + + echo "Generated matrix: $FINAL_MATRIX_JSON" + echo "matrix_json=$FINAL_MATRIX_JSON" >> "$GITHUB_OUTPUT" + shell: bash + + project_test_runner: + # Test the bazel rules introduced by repository on an independent open-source projects. + runs-on: ubuntu-24.04 + needs: prepare_project_matrix + strategy: + fail-fast: false + max-parallel: 2 # limit number of concurrent jobs + matrix: + project: ${{ fromJson(needs.prepare_project_matrix.outputs.project_configurations) }} + + name: "Test On Project: ${{ matrix.project.name }}" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: ./.github/actions/env_setup + + - name: Initializing test + run: | + cd ${{ matrix.project.folder }} + sh ./init.sh + + # Running bazel with test will signal failure because CodeChecker found problems + - name: Run Monolithic Bazel CodeChecker + run: | + cd ${{ matrix.project.folder }}/test-proj + bazel test :codechecker_test || [ $? -eq 3 ] && exit 0 || exit $? + + # Running bazel with test will signal failure because CodeChecker found problems + - name: Run Per File Bazel CodeChecker + run: | + cd ${{ matrix.project.folder }}/test-proj + bazel test :code_checker_test || [ $? -eq 3 ] && exit 0 || exit $? diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index b20213bb..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: codechecker-bazel-tests - -# Triggers the workflow on push or pull request events. -on: [push, pull_request] - -permissions: read-all - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - rules_test: - name: Unit tests - runs-on: ubuntu-24.04 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set bazel version to 6.5.0 - run: echo "6.5.0" > .bazelversion - - - name: Setup Bazel - uses: bazel-contrib/setup-bazel@0.15.0 - - - name: Install python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install CodeChecker analyzers - run: | - sudo apt-get update --quiet - sudo apt-get install --no-install-recommends \ - clang \ - clang-tools \ - clang-tidy - # The default naming of the clang-extdef-mapping, needed for CTU, is - # installed by clang-tools also contains the major version - # (e.g. clang-extdef-mapping-18), but the bazel rules reference it - # without the version number. To this end, we use update-alternatives - # to rename the binary to omit it. - sudo update-alternatives --install \ - /usr/bin/clang-extdef-mapping \ - clang-extdef-mapping \ - /usr/bin/clang-extdef-mapping-$(clang --version | head -n 1 | - sed -E 's/.*version ([0-9]+)\..*/\1/') \ - 100 - - - - name: Install CodeChecker - run: pip3 install codechecker - - - name: Print versions - run: | - bazel version - CodeChecker version - echo "[NOTE]: If you are debugging, its possible that " \ - "CodeChecker finds different analyzers when running in " \ - "bazel's sandbox environment!" - CodeChecker analyzers - - - name: Run tests - run: | - cd test - python3 test.py -vvv diff --git a/test/foss/README.md b/test/foss/README.md new file mode 100644 index 00000000..edb068bb --- /dev/null +++ b/test/foss/README.md @@ -0,0 +1,10 @@ +# How to add a new project: + +- Create a folder with the name of the project. +- place an init.sh script in the folder, this script should: + - Clone the test project into a folder named `test-proj`. + - To ensure the project doesn't change over time, check out a specific tag or commit instead of a branch! + - Copy the .bazelversion file from the templates directory. + - Append the WORKSPACE.template file to the WORKSPACE file of the project. + - Append the codechecker rules to the BUILD file of the project. + - There can be only two targets, codechecker_test and code_checker_test \ No newline at end of file diff --git a/test/foss/templates/.bazelversion b/test/foss/templates/.bazelversion new file mode 100644 index 00000000..4be2c727 --- /dev/null +++ b/test/foss/templates/.bazelversion @@ -0,0 +1 @@ +6.5.0 \ No newline at end of file diff --git a/test/foss/templates/WORKSPACE.template b/test/foss/templates/WORKSPACE.template new file mode 100644 index 00000000..ffda78d4 --- /dev/null +++ b/test/foss/templates/WORKSPACE.template @@ -0,0 +1,20 @@ +#---------------------------------------------------- + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +local_repository( + name = "bazel_codechecker", + path = "../../../../", +) + +load( + "@bazel_codechecker//src:tools.bzl", + "register_default_codechecker", + "register_default_python_toolchain", +) + +register_default_python_toolchain() + +register_default_codechecker() + +#---------------------------------------------------- \ No newline at end of file diff --git a/test/foss/yaml-cpp/init.sh b/test/foss/yaml-cpp/init.sh new file mode 100755 index 00000000..f86d5160 --- /dev/null +++ b/test/foss/yaml-cpp/init.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright 2023 Ericsson AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +git clone --recurse https://github.com/jbeder/yaml-cpp.git test-proj +cd test-proj +git checkout yaml-cpp-0.7.0 + +# This file must be in the root of the project to be analyzed for bazelisk to work +cp ../../templates/.bazelversion ./.bazelversion + +# Add codechecker to the project +cat <> BUILD.bazel +#------------------------------------------------------- + +# codechecker rules +load( + "@bazel_codechecker//src:codechecker.bzl", + "codechecker_test", +) +load( + "@bazel_codechecker//src:code_checker.bzl", + "code_checker_test", +) + + +codechecker_test( + name = "codechecker_test", + targets = [ + ":yaml-cpp", + ], +) + +code_checker_test( + name = "code_checker_test", + targets = [ + ":yaml-cpp", + ], +) + +#------------------------------------------------------- +EOF + +# Add codechecker_bazel repo to WORKSPACE +cat ../../templates/WORKSPACE.template >> WORKSPACE diff --git a/test/foss/zlib/init.sh b/test/foss/zlib/init.sh new file mode 100755 index 00000000..da02fc7e --- /dev/null +++ b/test/foss/zlib/init.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright 2023 Ericsson AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +git clone --recurse https://github.com/madler/zlib.git test-proj +cd test-proj +git checkout 5a82f71ed1dfc0bec044d9702463dbdf84ea3b71 + +# This file must be in the root of the project to be analyzed for bazelisk to work +cp ../../templates/.bazelversion ./.bazelversion + +# Add codechecker to the project +cat <> BUILD.bazel +#------------------------------------------------------- + +# codechecker rules +load( + "@bazel_codechecker//src:codechecker.bzl", + "codechecker_test", +) +load( + "@bazel_codechecker//src:code_checker.bzl", + "code_checker_test", +) + + +codechecker_test( + name = "codechecker_test", + targets = [ + ":z", + ], +) + +code_checker_test( + name = "code_checker_test", + targets = [ + ":z", + ], +) + +#------------------------------------------------------- +EOF + +# Add codechecker_bazel repo to WORKSPACE +cat ../../templates/WORKSPACE.template >> WORKSPACE diff --git a/test/foss_test.py b/test/foss_test.py new file mode 100644 index 00000000..67facffa --- /dev/null +++ b/test/foss_test.py @@ -0,0 +1,179 @@ +# Copyright 2023 Ericsson AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import shutil +import subprocess + +FOSS_BASE_DIR = "./foss" + +def main(): + """ + Finds all open source project and calls the tests on each of them + """ + if not os.path.isdir(FOSS_BASE_DIR): + print(f"Error: The directory '{FOSS_BASE_DIR}' does not exist.") + sys.exit(1) + + if len(sys.argv) > 1 and sys.argv[1] == "clean": + clean_projects() + sys.exit(0) + + print(f"Starting in '{FOSS_BASE_DIR}' ...") + print("-----------------------------------------------------------------") + + if shutil.which("CodeChecker") is None: + print("CodeChecker not found! Terminating...") + print("----------------------------------------------------------\ + -------") + sys.exit(1) + + try: + for project_name in os.listdir(FOSS_BASE_DIR): + project_dir = os.path.join(FOSS_BASE_DIR, project_name) + + if not os.path.isdir(project_dir): + continue + + if project_name == "templates": + continue + + process_project(project_dir, project_name) + + except OSError as e: + print(f"An error occurred while processing directories: {e}") + sys.exit(1) + + print("\n-----------------------------------------------------------------") + print("Done.") + +def clean_projects(): + """ + Finds and removes 'test-proj' directories and log files + """ + print("Starting cleanup process...") + for root, dirs, files in os.walk(FOSS_BASE_DIR): + depth = root.count(os.sep) - FOSS_BASE_DIR.count(os.sep) + if depth == 1 and "test-proj" in dirs: + test_proj_path = os.path.join(root, "test-proj") + print(f"Removing directory: {test_proj_path}") + shutil.rmtree(test_proj_path) + if "codechecker.log" in files: + log_pth = os.path.join(root, "codechecker.log") + print(log_pth) + os.remove(log_pth) + if "code_checker.log" in files: + log_pth = os.path.join(root, "code_checker.log") + print(log_pth) + os.remove(log_pth) + +def process_project(project_dir, project_name): + """ + Handles the initializing and testing process for a single project. + """ + print(f"\nProcessing project: {project_name} ({project_dir})") + print("-----------------------------------------------------------------") + + test_proj_dir = os.path.join(project_dir, "test-proj") + init_script_path = os.path.join(project_dir, "init.sh") + + if not os.path.isdir(test_proj_dir): + print(" Running ./init.sh...") + + if not os.path.isfile(init_script_path): + print(f" Warning: ./init.sh not found in {project_name}. \ + Skipping.") + print("---------------------------------------------------------\ + --------") + return + + if not os.access(init_script_path, os.X_OK): + print(f" Warning: ./init.sh found in {project_name} \ + but it is not executable.") + print(" Please run 'chmod +x init.sh' inside the project \ + directory and try again.") + print("-------------------------------------------------------\ + ----------") + return + + try: + subprocess.run( + ["./init.sh"], + cwd=project_dir, + check=True, + capture_output=True, + text=True + ) + except subprocess.CalledProcessError: + print(f" Warning: ./init.sh failed for {project_name}. Skipping.") + print("------------------------------------------------------\ + -----------") + return + else: + print(f"test-proj already exists. Skipping init.sh for {project_name}") + + if os.path.isdir(test_proj_dir): + print(" Running bazel build :codechecker_test...") + try: + tee_proc = subprocess.Popen(["tee", "../codechecker.log"], + stdin=subprocess.PIPE, cwd=test_proj_dir, text=True) + + subprocess.run( + ["bazel", "build", ":codechecker_test"], + cwd=test_proj_dir, + stdout=tee_proc.stdin, + stderr=tee_proc.stdin, + text=True, + check=True + ) + + tee_proc.stdin.close() + tee_proc.wait() + + except subprocess.CalledProcessError: + print(f" Error: 'bazel build :codechecker_test' failed for \ + {project_name}. Check logs above or in \ + {project_dir}/codechecker.log.") + + print(" Running bazel build :code_checker_test...") + try: + tee_proc = subprocess.Popen(["tee", "../code_checker.log"], + stdin=subprocess.PIPE, cwd=test_proj_dir, text=True) + + subprocess.run( + ["bazel", "build", ":code_checker_test"], + cwd=test_proj_dir, + stdout=tee_proc.stdin, + stderr=tee_proc.stdin, + text=True, + check=True + ) + + tee_proc.stdin.close() + tee_proc.wait() + except subprocess.CalledProcessError: + print(f" Error: 'bazel build :code_checker_test' failed for \ + {project_name}. Check logs above or in \ + {project_dir}/code_checker.log.") + else: + print(f" Error: 'test-proj' directory not found in {project_name}. \ + Skipping Bazel builds.") + + print("-----------------------------------------------------------------") + print(f"Finished processing project: {project_name}") + + +if __name__ == "__main__": + main()