diff --git a/.github/workflows/.beman_submodule b/.github/workflows/.beman_submodule new file mode 100644 index 00000000..bb152318 --- /dev/null +++ b/.github/workflows/.beman_submodule @@ -0,0 +1,4 @@ +[beman_submodule] +remote=https://github.com/bemanproject/infra-workflows.git +commit_hash=f1afd008de5fcee15a2a20a8b513e5dcde41e67e +allow_untracked_files=True diff --git a/.github/workflows/.github/CODEOWNERS b/.github/workflows/.github/CODEOWNERS new file mode 100644 index 00000000..7a5f53c5 --- /dev/null +++ b/.github/workflows/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ednolan diff --git a/.github/workflows/LICENSE b/.github/workflows/LICENSE new file mode 100644 index 00000000..111a208f --- /dev/null +++ b/.github/workflows/LICENSE @@ -0,0 +1,230 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the Beman Project: +============================================================================== +The Beman Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..f71fd913 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,85 @@ +# Beman Project Reusable Github Actions Repository + + + +This repository contains [reusable GitHub Actions +workflows](https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows) +workflow files, intended to help unify the GitHub Actions machinery used across Beman +repositories for CI. It contains the following reusable workflows: + +## `reusable-beman-build-and-test.yml` + +This is the main workflow file used for CI. It takes in a JSON build configuration like +the following example: + +```json +{ + "gcc": [ + { "versions": ["15"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++"], + "tests": [ + "Debug.Default", "Release.Default", "Debug.TSan", "Debug.MaxSan", + "Debug.Werror", "Debug.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23", "c++20", "c++17"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + "clang-p2996": [ + { "versions": ["trunk"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [{"stdlibs": ["libc++"], "tests": ["Release.-DCMAKE_CXX_FLAGS='-freflection-latest'"]}] + } + ] + } + ] +} +``` + +It then runs jobs corresponding to the specified set of configurations. + +## `reusable-beman-create-issue-when-fault.yml` + +This workflow is intended to help with projects that invoke CI on a scheduled basis when +those jobs fail. It creates a GitHub issue describing the CI failure. + +## `reusable-beman-preset-test.yml` + +This workflow is intended to ensure that the CMake presets provided by beman/infra are +valid and working for the given repository. It takes in a JSON build configuration like +the following: + +```json +[ + {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "appleclang-debug", "runner": "macos-latest"}, + {"preset": "appleclang-release", "runner": "macos-latest"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} +] +``` + +It then runs jobs corresponding to the specified set of presets. + +## `reusable-beman-pre-commit.yml` + +This provides a workflow for running the +[pre-commit](https://github.com/pre-commit/pre-commit) checks Beman libraries use, on pull +requests and on push. + +## `reusable-beman-submodule-check.yml` + +This provides a workflow for checking consistency of +[`beman-submodule`](https://github.com/bemanproject/infra/blob/main/tools/beman-submodule/README.md) +directories used by Beman repositories to deduplicate infrastructure. diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml new file mode 100644 index 00000000..0d7963a7 --- /dev/null +++ b/.github/workflows/ci_tests.yml @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Continuous Integration Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + schedule: + - cron: '30 15 * * *' + +jobs: + beman-submodule-check: + uses: ./.github/workflows/reusable-beman-submodule-check.yml + + preset-test: + uses: ./.github/workflows/reusable-beman-preset-test.yml + with: + matrix_config: > + [ + {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} + ] + + build-and-test: + uses: ./.github/workflows/reusable-beman-build-and-test.yml + with: + matrix_config: > + { + "gcc": [ + { "versions": ["15"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic", "Debug.Coverage" + ] + } + ] + }, + { "cxxversions": ["c++23", "c++20"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { "versions": ["14", "13"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "clang": [ + { "versions": ["20"], + "tests": [ + {"cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++", "libc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23", "c++20"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["19"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["18", "17"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "msvc": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23"], + "tests": [ + { "stdlibs": ["stl"], + "tests": ["Debug.Default", "Release.Default"] + } + ] + } + ] + } + ] + } + + create-issue-when-fault: + needs: [preset-test, build-and-test] + if: failure() && github.event_name == 'schedule' + uses: ./.github/workflows/reusable-beman-create-issue-when-fault.yml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml deleted file mode 100644 index bede8f33..00000000 --- a/.github/workflows/linux.yml +++ /dev/null @@ -1,58 +0,0 @@ -# .github/workflows/linux.yml -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -name: Linux Build - -on: - push: - branches: ["main"] - paths: - - "include/**" - - "src/**" - - "tests/**" - - "examples/**" - - "cmake/**" - - "Makefile" - - "CMakePresets.json" - - "CMakeLists.txt" - - ".github/workflows/linux.yml" - pull_request: - branches: ["main"] - paths: - - "include/**" - - "src/**" - - "tests/**" - - "examples/**" - - "cmake/**" - - "Makefile" - - "CMakePresets.json" - - "CMakeLists.txt" - - ".github/workflows/linux.yml" - -jobs: - build: - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - - matrix: - # TODO: sanitizer: [debug, release, asan, usan, tsan, lsan, msan] - preset: [debug, release] - compiler: [g++-14, clang++-19] - - steps: - - uses: actions/checkout@v4 - - - name: Install build tools - run: | - sudo apt-get install ninja-build -y -q - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 19 all - - - name: Linux ${{ matrix.compiler }} ${{ matrix.preset }} - run: CXX=${{ matrix.compiler }} cmake --workflow --preset ${{ matrix.preset }} - - - name: Linux ${{ matrix.compiler }} sanitizer - if: startsWith(matrix.preset, 'debug') - run: CXX=${{ matrix.compiler }} make all diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index d4de55eb..00000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,75 +0,0 @@ -# .github/workflows/macos.yml -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -name: Macos Build - -on: - push: - branches: ["main"] - paths: - - "include/**" - - "src/**" - - "tests/**" - - "examples/**" - - "cmake/**" - - "Makefile" - - "CMakePresets.json" - - "CMakeLists.txt" - - ".github/workflows/macos.yml" - pull_request: - branches: ["main"] - paths: - - "include/**" - - "src/**" - - "tests/**" - - "examples/**" - - "cmake/**" - - "Makefile" - - "CMakePresets.json" - - "CMakeLists.txt" - - ".github/workflows/macos.yml" - -jobs: - build: - runs-on: macos-15 - strategy: - fail-fast: false - - matrix: - preset: [debug, release] - # TODO: compiler: [g++, clang++-19] - compiler: [g++, clang++-18] - - steps: - - uses: actions/checkout@v4 - - - name: Setup Cpp - # if: startsWith(matrix.compiler, 'clang') - uses: aminya/setup-cpp@v1 - with: - # TODO: compiler: llvm-19 - # clangtidy: true - # cmake: true - ninja: true - - - name: Install llvm-19 - if: startsWith(matrix.compiler, 'clang') - run: | - brew install llvm@19 || echo ignored - - - name: macos clang++-18 ${{ matrix.preset }} - if: startsWith(matrix.compiler, 'clang') - run: CXX=$(brew --prefix llvm@18)/bin/clang++ cmake --workflow --preset ${{ matrix.preset }} - - - name: macos clang++-18 sanitizer - if: startsWith(matrix.compiler, 'clang') && startsWith(matrix.preset, 'debug') - run: CXX=$(brew --prefix llvm@18)/bin/clang++ make all - - - name: macos g++ ${{ matrix.preset }} - if: startsWith(matrix.compiler, 'g++') - run: CXX=${{ matrix.compiler }} cmake --workflow --preset ${{ matrix.preset }} - - # TODO: fails with AppleClang 16.0.0 on CI! - # - name: macos g++ sanitizer - # if: startsWith(matrix.compiler, 'g++') && startsWith(matrix.preset, 'debug') - # run: CXX=${{ matrix.compiler }} make all diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 63057b9f..2409d2f4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,44 +1,13 @@ name: Lint Check (pre-commit) on: - pull_request: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: push: + branches: + - main jobs: pre-commit: - runs-on: ubuntu-latest - name: pre-commit - permissions: - contents: read - checks: write - issues: write - pull-requests: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - - name: Get Changed Files - id: changed-files - uses: tj-actions/changed-files@v45 - - # See: - # https://github.com/tj-actions/changed-files?tab=readme-ov-file#using-local-git-directory- - - uses: pre-commit/action@v3.0.1 - with: - extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} - continue-on-error: true - - - name: suggester / pre-commit - if: ${{ github.event_name == 'pull_request' }} - uses: reviewdog/action-suggester@v1 - with: - tool_name: pre-commit - level: warning - reviewdog_flags: "-fail-level=error" - github_token: ${{ secrets.GITHUB_TOKEN }} + uses: ./.github/workflows/reusable-beman-pre-commit.yml diff --git a/.github/workflows/reusable-beman-build-and-test.yml b/.github/workflows/reusable-beman-build-and-test.yml new file mode 100644 index 00000000..b1cd4f10 --- /dev/null +++ b/.github/workflows/reusable-beman-build-and-test.yml @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: 'Beman build-and-test matrix' +on: + workflow_call: + inputs: + matrix_config: + description: 'JSON specification of tests to run' + type: string + required: true +jobs: + configure_test_matrix: + name: Configure test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - id: set-matrix + shell: python + run: | + import json + import os + import sys + + input_data = json.loads('''${{inputs.matrix_config}}''') + output_list = [] + + image_prefix = "ghcr.io/bemanproject/infra-containers-" + for compiler, compiler_tests in input_data.items(): + for compiler_test in compiler_tests: + for version in compiler_test["versions"]: + image = None + if compiler == "gcc" or compiler.startswith("clang"): + image = image_prefix + compiler + ":" + version + for versions_test in compiler_test["tests"]: + for cxxversion in versions_test["cxxversions"]: + for cxxversion_test in versions_test["tests"]: + for stdlib in cxxversion_test["stdlibs"]: + for stdlib_test in cxxversion_test["tests"]: + test = { + "compiler": compiler, + "version": version, + "cxxversion": cxxversion, + "stdlib": stdlib, + "test": stdlib_test + } + if image is not None: + test["image"] = image + output_list.append(test) + json_string = json.dumps(output_list) + with open(os.environ['GITHUB_OUTPUT'], 'a') as output_file: + print(f"matrix={json_string}", file=output_file) + test_matrix: + needs: configure_test_matrix + strategy: + fail-fast: false + matrix: + config: ${{ fromJson(needs.configure_test_matrix.outputs.matrix) }} + name: "${{ matrix.config.compiler }} ${{ matrix.config.version }} ${{ matrix.config.cxxversion }} ${{ matrix.config.stdlib }} ${{ matrix.config.test }}" + runs-on: >- + ${{ (matrix.config.compiler == 'gcc' || startsWith(matrix.config.compiler, 'clang')) && 'ubuntu-latest' + || (matrix.config.compiler == 'appleclang' && 'macos-latest') + || (matrix.config.compiler == 'msvc' && 'windows-latest') }} + container: ${{ matrix.config.image }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Setup MSVC + if: matrix.config.compiler == 'msvc' + uses: TheMrMilchmann/setup-msvc-dev@v3 + with: + arch: x64 + - name: Setup Macos + if: matrix.config.compiler == 'appleclang' + shell: bash + run: sudo chmod -R 777 /opt/ + - name: Print installed software + shell: bash + run: | + echo "Build system:" + cmake --version + ninja --version + - name: Process configuration + id: vars + shell: bash + run: | + cxxversion=${{ matrix.config.cxxversion }} + echo "cpp_version=${cxxversion#c++}" >> "$GITHUB_OUTPUT" + case ${{ matrix.config.compiler }} in + gcc) echo "toolchain_file=infra/cmake/gnu-toolchain.cmake" >> "$GITHUB_OUTPUT" ;; + clang*) + case ${{ matrix.config.stdlib }} in + libstdc++) + echo "toolchain_file=infra/cmake/llvm-toolchain.cmake" >> "$GITHUB_OUTPUT" ;; + libc++) + echo "toolchain_file=infra/cmake/llvm-libc++-toolchain.cmake" >> "$GITHUB_OUTPUT" ;; + esac ;; + appleclang) + echo "toolchain_file=infra/cmake/appleclang-toolchain.cmake" >> "$GITHUB_OUTPUT" ;; + msvc) + echo "toolchain_file=infra/cmake/msvc-toolchain.cmake" >> "$GITHUB_OUTPUT" ;; + esac + test=${{ matrix.config.test }} + echo "build_config=${test%%[.]*}" >> "$GITHUB_OUTPUT" + test_type=${test##*[.]} + echo "test_type=$test_type" >> "$GITHUB_OUTPUT" + case $test_type in + Default) ;; + TSan) + echo "cmake_extra_args=-DBEMAN_BUILDSYS_SANITIZER=TSan" >> "$GITHUB_OUTPUT" ;; + MaxSan) + echo "cmake_extra_args=-DBEMAN_BUILDSYS_SANITIZER=MaxSan" >> "$GITHUB_OUTPUT" ;; + Werror) + echo "cmake_extra_args=-DCMAKE_CXX_FLAGS='-Werror=all -Werror=extra'" >> "$GITHUB_OUTPUT" ;; + Dynamic) + echo "cmake_extra_args=-DBUILD_SHARED_LIBS=on" >> "$GITHUB_OUTPUT" ;; + Coverage) + echo "cmake_extra_args=-DCMAKE_CXX_FLAGS='-fno-default-inline -fno-inline --coverage -fprofile-abs-path'" >> "$GITHUB_OUTPUT";; + *) + echo "cmake_extra_args=$test_type" >> "$GITHUB_OUTPUT" ;; + esac + - name: Print parameters + shell: bash + run: | + echo ${{ matrix.config.compiler }} ${{ matrix.config.cxxversion }} ${{ matrix.config.stdlib }} ${{ matrix.config.test }} + echo ${{ steps.vars.outputs.cpp_version }} ${{ steps.vars.outputs.toolchain_file }} ${{ steps.vars.outputs.cmake_extra_args }} ${{ steps.vars.outputs.build_config }} + - name: Configure CMake + shell: bash + run: | + cmake \ + -B build \ + -S . \ + -DCMAKE_CXX_STANDARD=${{ steps.vars.outputs.cpp_version }} \ + -DCMAKE_TOOLCHAIN_FILE="${{ steps.vars.outputs.toolchain_file }}" \ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="./infra/cmake/use-fetch-content.cmake" \ + ${{ steps.vars.outputs.cmake_extra_args }} + env: + CMAKE_GENERATOR: "Ninja Multi-Config" + - name: Build + shell: bash + run: | + cmake --build build --config ${{ steps.vars.outputs.build_config }} --parallel --verbose + cmake --build build --config ${{ steps.vars.outputs.build_config }} --target all_verify_interface_header_sets + cmake --install build --config ${{ steps.vars.outputs.build_config }} --prefix /opt/beman.package + ls -R /opt/beman.package + - name: Test + shell: bash + run: ctest --test-dir build --build-config ${{ steps.vars.outputs.build_config }} --output-on-failure + - name: Generate Coverage Files + if: steps.vars.outputs.test_type == 'Coverage' + shell: bash + run: | + cat > gcovr.cfg < + +This repository contains the infrastructure for The Beman Project. This is NOT a library repository, so it does not +respect the usual structure of a Beman library repository nor The Beman Standard! + +## Description + +* `containers/`: Containers used for CI builds and tests in the Beman org. +* `tools/`: Tools used to manage the infrastructure and the codebase (e.g., linting, formatting, etc.). diff --git a/infra/cmake/appleclang-toolchain.cmake b/infra/cmake/appleclang-toolchain.cmake new file mode 100644 index 00000000..5f44e802 --- /dev/null +++ b/infra/cmake/appleclang-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for apple clang family of compiler. +# Note this is different from LLVM toolchain. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. Note that apple clang does not support leak sanitizer. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +# Prevent PATH collision with an LLVM clang installation by using the system +# compiler shims +set(CMAKE_C_COMPILER cc) +set(CMAKE_CXX_COMPILER c++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") diff --git a/infra/cmake/gnu-toolchain.cmake b/infra/cmake/gnu-toolchain.cmake new file mode 100644 index 00000000..b6dddf6a --- /dev/null +++ b/infra/cmake/gnu-toolchain.cmake @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for GNU family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures gcc and g++ to use all available non-conflicting +# sanitizers. +# - TSan: configures gcc and g++ to enable the use of thread sanitizer + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") diff --git a/infra/cmake/llvm-libc++-toolchain.cmake b/infra/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 00000000..76264c69 --- /dev/null +++ b/infra/cmake/llvm-libc++-toolchain.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSL-1.0 + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) + +if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") +endif() diff --git a/infra/cmake/llvm-toolchain.cmake b/infra/cmake/llvm-toolchain.cmake new file mode 100644 index 00000000..5f5ee4b8 --- /dev/null +++ b/infra/cmake/llvm-toolchain.cmake @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") diff --git a/infra/cmake/msvc-toolchain.cmake b/infra/cmake/msvc-toolchain.cmake new file mode 100644 index 00000000..c2fffa79 --- /dev/null +++ b/infra/cmake/msvc-toolchain.cmake @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for MSVC family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures cl to use all available non-conflicting sanitizers. +# +# Note that in other toolchain files, TSan is also a possible value for +# BEMAN_BUILDSYS_SANITIZER, however, MSVC does not support thread sanitizer, +# thus this value is omitted. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER cl) +set(CMAKE_CXX_COMPILER cl) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + # /Zi flag (add debug symbol) is needed when using address sanitizer + # See C5072: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-c5072 + set(SANITIZER_FLAGS "/fsanitize=address /Zi") +endif() + +set(CMAKE_CXX_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") +set(CMAKE_C_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "/EHsc /permissive- /O2 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") diff --git a/infra/cmake/use-fetch-content.cmake b/infra/cmake/use-fetch-content.cmake new file mode 100644 index 00000000..07c1a15d --- /dev/null +++ b/infra/cmake/use-fetch-content.cmake @@ -0,0 +1,179 @@ +cmake_minimum_required(VERSION 3.24) + +include(FetchContent) + +if(NOT BEMAN_EXEMPLAR_LOCKFILE) + set(BEMAN_EXEMPLAR_LOCKFILE + "lockfile.json" + CACHE FILEPATH + "Path to the dependency lockfile for the Beman Exemplar." + ) +endif() + +set(BemanExemplar_projectDir "${CMAKE_CURRENT_LIST_DIR}/../..") +message(TRACE "BemanExemplar_projectDir=\"${BemanExemplar_projectDir}\"") + +message(TRACE "BEMAN_EXEMPLAR_LOCKFILE=\"${BEMAN_EXEMPLAR_LOCKFILE}\"") +file( + REAL_PATH + "${BEMAN_EXEMPLAR_LOCKFILE}" + BemanExemplar_lockfile + BASE_DIRECTORY "${BemanExemplar_projectDir}" + EXPAND_TILDE +) +message(DEBUG "Using lockfile: \"${BemanExemplar_lockfile}\"") + +# Force CMake to reconfigure the project if the lockfile changes +set_property( + DIRECTORY "${BemanExemplar_projectDir}" + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${BemanExemplar_lockfile}" +) + +# For more on the protocol for this function, see: +# https://cmake.org/cmake/help/latest/command/cmake_language.html#provider-commands +function(BemanExemplar_provideDependency method package_name) + # Read the lockfile + file(READ "${BemanExemplar_lockfile}" BemanExemplar_rootObj) + + # Get the "dependencies" field and store it in BemanExemplar_dependenciesObj + string( + JSON + BemanExemplar_dependenciesObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_rootObj}" + "dependencies" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + # Get the length of the libraries array and store it in BemanExemplar_dependenciesObj + string( + JSON + BemanExemplar_numDependencies + ERROR_VARIABLE BemanExemplar_error + LENGTH "${BemanExemplar_dependenciesObj}" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + if(BemanExemplar_numDependencies EQUAL 0) + return() + endif() + + # Loop over each dependency object + math(EXPR BemanExemplar_maxIndex "${BemanExemplar_numDependencies} - 1") + foreach(BemanExemplar_index RANGE "${BemanExemplar_maxIndex}") + set(BemanExemplar_errorPrefix + "${BemanExemplar_lockfile}, dependency ${BemanExemplar_index}" + ) + + # Get the dependency object at BemanExemplar_index + # and store it in BemanExemplar_depObj + string( + JSON + BemanExemplar_depObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_dependenciesObj}" + "${BemanExemplar_index}" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "name" field and store it in BemanExemplar_name + string( + JSON + BemanExemplar_name + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "package_name" field and store it in BemanExemplar_pkgName + string( + JSON + BemanExemplar_pkgName + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "package_name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_repository" field and store it in BemanExemplar_repo + string( + JSON + BemanExemplar_repo + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_repository" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_tag" field and store it in BemanExemplar_tag + string( + JSON + BemanExemplar_tag + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_tag" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + if(method STREQUAL "FIND_PACKAGE") + if(package_name STREQUAL BemanExemplar_pkgName) + string( + APPEND + BemanExemplar_debug + "Redirecting find_package calls for ${BemanExemplar_pkgName} " + "to FetchContent logic fetching ${BemanExemplar_repo} at " + "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." + ) + message(DEBUG "${BemanExemplar_debug}") + FetchContent_Declare( + "${BemanExemplar_name}" + GIT_REPOSITORY "${BemanExemplar_repo}" + GIT_TAG "${BemanExemplar_tag}" + EXCLUDE_FROM_ALL + ) + set(INSTALL_GTEST OFF) # Disable GoogleTest installation + FetchContent_MakeAvailable("${BemanExemplar_name}") + + # Important! _FOUND tells CMake that `find_package` is + # not needed for this package anymore + set("${BemanExemplar_pkgName}_FOUND" TRUE PARENT_SCOPE) + endif() + endif() + endforeach() +endfunction() + +cmake_language( + SET_DEPENDENCY_PROVIDER BemanExemplar_provideDependency + SUPPORTED_METHODS FIND_PACKAGE +) diff --git a/infra/tools/beman-submodule/README.md b/infra/tools/beman-submodule/README.md new file mode 100644 index 00000000..36883ada --- /dev/null +++ b/infra/tools/beman-submodule/README.md @@ -0,0 +1,63 @@ +# beman-submodule + + + +## What is this script? + +`beman-submodule` provides some of the features of `git submodule`, adding child git +repositories to a parent git repository, but unlike with `git submodule`, the entire child +repo is directly checked in, so only maintainers, not users, need to run this script. The +command line interface mimics `git submodule`'s. + +## How do I add a beman submodule to my repository? + +The first beman submodule you should add is this repository, `infra/`, which you can +bootstrap by running: + + +```sh +curl -s https://raw.githubusercontent.com/bemanproject/infra/refs/heads/main/tools/beman-submodule/beman-submodule | python3 - add https://github.com/bemanproject/infra.git +``` + +Once that's added, you can run the script from `infra/tools/beman-submodule/beman-submodule`. + +## How do I update a beman submodule to the latest trunk? + +You can run `beman-submodule update --remote` to update all beman submodule to latest +trunk, or e.g. `beman-submodule update --remote infra` to update only a specific one. + +## How does it work under the hood? + +Along with the files from the child repository, it creates a dotfile called +`.beman_submodule`, which looks like this: + +```ini +[beman_submodule] +remote=https://github.com/bemanproject/infra.git +commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77 +``` + +## How do I update a beman submodule to a specific commit or change the remote URL? + +You can edit the corresponding lines in the `.beman_submodule` file and run +`beman-submodule update` to update the state of the beman submodule to the new +`.beman_submodule` settings. + +## How can I make CI ensure that my beman submodules are in a valid state? + +Add this job to your CI workflow: + +```yaml + beman-submodule-test: + runs-on: ubuntu-latest + name: "Check beman submodules for consistency" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: beman submodule consistency check + run: | + (set -o pipefail; ./infra/tools/beman-submodule/beman-submodule status | grep -qvF '+') +``` + +This will fail if the contents of any beman submodule don't match what's specified in the +`.beman_submodule` file. diff --git a/infra/tools/beman-submodule/beman-submodule b/infra/tools/beman-submodule/beman-submodule new file mode 100755 index 00000000..66cb96e1 --- /dev/null +++ b/infra/tools/beman-submodule/beman-submodule @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import argparse +import configparser +import filecmp +import glob +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def directory_compare( + reference: str | Path, actual: str | Path, ignore, allow_untracked_files: bool): + reference, actual = Path(reference), Path(actual) + + compared = filecmp.dircmp(reference, actual, ignore=ignore) + if (compared.left_only + or (compared.right_only and not allow_untracked_files) + or compared.diff_files): + return False + for common_dir in compared.common_dirs: + path1 = reference / common_dir + path2 = actual / common_dir + if not directory_compare(path1, path2, ignore, allow_untracked_files): + return False + return True + +class BemanSubmodule: + def __init__( + self, dirpath: str | Path, remote: str, commit_hash: str, + allow_untracked_files: bool): + self.dirpath = Path(dirpath) + self.remote = remote + self.commit_hash = commit_hash + self.allow_untracked_files = allow_untracked_files + +def parse_beman_submodule_file(path): + config = configparser.ConfigParser() + read_result = config.read(path) + def fail(): + raise Exception(f'Failed to parse {path} as a .beman_submodule file') + if not read_result: + fail() + if not 'beman_submodule' in config: + fail() + if not 'remote' in config['beman_submodule']: + fail() + if not 'commit_hash' in config['beman_submodule']: + fail() + allow_untracked_files = config.getboolean( + 'beman_submodule', 'allow_untracked_files', fallback=False) + return BemanSubmodule( + Path(path).resolve().parent, + config['beman_submodule']['remote'], + config['beman_submodule']['commit_hash'], + allow_untracked_files) + +def get_beman_submodule(path: str | Path): + beman_submodule_filepath = Path(path) / '.beman_submodule' + + if beman_submodule_filepath.is_file(): + return parse_beman_submodule_file(beman_submodule_filepath) + else: + return None + +def find_beman_submodules_in(path): + path = Path(path) + assert path.is_dir() + + result = [] + for dirpath, _, filenames in path.walk(): + if '.beman_submodule' in filenames: + result.append(parse_beman_submodule_file(dirpath / '.beman_submodule')) + return sorted(result, key=lambda module: module.dirpath) + +def cwd_git_repository_path(): + process = subprocess.run( + ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, + check=False) + if process.returncode == 0: + return process.stdout.strip() + elif "fatal: not a git repository" in process.stderr: + return None + else: + raise Exception("git rev-parse --show-toplevel failed") + +def clone_beman_submodule_into_tmpdir(beman_submodule, remote): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', beman_submodule.remote, tmpdir.name], capture_output=True, + check=True) + if not remote: + subprocess.run( + ['git', '-C', tmpdir.name, 'reset', '--hard', beman_submodule.commit_hash], + capture_output=True, check=True) + return tmpdir + +def get_paths(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + paths = set(glob.glob('*', root_dir=Path(tmpdir.name), include_hidden=True)) + paths.remove('.git') + return paths + +def beman_submodule_status(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + if directory_compare( + tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git'], + beman_submodule.allow_untracked_files): + status_character=' ' + else: + status_character='+' + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + relpath = Path(beman_submodule.dirpath).relative_to(Path(parent_repo_path)) + return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath) + +def beman_submodule_update(beman_submodule, remote): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote) + tmp_path = Path(tmpdir.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmp_path) + + if beman_submodule.allow_untracked_files: + for path in get_paths(beman_submodule): + path2 = Path(beman_submodule.dirpath) / path + if Path(path2).is_dir(): + shutil.rmtree(path2) + elif Path(path2).is_file(): + os.remove(path2) + else: + shutil.rmtree(beman_submodule.dirpath) + + submodule_path = tmp_path / '.beman_submodule' + with open(submodule_path, 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={beman_submodule.remote}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if beman_submodule.allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmp_path / '.git') + shutil.copytree(tmp_path, beman_submodule.dirpath, dirs_exist_ok=True) + +def update_command(remote, path): + if not path: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules = [beman_submodule] + for beman_submodule in beman_submodules: + beman_submodule_update(beman_submodule, remote) + +def add_command(repository, path, allow_untracked_files): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) + repository_name = os.listdir(tmpdir.name)[0] + if not path: + path = Path(repository_name) + else: + path = Path(path) + if not allow_untracked_files and path.exists(): + raise Exception(f'{path} exists') + path.mkdir(exist_ok=allow_untracked_files) + tmpdir_repo = Path(tmpdir.name) / repository_name + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir_repo) + with open(tmpdir_repo / '.beman_submodule', 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={repository}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmpdir_repo /'.git') + shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) + +def status_command(paths): + if not paths: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodules = [] + for path in paths: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules.append(beman_submodule) + for beman_submodule in beman_submodules: + print(beman_submodule_status(beman_submodule)) + +def get_parser(): + parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool') + subparsers = parser.add_subparsers(dest='command', help='available commands') + parser_update = subparsers.add_parser('update', help='update beman_submodules') + parser_update.add_argument( + '--remote', action='store_true', + help='update a beman_submodule to its latest from upstream') + parser_update.add_argument( + 'beman_submodule_path', nargs='?', + help='relative path to the beman_submodule to update') + parser_add = subparsers.add_parser('add', help='add a new beman_submodule') + parser_add.add_argument('repository', help='git repository to add') + parser_add.add_argument( + 'path', nargs='?', help='path where the repository will be added') + parser_add.add_argument( + '--allow-untracked-files', action='store_true', + help='the beman_submodule will not occupy the subdirectory exclusively') + parser_status = subparsers.add_parser( + 'status', help='show the status of beman_submodules') + parser_status.add_argument('paths', nargs='*') + return parser + +def parse_args(args): + return get_parser().parse_args(args); + +def usage(): + return get_parser().format_help() + +def run_command(args): + if args.command == 'update': + update_command(args.remote, args.beman_submodule_path) + elif args.command == 'add': + add_command(args.repository, args.path, args.allow_untracked_files) + elif args.command == 'status': + status_command(args.paths) + else: + raise Exception(usage()) + +def check_for_git(path): + env = os.environ.copy() + if path is not None: + env["PATH"] = path + return shutil.which("git", path=env.get("PATH")) is not None + +def main(): + try: + if not check_for_git(None): + raise Exception('git not found in PATH') + args = parse_args(sys.argv[1:]) + run_command(args) + except Exception as e: + print("Error:", e, file=sys.stderr) + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/infra/tools/beman-submodule/test/test_beman_submodule.py b/infra/tools/beman-submodule/test/test_beman_submodule.py new file mode 100644 index 00000000..600fc070 --- /dev/null +++ b/infra/tools/beman-submodule/test/test_beman_submodule.py @@ -0,0 +1,539 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import glob +import os +import pytest +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path + +# https://stackoverflow.com/a/19011259 +import types +import importlib.machinery +loader = importlib.machinery.SourceFileLoader( + 'beman_submodule', + str(Path(__file__).parent.resolve().parent / 'beman-submodule')) +beman_submodule = types.ModuleType(loader.name) +loader.exec_module(beman_submodule) + +def create_test_git_repository(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + def make_commit(a_txt_contents): + with open(tmp_path / 'a.txt', 'w') as f: + f.write(a_txt_contents) + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + make_commit('A') + make_commit('a') + return tmpdir + +def create_test_git_repository2(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'a.txt', 'w') as f: + f.write('a') + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + os.remove(tmp_path / 'a.txt') + subprocess.run( + ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'b.txt', 'w') as f: + f.write('b') + subprocess.run( + ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + return tmpdir + +def test_directory_compare(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as dir_a, \ + tempfile.TemporaryDirectory() as dir_b: + path_a = Path(dir_a) + path_b = Path(dir_b) + + create_dir_structure(path_a) + create_dir_structure(path_b) + + assert beman_submodule.directory_compare(dir_a, dir_b, [], False) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], False) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) + +def test_directory_compare_untracked_files(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as reference, \ + tempfile.TemporaryDirectory() as actual: + path_a = Path(reference) + path_b = Path(actual) + + create_dir_structure(path_a) + create_dir_structure(path_b) + (path_b / 'c.txt').touch() + + assert beman_submodule.directory_compare(reference, actual, [], True) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], True) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) + +def test_parse_beman_submodule_file(): + def valid_file(): + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + module = beman_submodule.parse_beman_submodule_file(tmpfile.name) + assert module.dirpath == Path(tmpfile.name).resolve().parent + assert module.remote == 'git@github.com:bemanproject/infra.git' + assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' + valid_file() + def invalid_file_missing_remote(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_remote() + def invalid_file_missing_commit_hash(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_commit_hash() + def invalid_file_wrong_section(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[invalid]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_wrong_section() + +def test_get_beman_submodule(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + assert beman_submodule.get_beman_submodule('foo') + os.remove('foo/.beman_submodule') + assert not beman_submodule.get_beman_submodule('foo') + os.chdir(original_cwd) + +def test_find_beman_submodules_in(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' + assert beman_submodules[0].remote == tmpdir.name + assert beman_submodules[0].commit_hash == sha + assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' + assert beman_submodules[1].remote == tmpdir.name + assert beman_submodules[1].commit_hash == sha + os.chdir(original_cwd) + +def test_cwd_git_repository_path(): + original_cwd = Path.cwd() + tmpdir = tempfile.TemporaryDirectory() + os.chdir(tmpdir.name) + assert not beman_submodule.cwd_git_repository_path() + subprocess.run(['git', 'init']) + assert beman_submodule.cwd_git_repository_path() == tmpdir.name + os.chdir(original_cwd) + +def test_clone_beman_submodule_into_tmpdir(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + module.commit_hash = sha + tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) + assert not beman_submodule.directory_compare( + tmpdir.name, tmpdir3.name, ['.git'], False) + tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) + subprocess.run( + ['git', 'reset', '--hard', sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) + os.chdir(original_cwd) + +def test_get_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + assert beman_submodule.get_paths(module) == set(['a.txt']) + os.chdir(original_cwd) + +def test_beman_submodule_status(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: + f.write('b') + assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + os.chdir(original_cwd) + +def test_update_command_no_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_with_path(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() + shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_untracked_files(): + tmpdir = create_test_git_repository2() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd(); + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') + beman_submodule.update_command(False, 'foo') + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + beman_submodule.update_command(True, 'foo') + assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_add_command(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' + os.chdir(original_cwd) + +def test_add_command_untracked_files(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.add_command(tmpdir.name, 'foo', True) + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_status_command_no_paths(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command([]) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_status_command_with_path(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command(['bar']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + os.chdir(original_cwd) + +def test_status_command_untracked_files(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', True) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.status_command(['foo']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_check_for_git(): + tmpdir = tempfile.TemporaryDirectory() + assert not beman_submodule.check_for_git(tmpdir.name) + fake_git_path = Path(tmpdir.name) / 'git' + with open(fake_git_path, 'w'): + pass + os.chmod(fake_git_path, stat.S_IRWXU) + assert beman_submodule.check_for_git(tmpdir.name) + +def test_parse_args(): + def plain_update(): + args = beman_submodule.parse_args(['update']) + assert args.command == 'update' + assert not args.remote + assert not args.beman_submodule_path + plain_update() + def update_remote(): + args = beman_submodule.parse_args(['update', '--remote']) + assert args.command == 'update' + assert args.remote + assert not args.beman_submodule_path + update_remote() + def update_path(): + args = beman_submodule.parse_args(['update', 'infra/']) + assert args.command == 'update' + assert not args.remote + assert args.beman_submodule_path == 'infra/' + update_path() + def update_path_remote(): + args = beman_submodule.parse_args(['update', '--remote', 'infra/']) + assert args.command == 'update' + assert args.remote + assert args.beman_submodule_path == 'infra/' + update_path_remote() + def plain_add(): + args = beman_submodule.parse_args(['add', 'git@github.com:bemanproject/infra.git']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert not args.path + plain_add() + def add_path(): + args = beman_submodule.parse_args( + ['add', 'git@github.com:bemanproject/infra.git', 'infra/']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert args.path == 'infra/' + add_path() + def plain_status(): + args = beman_submodule.parse_args(['status']) + assert args.command == 'status' + assert args.paths == [] + plain_status() + def status_one_module(): + args = beman_submodule.parse_args(['status', 'infra/']) + assert args.command == 'status' + assert args.paths == ['infra/'] + status_one_module() + def status_multiple_modules(): + args = beman_submodule.parse_args(['status', 'infra/', 'foobar/']) + assert args.command == 'status' + assert args.paths == ['infra/', 'foobar/'] + status_multiple_modules() diff --git a/infra/tools/beman-tidy/.markdownlintignore b/infra/tools/beman-tidy/.markdownlintignore new file mode 100644 index 00000000..2bfa6a4d --- /dev/null +++ b/infra/tools/beman-tidy/.markdownlintignore @@ -0,0 +1 @@ +tests/ diff --git a/infra/tools/beman-tidy/.python-version b/infra/tools/beman-tidy/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/infra/tools/beman-tidy/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/infra/tools/beman-tidy/README.md b/infra/tools/beman-tidy/README.md new file mode 100644 index 00000000..c01045e8 --- /dev/null +++ b/infra/tools/beman-tidy/README.md @@ -0,0 +1,160 @@ +# beman-tidy: The Codebase Bemanification Tool + + + +## Description + +`beman-tidy` is a tool used to check and apply +[The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). + +Purpose: The tool is used to `check` (`--dry-run`) and `apply` (`--fix-inplace`) the Beman Standard to a repository. +Note: `2025-06-07`: In order to make the best and quickly use of the tool in the entire organization, most of the +checks will not support the `--fix-inplace` flag in the first iteration. + +## Installation + +- The current recommended workflow relies on [Astral's uv](https://docs.astral.sh/uv/) +- However, we provide a [PEP 751](https://peps.python.org/pep-0751/) `pylock.toml`, so don't feel forced to use uv +- You can use beman-tidy as a pre-commit hook or install it on your system using `pipx` + +```shell +uv build +pipx install path/to/wheel +``` + +
+beman-tidy: Full example - build and install + +```shell +$ uv build +Building source distribution... +Building wheel from source distribution... +Successfully built dist/beman_tidy-0.1.0.tar.gz +Successfully built dist/beman_tidy-0.1.0-py3-none-any.whl + +$ pipx install dist/beman_tidy-0.1.0-py3-none-any.whl +Installing to existing venv 'beman-tidy' + installed package beman-tidy 0.1.0, installed using Python 3.13.4 + These apps are now globally available + - beman-tidy +... +You will need to open a new terminal or re-login for the PATH changes to take effect. Alternatively, you can source your shell's config file with e.g. 'source ~/.bashrc'. + +$ beman-tidy --help +usage: beman-tidy [-h] [--fix-inplace | --no-fix-inplace] [--verbose | --no-verbose] [--checks CHECKS] repo_path +... +``` + +
+ +## Usage + +- Display help: + +```shell +$ uv run beman-tidy --help +usage: beman-tidy [-h] [--fix-inplace | --no-fix-inplace] [--verbose | --no-verbose] [--require-all | --no-require-all] [--checks CHECKS] repo_path + +positional arguments: + repo_path path to the repository to check + +options: + -h, --help show this help message and exit + --fix-inplace, --no-fix-inplace + Try to automatically fix found issues + --verbose, --no-verbose + print verbose output for each check + --require-all, --no-require-all + all checks are required regardless of the check type (e.g., RECOMMENDATION becomes REQUIREMENT) + --checks CHECKS array of checks to run +``` + +- Run beman-tidy on the exemplar repository **(default: dry-run mode)** + +```shell +# dry-run, require-all, non-verbose +$ uv run beman-tidy /path/to/exemplar --require-all +Summary REQUIREMENT: 1 checks PASSED, 0 checks FAILED, 4 skipped (NOT implemented). +Summary RECOMMENDATION: 2 checks PASSED, 1 checks FAILED, 35 skipped (NOT implemented). + +Coverage REQUIREMENT: 100.0% (1/1 checks passed). +Coverage RECOMMENDATION: 66.67% (2/3 checks passed). + +# dry-run, non-require-all, non-verbose +$ uv run beman-tidy /path/to/exemplar +Summary REQUIREMENT: 1 checks PASSED, 0 checks FAILED, 4 skipped (NOT implemented). +Summary RECOMMENDATION: 2 checks PASSED, 1 checks FAILED, 35 skipped (NOT implemented). + +Coverage REQUIREMENT: 100.0% (1/1 checks passed). +Note: RECOMMENDATIONs are not included (--require-all NOT set). + +``` + +or verbose mode: + +```shell +# dry-run, require-all, verbose mode - no errors +$ uv run beman-tidy /path/to/exemplar --require-all --verbose +beman-tidy pipeline started ... + +Running check [RECOMMENDATION][README.TITLE] ... + check [RECOMMENDATION][README.TITLE] ... PASSED + +Running check [REQUIREMENT][README.BADGES] ... + check [REQUIREMENT][README.BADGES] ... PASSED + +Running check [RECOMMENDATION][README.LIBRARY_STATUS] ... + check [RECOMMENDATION][README.LIBRARY_STATUS] ... PASSED + +Running check [RECOMMENDATION][DIRECTORY.SOURCES] ... +[WARNING ][DIRECTORY.SOURCES ]: The directory '/Users/dariusn/dev/dn/git/Beman/exemplar/src/beman/exemplar' does not exist. + check [RECOMMENDATION][DIRECTORY.SOURCES] ... FAILED + + +beman-tidy pipeline finished. + +Summary REQUIREMENT: 1 checks PASSED, 0 checks FAILED, 4 skipped (NOT implemented). +Summary RECOMMENDATION: 2 checks PASSED, 1 checks FAILED, 35 skipped (NOT implemented). + +Coverage REQUIREMENT: 100.0% (1/1 checks passed). +Coverage RECOMMENDATION: 66.67% (2/3 checks passed). +``` + +```shell +# dry-run, require-all, verbose mode - no errors +$ uv run beman-tidy /path/to/exemplar --require-all --verbose +beman-tidy pipeline started ... + +Running check [RECOMMENDATION][README.TITLE] ... + check [RECOMMENDATION][README.TITLE] ... PASSED + +Running check [REQUIREMENT][README.BADGES] ... + check [REQUIREMENT][README.BADGES] ... PASSED + +Running check [RECOMMENDATION][README.LIBRARY_STATUS] ... + check [RECOMMENDATION][README.LIBRARY_STATUS] ... PASSED + +Running check [RECOMMENDATION][DIRECTORY.SOURCES] ... + check [RECOMMENDATION][DIRECTORY.SOURCES] ... PASSED + + +beman-tidy pipeline finished. + +Summary REQUIREMENT: 1 checks PASSED, 0 checks FAILED, 4 skipped (NOT implemented). +Summary RECOMMENDATION: 3 checks PASSED, 0 checks FAILED, 35 skipped (NOT implemented). + +Coverage REQUIREMENT: 100.0% (1/1 checks passed). +Coverage RECOMMENDATION: 100.0% (3/3 checks passed). +``` + +- Run beman-tidy on the exemplar repository (fix issues in-place): + +```shell +uv run beman-tidy path/to/exemplar --fix-inplace --verbose +``` + +## beman-tidy Development + +Please refer to the [Beman Tidy Development Guide](./docs/dev-guide.md) for more details. diff --git a/infra/tools/beman-tidy/beman_tidy/.beman-standard.yml b/infra/tools/beman-tidy/beman_tidy/.beman-standard.yml new file mode 100644 index 00000000..47519973 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/.beman-standard.yml @@ -0,0 +1,163 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# TODO: 2025-06-07: This file is a partial stable snapshot of the standard. +# TODOs and placeholders will be solved in follow-up PRs when implementing the checks. + +# LICENSE +LICENSE.APPROVED: + - type: REQUIREMENT + - licenses: + - apache-v2: + - spdx: "Apache License v2.0 with LLVM Exceptions" + - path: "docs/licenses/apache-v2.txt" + - boost-v1: + - spdx: "Boost Software License 1.0" + - path: "docs/licenses/boost-v1.txt" + - mit: + - spdx: "The MIT License" + - path: "docs/licenses/mit.txt" +LICENSE.APACHE_LLVM: + - type: RECOMMENDATION +LICENSE.CRITERIA: + - type: REQUIREMENT + +# GENERAL +LIBRARY.NAMES: + - type: RECOMMENDATION + - regex: +REPOSITORY.NAME: + - type: RECOMMENDATION + - regex: +REPOSITORY.CODEOWNERS: + - type: REQUIREMENT + - default_group: "@bemanproject/core-reviewers" +REPOSITORY.DISALLOW_GIT_SUBMODULES: + - type: RECOMMENDATION + +# RELEASE +RELEASE.GITHUB: + - type: REQUIREMENT +RELEASE.NOTES: + - type: RECOMMENDATION +RELEASE.GODBOLT_TRUNK_VERSION: + - type: RECOMMENDATION +# TOP LEVEL +TOPLEVEL.CMAKE: + - type: REQUIREMENT + - value: CMakeLists.txt +TOPLEVEL.LICENSE: + - type: REQUIREMENT + - file_name: LICENSE +TOPLEVEL.README: + - type: REQUIREMENT + - file_name: README.md + +# README +README.TITLE: + - type: RECOMMENDATION +README.BADGES: + - type: REQUIREMENT + - values: [ + "![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)", + "![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_production_ready_api_may_undergo_changes.svg)", + "![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_production_ready_stable_api.svg)", + "![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_retired.svg)" + ] +README.PURPOSE: + - type: RECOMMENDATION +README.IMPLEMENTS: + - type: RECOMMENDATION +README.LIBRARY_STATUS: + - type: REQUIREMENT + - values: [ + "**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use)", + "**Status**: [Production ready. API may undergo changes.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-api-may-undergo-changes)", + "**Status**: [Production ready. Stable API.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-stable-api)", + "**Status**: [Retired. No longer maintained or actively developed.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#retired-no-longer-maintained-or-actively-developed)", + ] + +# CMAKE +CMAKE.DEFAULT: + - type: RECOMMENDATION +CMAKE.USE_FETCH_CONTENT: + - type: RECOMMENDATION +CMAKE.PROJECT_NAME: + - type: RECOMMENDATION +CMAKE.PASSIVE_PROJECTS: + - type: RECOMMENDATION +CMAKE.LIBRARY_NAME: + - type: RECOMMENDATION + - regex: +CMAKE.LIBRARY_ALIAS: + - type: REQUIREMENT + - regex: +CMAKE.TARGET_NAMES: + - type: RECOMMENDATION + - regex: +CMAKE.PASSIVE_TARGETS: + - type: REQUIREMENT + - regex: +CMAKE.CONFIG: + - type: REQUIREMENT +CMAKE.SKIP_TESTS: + - type: RECOMMENDATION + - regex: +CMAKE.SKIP_EXAMPLES: + - type: RECOMMENDATION + - regex: +CMAKE.AVOID_PASSTHROUGHS: + - type: RECOMMENDATION + +# DIRECTORY +DIRECTORY.INTERFACE_HEADERS: + - type: REQUIREMENT + - directory_name: include + - regex: +DIRECTORY.IMPLEMENTATION_HEADERS: + - type: REQUIREMENT + - regex: +DIRECTORY.SOURCES: + - type: REQUIREMENT + - directory_name: src + - regex: +DIRECTORY.TESTS: + - type: REQUIREMENT + - directory_name: tests + - regex: +DIRECTORY.EXAMPLES: + - type: REQUIREMENT + - directory_name: examples + - regex: +DIRECTORY.DOCS: + - type: REQUIREMENT + - directory_name: docs + - regex: +DIRECTORY.PAPERS: + - type: REQUIREMENT + - directory_name: papers + +# FILE +FILE.CPP_NAMES: + - type: RECOMMENDATION + - regex: +FILE.TEST_NAMES: + - type: REQUIREMENT + - regex: +FILE.LICENSE_ID: + - type: REQUIREMENT + - regex: [ + "// SPDX-License-Identifier: ", + "# SPDX-License-Identifier: ", + "" + ] +FILE.COPYRIGHT: + - type: RECOMMENDATION + - regex: + +# CPP +CPP.NAMESPACE: + - type: RECOMMENDATION +CPP.NO_FLAG_FORKING: + - type: REQUIREMENT +CPP.EXTENSION_IDENTIFIERS: + - type: RECOMMENDATION diff --git a/infra/tools/beman-tidy/beman_tidy/__init__.py b/infra/tools/beman-tidy/beman_tidy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/cli.py b/infra/tools/beman-tidy/beman_tidy/cli.py new file mode 100755 index 00000000..de648110 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/cli.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import argparse +import sys + +from beman_tidy.lib.utils.git import get_repo_info, load_beman_standard_config +from beman_tidy.lib.pipeline import run_checks_pipeline + + +def parse_args(): + """ + Parse the CLI arguments. + """ + + parser = argparse.ArgumentParser() + parser.add_argument("repo_path", help="path to the repository to check", type=str) + parser.add_argument( + "--fix-inplace", + help="Try to automatically fix found issues", + action=argparse.BooleanOptionalAction, + default=False, + ) + parser.add_argument( + "--verbose", + help="print verbose output for each check", + action=argparse.BooleanOptionalAction, + default=False, + ) + parser.add_argument( + "--require-all", + help="all checks are required regardless of their type (e.g., all RECOMMENDATIONs become REQUIREMENTs)", + action=argparse.BooleanOptionalAction, + default=False, + ) + parser.add_argument( + "--checks", help="array of checks to run", type=str, default=None + ) + args = parser.parse_args() + + args.repo_info = get_repo_info(args.repo_path) + args.checks = args.checks.split(",") if args.checks else None + + return args + + +def main(): + """ + The beman-tidy main entry point. + """ + args = parse_args() + + beman_standard_check_config = load_beman_standard_config() + if not beman_standard_check_config or len(beman_standard_check_config) == 0: + print("Failed to download the beman standard. STOP.") + return + + checks_to_run = ( + [check for check in beman_standard_check_config] + if args.checks is None + else args.checks + ) + + failed_checks = run_checks_pipeline( + checks_to_run, args, beman_standard_check_config + ) + sys.exit(failed_checks) + + +if __name__ == "__main__": + main() diff --git a/infra/tools/beman-tidy/beman_tidy/lib/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/base/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py new file mode 100644 index 00000000..f3431ab6 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from abc import ABC, abstractmethod +from pathlib import Path + +from ..system.registry import get_beman_standard_check_name_by_class + + +class BaseCheck(ABC): + """ + Base class for checks. + This class is not meant to be used directly, it's meant to be subclassed. + e.g., check for repository name, check for changelog, check for license, etc. + """ + + def __init__(self, repo_info, beman_standard_check_config, name=None): + """ + Create a new check instance. + """ + + # check name - e.g. "README.TITLE" + self.name = ( + name + if name is not None + else get_beman_standard_check_name_by_class(self.__class__) + ) + assert self.name is not None, ( + f"Check name not found for class: {self.__class__.__name__}" + ) + + # save the check config + self.config = beman_standard_check_config[self.name] + + # set type - e.g. "REQUIREMENT" or "RECOMMENDATION" + self.type = beman_standard_check_config[self.name]["type"] + assert self.type in ["REQUIREMENT", "RECOMMENDATION"], ( + f"Invalid check type: {self.type} for check = {self.name}." + ) + + # set full text body - e.g. "The README.md should begin ..." + self.full_text_body = beman_standard_check_config[self.name]["full_text_body"] + assert self.full_text_body is not None + + # set log level - e.g. "ERROR" or "WARNING" + self.log_level = "ERROR" if self.type == "REQUIREMENT" else "WARNING" + self.log_enabled = False + + # set repo info + self.repo_info = repo_info + assert "name" in repo_info + self.repo_name = repo_info["name"] + assert "top_level" in repo_info + self.repo_path = Path(repo_info["top_level"]) + assert self.repo_path is not None + self.library_name = f"beman.{self.repo_name}" + assert self.library_name is not None + + # set beman library maturity model + beman_library_maturity_model = beman_standard_check_config[ + "README.LIBRARY_STATUS" + ] + assert "values" in beman_library_maturity_model + assert len(beman_library_maturity_model["values"]) == 4 + self.beman_library_maturity_model = beman_library_maturity_model["values"] + + def pre_check(self): + """ + Pre-checks if this rule is properly initialized. + Usually, this is internal use only. + + Note: This method is internally called by the framework. + """ + if self.name is None: + self.log("The name is not set.") + return False + + if self.repo_name is None: + self.log(f"The repo_name is not set for check = {self.name}.") + return False + + if not self.repo_path: + self.log(f"The repo_path is not set for check = {self.name}.") + return False + + return True + + @abstractmethod + def check(self): + """ + Checks if the Beman Standard check is already applied. + - If it's applied, this method should return True. + - Otherwise, it returns False and self.fix() must be able to fix the issue. + + Note: This methods must be always implemented. + """ + pass + + @abstractmethod + def fix(self): + """ + Fixes the issue if the Beman Standard is not applied. + - If check already applied, this method is a no-op and should return True. + - Otherwise, it will try to apply the check inplace. Returns the status of the fix attempt. + + Note: The subclasses might not implement more than a stub if the fix method + is too difficult to implement or does not make sense. + """ + pass + + def log(self, message, enabled=True): + """ + Logs a message with the check's log level. + e.g. [WARN][REPOSITORY.NAME]: The name "${name}" should be snake_case.' + e.g. [ERROR][TOPLEVEL.CMAKE]: Missing top level CMakeLists.txt.' + """ + + if self.log_enabled and enabled: + print(f"[{self.log_level:<15}][{self.name:<25}]: {message}") diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/base/directory_base_check.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/directory_base_check.py new file mode 100644 index 00000000..d43a39fe --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/directory_base_check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from abc import abstractmethod +from pathlib import Path + +from .base_check import BaseCheck + + +class DirectoryBaseCheck(BaseCheck): + """ + Base class for checks that operate on a directory. + """ + + def __init__(self, repo_info, beman_standard_check_config, relative_path): + super().__init__(repo_info, beman_standard_check_config) + + # set path - e.g. "src/beman/exemplar" + self.path = self.repo_path / relative_path + + def pre_check(self): + """ + Override. + Pre-checks if the directory exists and is not empty. + """ + if not super().pre_check(): + return False + + if self.path is None: + self.log("The path is not set.") + return False + + if not self.path.exists(): + self.log(f"The directory '{self.path}' does not exist.") + return False + + if self.is_empty(): + self.log(f"The directory '{self.path}' is empty.") + return False + + return True + + @abstractmethod + def check(self): + """ + Override this method, make it abstract because this is style an abstract class. + """ + pass + + @abstractmethod + def fix(self): + """ + Override this method, make it abstract because this is style an abstract class. + """ + pass + + def read(self) -> list[Path]: + """ + Read the directory content. + """ + try: + return list(self.path.iterdir()) + except Exception: + return [] + + def is_empty(self): + """ + Check if the directory is empty. + """ + return len(self.read()) == 0 diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/base/file_base_check.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/file_base_check.py new file mode 100644 index 00000000..bea8eeee --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/base/file_base_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from abc import abstractmethod +import re + +from .base_check import BaseCheck + + +class FileBaseCheck(BaseCheck): + """ + Base class for checks that operate on a file. + """ + + def __init__(self, repo_info, beman_standard_check_config, relative_path): + super().__init__(repo_info, beman_standard_check_config) + + # set path - e.g. "README.md" + self.path = self.repo_path / relative_path + + def pre_check(self): + """ + Override. + Pre-checks if the file exists and is not empty. + """ + if not super().pre_check(): + return False + + if self.path is None: + self.log("The path is not set.") + return False + + if not self.path.exists(): + self.log(f"The file '{self.path}' does not exist.") + return False + + if self.is_empty(): + self.log(f"The file '{self.path}' is empty.") + return False + + return True + + @abstractmethod + def check(self): + """ + Override this method, make it abstract because this is style an abstract class. + """ + pass + + @abstractmethod + def fix(self): + """ + Override this method, make it abstract because this is style an abstract class. + """ + pass + + def read(self): + """ + Read the file content. + """ + try: + with open(self.path, "r") as file: + return file.read() + except Exception: + return "" + + def read_lines(self): + """ + Read the file content as lines. + """ + try: + with open(self.path, "r") as file: + return file.readlines() + except Exception: + return [] + + def read_lines_strip(self): + """ + Read the file content as lines and strip them. + """ + return [line.strip() for line in self.read_lines()] + + def write(self, content): + """ + Write the content to the file. + """ + try: + with open(self.path, "w") as file: + file.write(content) + except Exception as e: + self.log(f"Error writing the file '{self.path}': {e}") + + def write_lines(self, lines): + """ + Write the lines to the file. + """ + self.write("\n".join(lines)) + + def replace_line(self, line_number, new_line): + """ + Replace the line at the given line number with the new line. + """ + lines = self.read_lines() + lines[line_number] = new_line + self.write_lines(lines) + + def is_empty(self): + """ + Check if the file is empty. + """ + return len(self.read()) == 0 + + def has_content(self, content_to_match): + """ + Check if the file contains the given content (literal string match). + """ + readme_content = self.read() + if not readme_content or len(readme_content) == 0: + return False + return re.search(re.escape(content_to_match), readme_content) is not None diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cmake.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cmake.py new file mode 100644 index 00000000..49d9d24c --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cmake.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from ..base.file_base_check import FileBaseCheck + +# [CMAKE.*] checks category. +# All checks in this file extend the CMakeBaseCheck class. +# +# Note: CMakeBaseCheck is not a registered check! + + +class CMakeBaseCheck(FileBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "CMakeLists.txt") + + +# TODO CMAKE.DEFAULT + + +# TODO CMAKE.USE_FETCH_CONTENT + + +# TODO CMAKE.PROJECT_NAME + + +# TODO CMAKE.PASSIVE_PROJECTS + + +# TODO CMAKE.LIBRARY_NAME + + +# TODO CMAKE.LIBRARY_ALIAS + + +# TODO CMAKE.TARGET_NAMES + + +# TODO CMAKE.PASSIVE_TARGETS + + +# TODO CMAKE.SKIP_TESTS + + +# TODO CMAKE.SKIP_EXAMPLES + + +# TODO CMAKE.AVOID_PASSTHROUGHS diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cpp.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cpp.py new file mode 100644 index 00000000..6868f23e --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cpp.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# [CPP.*] checks category. + + +# TODO CPP.NAMESPACE + + +# TODO CPP.NO_FLAG_FORKING + + +# TODO CPP.EXTENSION_IDENTIFIERS diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py new file mode 100644 index 00000000..d533bb3a --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from ..base.directory_base_check import DirectoryBaseCheck +from ..system.registry import register_beman_standard_check + + +# [DIRECTORY.*] checks category. +class BemanTreeDirectoryCheck(DirectoryBaseCheck): + """ + Check if the directory tree is a Beman tree. + """ + + def __init__(self, repo_info, beman_standard_check_config, prefix_path): + super().__init__( + repo_info, + beman_standard_check_config, + f"{prefix_path}/beman/{repo_info['name']}", + ) + + +# TODO DIRECTORY.INTERFACE_HEADERS + + +# TODO DIRECTORY.IMPLEMENTATION_HEADERS + + +# TODO DIRECTORY.SOURCES +@register_beman_standard_check("DIRECTORY.SOURCES") +class DirectorySourcesCheck(BemanTreeDirectoryCheck): + """ + Check if the sources directory is src/beman/. + """ + + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "src") + + def check(self): + return self.pre_check() + + def fix(self): + """ + TODO: Implement the fix. + """ + pass + + +# TODO DIRECTORY.TESTS + + +# TODO DIRECTORY.EXAMPLES + + +# TODO DIRECTORY.DOCS + + +# TODO DIRECTORY.PAPERS diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/file.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/file.py new file mode 100644 index 00000000..b66e8bec --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/file.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# [FILE.*] checks category. +# All checks in this file extend the FileBaseCheck class. +# +# Note: FileBaseCheck is not a registered check! + + +# TODO FILE.NAMES + + +# TODO FILE.TEST_NAMES + + +# TODO FILE.LICENSE_ID + + +# TODO FILE.COPYRIGHT diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py new file mode 100644 index 00000000..d0847539 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# TODO LIBRARY.NAMES +# TODO REPOSITORY.NAME +# TODO REPOSITORY.CODEOWNERS +# TODO REPOSITORY.DISALLOW_GIT_SUBMODULES diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py new file mode 100644 index 00000000..6eb75ab5 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from ..base.file_base_check import FileBaseCheck + +# [LICENSE.*] checks category. +# All checks in this file extend the LicenseBaseCheck class. +# +# Note: LicenseBaseCheck is not a registered check! + + +class LicenseBaseCheck(FileBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "LICENSE") + + +# TODO LICENSE.APPROVED + + +# TODO LICENSE.APACHE_LLVM + + +# TODO LICENSE.CRITERIA diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py new file mode 100644 index 00000000..fcbead60 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re + +from ..base.file_base_check import FileBaseCheck +from ..system.registry import register_beman_standard_check + +# [README.*] checks category. +# All checks in this file extend the ReadmeBaseCheck class. +# +# Note: ReadmeBaseCheck is not a registered check! + + +class ReadmeBaseCheck(FileBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "README.md") + + +@register_beman_standard_check("README.TITLE") +class ReadmeTitleCheck(ReadmeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + lines = self.read_lines_strip() + first_line = lines[0] + + # Match the pattern "# [: ]" + regex = rf"^# {re.escape(self.library_name)}: (.*)$" # noqa: F541 + if not re.match(regex, first_line): + self.log( + f"The first line of the file '{self.path}' is invalid. It should start with '# {self.library_name}: '." + ) + return False + + return True + + def fix(self): + """ + Fix the issue if the Beman Standard is not applied. + """ + new_title_line = f"# {self.library_name}: TODO Short Description" + self.replace_line(0, new_title_line) + return True + + +@register_beman_standard_check("README.BADGES") +class ReadmeBadgesCheck(ReadmeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + """ + self.config["values"] contains a fixed set of Beman badges. + """ + badges = self.config["values"] + assert len(badges) == 4 # The number of library maturity model states + + # Check if exactly one of the required badges is present. + badge_count = len([badge for badge in badges if self.has_content(badge)]) + if badge_count != 1: + self.log( + f"The file '{self.path}' does not contain exactly one of the required badges from {badges}" + ) + return False + + return True + + def fix(self): + # TODO: Implement the fix. + pass + + +# TODO README.PURPOSE + + +# TODO README.IMPLEMENTS + + +@register_beman_standard_check("README.LIBRARY_STATUS") +class ReadmeLibraryStatusCheck(ReadmeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + """ + self.config["values"] contains a fixed set of Beman library statuses. + """ + statuses = self.config["values"] + assert len(statuses) == len(self.beman_library_maturity_model) + + # Check if at least one of the required status values is present. + status_count = len([status for status in statuses if self.has_content(status)]) + if status_count != 1: + self.log( + f"The file '{self.path}' does not contain exactly one of the required statuses from {statuses}" + ) + return False + + return True + + def fix(self): + # TODO: Implement the fix. + pass diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py new file mode 100644 index 00000000..6c2f59b1 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# [RELEASE.*] checks category. +# Note: Data is stored online - e.g. https://github.com/bemanproject/exemplar/releases +# TBD - Do we want to implement these checks? + + +# TODO RELEASE.GITHUB + + +# TODO RELEASE.NOTES + + +# TODO RELEASE.GODBOLT_TRUNK_VERSION diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py new file mode 100644 index 00000000..f4c4ad30 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from .cmake import CMakeBaseCheck +from .license import LicenseBaseCheck +from .readme import ReadmeBaseCheck +from ..system.registry import register_beman_standard_check + +# [TOPLEVEL.*] checks category. +# All checks in this file extend the ToplevelBaseCheck class. +# +# Note: ToplevelBaseCheck is not a registered check! + + +@register_beman_standard_check(check="TOPLEVEL.CMAKE") +class ToplevelCmakeCheck(CMakeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + return super().pre_check() + + def fix(self): + # TODO: Implement the fix. + pass + + +@register_beman_standard_check("TOPLEVEL.LICENSE") +class ToplevelLicenseCheck(LicenseBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + # since this class simply checks for the existence of a LICENSE file, + # there's nothing more to do than the default pre-check. + return super().pre_check() + + def fix(self): + self.log( + "Please add a LICENSE file to the repository. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#license for more information." + ) + + +@register_beman_standard_check("TOPLEVEL.README") +class ToplevelReadmeCheck(ReadmeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + # since this class simply checks for the existence of a README file, + # there's nothing more to do than the default pre-check. + return super().pre_check() + + def fix(self): + self.log( + "Please write a README file. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#readmemd for the desired format." + ) diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/system/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/system/git.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/system/git.py new file mode 100644 index 00000000..ecc11456 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/system/git.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import sys + +from ..base.base_check import BaseCheck + + +class DisallowFixInplaceAndUnstagedChangesCheck(BaseCheck): + """ + If fix is attempted, disallow it if there are unstaged changes. + """ + + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "NO_UNSTAGED_CHANGES") + + def check(self): + """ + Should not allow fix if there are unstaged changes. + """ + return len(self.repo_info["unstaged_changes"]) == 0 + + def fix(self): + """ + Stop the program if there are unstaged changes. + """ + self.log( + "The fix cannot be applied inplace. Please commit or stash your changes. STOP." + ) + sys.exit(1) diff --git a/infra/tools/beman-tidy/beman_tidy/lib/checks/system/registry.py b/infra/tools/beman-tidy/beman_tidy/lib/checks/system/registry.py new file mode 100644 index 00000000..cabb8ac2 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/checks/system/registry.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +from typing import Dict, Type, List + +# Registry to store all The Beman Standard check classes. +_beman_standard_check_registry: Dict[str, Type] = {} + + +def register_beman_standard_check(check: str): + """ + Decorator to register a check class with a specific ID. + + Usage: + @register_beman_standard_check("README.TITLE") + class ReadmeTitleCheck(ReadmeBaseCheck): + ... + + Notes: Only register most derived check classes, which are actually part from + The Beman Standard - e.g., README.TITLE, README.BADGES, etc. + """ + + def decorator(check_class: Type) -> Type: + _beman_standard_check_registry[check] = check_class + return check_class + + return decorator + + +def get_registered_beman_standard_checks() -> Dict[str, Type]: + """Get all registered check classes""" + return _beman_standard_check_registry.copy() + + +def get_beman_standard_check_by_name(check_name: str) -> Type: + """Get a specific check class by its name""" + return _beman_standard_check_registry.get(check_name) + + +def get_all_beman_standard_check_names() -> List[str]: + """Get all registered check names""" + return list(_beman_standard_check_registry.keys()) + + +def get_beman_standard_check_name_by_class(target_check_class: Type) -> str: + """Get the name of a check class""" + for check_name, check_class in _beman_standard_check_registry.items(): + if check_class == target_check_class: + return check_name + return None diff --git a/infra/tools/beman-tidy/beman_tidy/lib/pipeline.py b/infra/tools/beman-tidy/beman_tidy/lib/pipeline.py new file mode 100644 index 00000000..a5d7d408 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/pipeline.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import sys + +from .checks.system.registry import get_registered_beman_standard_checks +from .checks.system.git import DisallowFixInplaceAndUnstagedChangesCheck + +# import all the implemented checks. +# TODO: Consider removing F403 from ignored lint checks +from .checks.beman_standard.cmake import * # noqa: F401, F403 +from .checks.beman_standard.cpp import * # noqa: F401, F403 +from .checks.beman_standard.directory import * # noqa: F401, F403 +from .checks.beman_standard.file import * # noqa: F401, F403 +from .checks.beman_standard.general import * # noqa: F401, F403 +from .checks.beman_standard.license import * # noqa: F401, F403 +from .checks.beman_standard.readme import * # noqa: F401, F403 +from .checks.beman_standard.release import * # noqa: F401, F403 +from .checks.beman_standard.toplevel import * # noqa: F401, F403 + +red_color = "\033[91m" +green_color = "\033[92m" +yellow_color = "\033[93m" +gray_color = "\033[90m" +no_color = "\033[0m" + + +def run_checks_pipeline(checks_to_run, args, beman_standard_check_config): + """ + Run the checks pipeline for The Beman Standard. + Read-only checks if args.fix_inplace is False, otherwise try to fix the issues in-place. + Verbosity is controlled by args.verbose. + + @return: The number of failed checks. + """ + + """ + Helper function to log messages. + """ + + def log(msg): + if args.verbose: + print(msg) + + """ + Helper function to run a check. + @param check_class: The check class type to run. + @param log_enabled: Whether to log the check result. + @return: True if the check passed, False otherwise. + """ + + def run_check(check_class, log_enabled=args.verbose): + check_instance = check_class(args.repo_info, beman_standard_check_config) + check_instance.log_enabled = log_enabled + check_type = check_instance.type + + log(f"Running check [{check_instance.type}][{check_instance.name}] ... ") + + if (check_instance.pre_check() and check_instance.check()) or ( + args.fix_inplace and check_instance.fix() + ): + log( + f"\tcheck [{check_instance.type}][{check_instance.name}] ... {green_color}PASSED{no_color}\n" + ) + return check_type, True + else: + log( + f"\tcheck [{check_instance.type}][{check_instance.name}] ... {red_color}FAILED{no_color}\n" + ) + return check_type, False + + """ + Main pipeline. + """ + + def run_pipeline_helper(): + # Internal checks + if args.fix_inplace: + run_check(DisallowFixInplaceAndUnstagedChangesCheck, log_enabled=False) + + implemented_checks = get_registered_beman_standard_checks() + all_checks = beman_standard_check_config + + cnt_passed = { + "REQUIREMENT": 0, + "RECOMMENDATION": 0, + } + cnt_failed = { + "REQUIREMENT": 0, + "RECOMMENDATION": 0, + } + for check_name in checks_to_run: + if check_name not in implemented_checks: + continue + + check_type, passed = run_check(implemented_checks[check_name]) + if passed: + cnt_passed[check_type] += 1 + else: + cnt_failed[check_type] += 1 + + cnt_skipped = { + "REQUIREMENT": 0, + "RECOMMENDATION": 0, + } + cnt_all_beman_standard_checks = { + "REQUIREMENT": 0, + "RECOMMENDATION": 0, + } + cnt_implemented_checks = { + "REQUIREMENT": 0, + "RECOMMENDATION": 0, + } + for check_name in all_checks: + check_type = all_checks[check_name]["type"] + cnt_all_beman_standard_checks[check_type] += 1 + + if check_name not in implemented_checks: + cnt_skipped[check_type] += 1 + else: + cnt_implemented_checks[check_type] += 1 + + return ( + cnt_passed, + cnt_failed, + cnt_skipped, + cnt_implemented_checks, + cnt_all_beman_standard_checks, + ) + + log("beman-tidy pipeline started ...\n") + ( + cnt_passed, + cnt_failed, + cnt_skipped, + cnt_implemented_checks, + cnt_all_beman_standard_checks, + ) = run_pipeline_helper() + log("\nbeman-tidy pipeline finished.\n") + + # Always print the summary. + print( + f"Summary REQUIREMENT: {green_color} {cnt_passed['REQUIREMENT']} checks PASSED{no_color}, {red_color}{cnt_failed['REQUIREMENT']} checks FAILED{no_color}, {gray_color}{cnt_skipped['REQUIREMENT']} skipped (NOT implemented).{no_color}" + ) + print( + f"Summary RECOMMENDATION: {green_color} {cnt_passed['RECOMMENDATION']} checks PASSED{no_color}, {red_color}{cnt_failed['RECOMMENDATION']} checks FAILED{no_color}, {gray_color}{cnt_skipped['RECOMMENDATION']} skipped (NOT implemented).{no_color}" + ) + + # Always print the coverage. + coverage_requirement = round( + cnt_passed["REQUIREMENT"] / cnt_implemented_checks["REQUIREMENT"] * 100, 2 + ) + coverage_recommendation = round( + cnt_passed["RECOMMENDATION"] / cnt_implemented_checks["RECOMMENDATION"] * 100, 2 + ) + total_passed = cnt_passed["REQUIREMENT"] + cnt_passed["RECOMMENDATION"] + total_implemented = ( + cnt_implemented_checks["REQUIREMENT"] + cnt_implemented_checks["RECOMMENDATION"] + ) + total_coverage = round((total_passed) / (total_implemented) * 100, 2) + print( + f"\n{__calculate_coverage_color(coverage_requirement)}Coverage REQUIREMENT: {coverage_requirement:{6}.2f}% ({cnt_passed['REQUIREMENT']}/{cnt_implemented_checks['REQUIREMENT']} checks passed).{no_color}" + ) + if args.require_all: + print( + f"{__calculate_coverage_color(coverage_recommendation)}Coverage RECOMMENDATION: {coverage_recommendation:{6}.2f}% ({cnt_passed['RECOMMENDATION']}/{cnt_implemented_checks['RECOMMENDATION']} checks passed).{no_color}" + ) + print( + f"{__calculate_coverage_color(total_coverage)}Coverage TOTAL: {total_coverage:{6}.2f}% ({total_passed}/{total_implemented} checks passed).{no_color}" + ) + else: + print("Note: RECOMMENDATIONs are not included (--require-all NOT set).") + total_cnt_failed = cnt_failed["REQUIREMENT"] + ( + cnt_failed["RECOMMENDATION"] if args.require_all else 0 + ) + + sys.stdout.flush() + return total_cnt_failed + + +def __calculate_coverage_color(cov): + """ + Returns the colour for the coverage print based on severity + Green for 100% + Red for 0% + Yellow for anything else + """ + if cov == 100: + return green_color + elif cov == 0: + return red_color + else: + return yellow_color diff --git a/infra/tools/beman-tidy/beman_tidy/lib/utils/__init__.py b/infra/tools/beman-tidy/beman_tidy/lib/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/beman_tidy/lib/utils/git.py b/infra/tools/beman-tidy/beman_tidy/lib/utils/git.py new file mode 100644 index 00000000..7f8e3d85 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/utils/git.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import sys +import yaml +from pathlib import Path + +from git import Repo, InvalidGitRepositoryError + + +def get_repo_info(path: str): + """ + Get information about the repository at the given path. + Returns data as a dictionary. + """ + + path: Path = Path(path) + try: + # Initialize the repository object + repo = Repo(path.absolute(), search_parent_directories=True) + + # Get the top-level directory of the repository + top_level_dir = Path(repo.git.rev_parse("--show-toplevel")) + + # Get the repository name (directory name of the top level) + repo_name = top_level_dir.name + + # Get the remote URL (assuming 'origin' is the remote name) + remote_url = None + if "origin" in repo.remotes: + remote_url = repo.remotes.origin.url + + # Get the current branch + current_branch = repo.active_branch.name + + # Get the commit hash + commit_hash = repo.head.commit.hexsha + + # Get the status of the repository + status = repo.git.status() + + # Get unstaged changes + unstaged_changes = repo.git.diff("--stat") + + return { + "top_level": top_level_dir, + "name": repo_name, + "remote_url": remote_url, + "current_branch": current_branch, + "commit_hash": commit_hash, + "status": status, + "unstaged_changes": unstaged_changes, + } + except InvalidGitRepositoryError: + print(f"The path '{path}' is not inside a valid Git repository.") + sys.exit(1) + except Exception: + print(f"An error occurred while getting repository information. Check {path}.") + sys.exit(1) + + +def get_beman_standard_config_path(): + """ + Get the path to the Beman Standard YAML configuration file. + """ + return Path(__file__).parent.parent.parent / ".beman-standard.yml" + + +def load_beman_standard_config(path=get_beman_standard_config_path()): + """ + Load the Beman Standard YAML configuration file from the given path. + """ + with open(path, "r") as file: + beman_standard_yml = yaml.safe_load(file) + + beman_standard_check_config = {} + for check_name in beman_standard_yml: + check_config = { + "name": check_name, + "full_text_body": "", + "type": "", + "regex": "", + "file_name": "", + "directory_name": "", + "badge_lines": "", + "status_lines": "", + "licenses": "", + "default_group": "", + } + for entry in beman_standard_yml[check_name]: + if "type" in entry: + check_config["type"] = entry["type"] + elif "value" in entry: # e.g., "a string value" + check_config["value"] = entry["value"] + # e.g., ["a string value", "another string value"] + elif "values" in entry: + check_config["values"] = entry["values"] + elif "regex" in entry: + # TODO: Implement the regex check. + pass + elif "file_name" in entry: + check_config["file_name"] = entry["file_name"] + elif "directory_name" in entry: + pass + elif "values" in entry: + # TODO: Implement the values check. + pass + elif "status_lines" in entry: + # TODO: Implement the status check. + pass + elif "licenses" in entry: + # TODO: Implement the license check. + pass + elif "default_group" in entry: + check_config["default_group"] = entry["default_group"] + else: + raise ValueError(f"Invalid entry in Beman Standard YAML: {entry}") + + beman_standard_check_config[check_name] = check_config + + return beman_standard_check_config diff --git a/infra/tools/beman-tidy/beman_tidy/lib/utils/string.py b/infra/tools/beman-tidy/beman_tidy/lib/utils/string.py new file mode 100644 index 00000000..07329da2 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/utils/string.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re + + +def is_snake_case(name): + return re.match("(^[a-z0-9]+$)|(^[a-z0-9][a-z0-9_.]+[a-z0-9]$)", name) + + +def is_beman_snake_case(name): + """ + Has prefix "beman." and continues with snake_case. + It must NOT end with a C++ target standard version - e.g. 17, 20, 23, 26, 32, etc. + """ + + return ( + name[:6] == "beman." + and is_snake_case(name[6:]) + and not re.match(".*[0-9]+$", name[6:]) + ) + + +def match_badges(string): + """ + e.g., ![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + """ + if string is None: + return None + + badges_str = re.findall(r"!\[[^\]]+\]\([^)]+\)", string) + return [ + re.match(r"!\[([^\]]+)\]\(([^)]+)\)", badge).groups() for badge in badges_str + ] + + +def skip_lines(lines, n): + return lines[n:] if lines is not None else None + + +def skip_empty_lines(lines): + if lines is None: + return None + + while len(lines) > 0 and len(lines[0].strip()) == 0: + lines = lines[1:] + return lines diff --git a/infra/tools/beman-tidy/beman_tidy/lib/utils/terminal.py b/infra/tools/beman-tidy/beman_tidy/lib/utils/terminal.py new file mode 100644 index 00000000..6ba12977 --- /dev/null +++ b/infra/tools/beman-tidy/beman_tidy/lib/utils/terminal.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import subprocess + + +def run_command(command, return_stdout=False, cwd=None): + """ + Run a command in the shell and return the return code. + If return_stdout is True, return the stdout of the command. + Optionally, change the current working directory to cwd. + """ + print(f"Running command: {command} with cwd: {cwd}") + if return_stdout: + bin = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, cwd=cwd + ).stdout.read() + return bin.decode("utf-8") + else: + return subprocess.run(command, shell=True, cwd=cwd).returncode diff --git a/infra/tools/beman-tidy/docs/dev-guide.md b/infra/tools/beman-tidy/docs/dev-guide.md new file mode 100644 index 00000000..036e4732 --- /dev/null +++ b/infra/tools/beman-tidy/docs/dev-guide.md @@ -0,0 +1,135 @@ +# Beman Tidy Development Guide + +## Tree structure + +* `README.md`: The public documentation for the `beman-tidy` tool. +* `docs/`: The internal documentation. +* `beman_tidy/`: The package/production code for the tool. + * `beman_tidy/cli.py`: The CLI / entry point for the tool. + * `beman_tidy/lib/`: The library for the tool. + * `beman_tidy/lib/checks/`: The checks for the tool. + * `beman_tidy/lib/pipeline.py`: The checks pipeline for the `beman-tidy` tool. + * `beman_tidy/.beman-standard.yml`: Stable (offline) version of the standard. +* `tests/`: Unit tests for the tool. + * Structure is similar to the `beman_tidy/` directory. + * `pytest` is used for testing. + +## Adding a new check + +Find an unimplemented check in the [BEMAN_STANDARD.md](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md) file and check that is not already assigned in [Planning for beman-tidy: The Codebase Bemanification Tool](https://github.com/orgs/bemanproject/projects/8/views/1). + + +Check this PR example: [beman-tidy: add check - README.LIBRARY_STATUS](https://github.com/bemanproject/infra/pull/35). + +
+Step by step tutorial: add a new check + +* `[mandatory]` Make sure `beman_tidy/.beman-standard.yml` reflects your check metadata (latest status from [BEMAN_STANDARD.md](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md)). + * `[optional]` New syntax / keys from yml config can be added in + [infra/tools/beman-tidy/beman_tidy/lib/utils_git.py:load_beman_standard_config()](https://github.com/bemanproject/infra/blob/main/tools/beman-tidy/beman_tidy/lib/utils/git.py) + if not already implemented. Checks for TODOs in `load_beman_standard_config()`. +* `[mandatory]` Add the check to the `beman_tidy/lib/checks/beman_standard/` directory. + * `[mandatory]` e.g., `README.*` checks will most likely go to a path similar to `beman_tidy/lib/checks/beman_standard/readme.py`. + * `[mandatory]` Use an appropriate base class - e.g., defaults like `FileBaseCheck` / `DirectoryBaseCheck` or create + specializations for reusing code - e.g., `ReadmeBaseCheck(FileBaseCheck)` / `CmakeBaseCheck(FileBaseCheck)` / + `CppBaseCheck(FileBaseCheck)` etc. + * `[mandatory]` Register the new check via `@register_beman_standard_check` decorator - e.g., + + ```python + @register_beman_standard_check("README.TITLE") + class ReadmeTitleCheck(ReadmeBaseCheck): + ``` + +* `[mandatory]` Add tests for the check to the `tests/beman_standard/` directory. More in [Writing Tests](#writing-tests). +* `[optional]` Updates docs if needed in `README.md` and `docs/dev-guide.md` files. +* `[optional]` Update the `beman_tidy/cli.py` file if the public API has changed. + +
+ + +## Linting + +Run the linter on the beman-tidy's codebase: + +```shell +uv run ruff check --diff +uv run ruff check --fix +``` + +## Testing + +### Running Tests + +Run the tests: + +```shell +$ uv run pytest +================================================================================================================ test session starts ================================================================================================================ +platform darwin -- Python 3.14.0b2, pytest-8.4.0, pluggy-1.6.0 -- /Users/dariusn/dev/dn/git/Beman/infra/tools/beman-tidy/.venv/bin/python +cachedir: .pytest_cache +rootdir: /Users/dariusn/dev/dn/git/Beman/infra/tools/beman-tidy +configfile: pyproject.toml +collected 6 items + +tests/beman_standard/readme/test_readme.py::test__README_TITLE__valid PASSED [ 16%] +tests/beman_standard/readme/test_readme.py::test__README_TITLE__invalid PASSED [ 33%] +tests/beman_standard/readme/test_readme.py::test__README_TITLE__fix_inplace PASSED [ 50%] +tests/beman_standard/readme/test_readme.py::test__README_BADGES__valid PASSED [ 66%] +tests/beman_standard/readme/test_readme.py::test__README_BADGES__invalid PASSED [ 83%] +tests/beman_standard/readme/test_readme.py::test__README_BADGES__fix_inplace SKIPPED (NOT implemented) [100%] + +=========================================================================================================== 5 passed, 1 skipped in 0.07s ============================================================================================================ +``` + +### Writing Tests + +* `tests/lib/checks/beman_standard//test_.py`: The test file for the `` + check. + * e.g., for `check_category = "readme"` the test file is `tests/lib/checks/beman_standard/readme/test_readme.py`. +* `test____()` function inside the test file. + * e.g., for `check_category = "readme"` and `test_case_name = "valid"` the function is `test__README_TITLE__valid()`. + * e.g., for `check_category = "readme"` and `test_case_name = "invalid"` the function is + `test__README_TITLE__invalid()`. +* `tests/beman_standard//data/`: The data for the tests (e.g., files, directories, etc.). + * e.g., for `check_category = "readme"` and `test_case_name = "valid"` the data is in + `tests/lib/checks/beman_standard/readme/data/valid/`. + * e.g., for `check_category = "readme"` and `test_case_name = "invalid"` the data is in + `tests/lib/checks/beman_standard/readme/data/invalid/`. + * e.g., for `check_category = "readme"` and `test_case_name = "fix_inplace"` the data may use both `valid` and + `invalid` files. It is recommended to not change these files and use temporary copies having suffix `.delete_me` + (which are not tracked by git). +* Default setup / mocks: + * `repo_info`: The repository information (e.g., path, name, etc.). Mocked with hardcoded values of `beman.exemplar`. + * `beman_standard_check_config`: The Beman Standard configuration file. Actual load of the `.beman-standard.yml` + file. +* Always add at least 3 test cases for each check. + * `valid`: The test case for the valid case. + * `invalid`: The test case for the invalid case. + * `fix_inplace`: The test case for the fix invalid case. If the fix is not (yet) implementable, add a + `@pytest.mark.skip(reason="NOT implemented")` decorator to track the progress. + +## Changing dependencies + +* Add / update the dependency to the `pyproject.toml` file. +* Run `uv clean` to make sure the dependencies are updated. +* Run `uv sync` to update the uv lockfile +* Run `uv export -o pylock.toml` to update `pylock.toml` +* Run `uv build` to build the wheel. +* Run `uv run beman-tidy --help` to check if the new dependency is available. +* Commit the changes from `pyproject.toml`, `pylock.toml` and `uv.lock`. + +## Development Notes + +Requirements: + +* `beman-tidy` must be able to run on Windows, Linux, and macOS, thus it's 100% Python. +* `beman-tidy` must NOT use internet access. A local snapshot of the standard is used (check `.beman-standard.yml`). +* `beman-tidy` must have `verbose` and `non-verbose` modes. Default is `non-verbose`. +* `beman-tidy` must have `dry-run` and `fix-inplace` modes. Default is `dry-run`. +* `beman-tidy` must detect types of checks: failed, passed, skipped (not implemented) and print the summary/coverage. + +Limitations: + +* `2025-06-07`: `beman-tidy` will not support the `--fix-inplace` flag in the first iteration for most of the checks. +* `2025-06-07`: `beman-tidy` may generate small changes to the standard (e.g., for automated fixes), while the standard + is not stable. Thus, the tool itself may be unstable. diff --git a/infra/tools/beman-tidy/pylock.toml b/infra/tools/beman-tidy/pylock.toml new file mode 100644 index 00000000..8fb1d543 --- /dev/null +++ b/infra/tools/beman-tidy/pylock.toml @@ -0,0 +1,124 @@ +# This file was autogenerated by uv via the following command: +# uv export -o pylock.toml +lock-version = "1.0" +created-by = "uv" +requires-python = ">=3.12" + +[[packages]] +name = "beman-tidy" +directory = { path = ".", editable = true } + +[[packages]] +name = "colorama" +version = "0.4.6" +marker = "sys_platform == 'win32'" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", upload-time = 2022-10-25T02:36:22Z, size = 27697, hashes = { sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", upload-time = 2022-10-25T02:36:20Z, size = 25335, hashes = { sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" } }] + +[[packages]] +name = "gitdb" +version = "4.0.12" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", upload-time = 2025-01-02T07:20:46Z, size = 394684, hashes = { sha256 = "5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", upload-time = 2025-01-02T07:20:43Z, size = 62794, hashes = { sha256 = "67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf" } }] + +[[packages]] +name = "gitpython" +version = "3.1.44" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", upload-time = 2025-01-02T07:32:43Z, size = 214196, hashes = { sha256 = "c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269" } } +wheels = [{ name = "gitpython-3.1.44-py3-none-any.whl", url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", upload-time = 2025-01-02T07:32:40Z, size = 207599, hashes = { sha256 = "9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110" } }] + +[[packages]] +name = "iniconfig" +version = "2.1.0" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", upload-time = 2025-03-19T20:09:59Z, size = 4793, hashes = { sha256 = "3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", upload-time = 2025-03-19T20:10:01Z, size = 6050, hashes = { sha256 = "9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" } }] + +[[packages]] +name = "packaging" +version = "25.0" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", upload-time = 2025-04-19T11:48:59Z, size = 165727, hashes = { sha256 = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", upload-time = 2025-04-19T11:48:57Z, size = 66469, hashes = { sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" } }] + +[[packages]] +name = "pluggy" +version = "1.6.0" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", upload-time = 2025-05-15T12:30:07Z, size = 69412, hashes = { sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", upload-time = 2025-05-15T12:30:06Z, size = 20538, hashes = { sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" } }] + +[[packages]] +name = "pygments" +version = "2.19.1" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", upload-time = 2025-01-06T17:26:30Z, size = 4968581, hashes = { sha256 = "61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", upload-time = 2025-01-06T17:26:25Z, size = 1225293, hashes = { sha256 = "9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" } }] + +[[packages]] +name = "pytest" +version = "8.4.0" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", upload-time = 2025-06-02T17:36:30Z, size = 1515232, hashes = { sha256 = "14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", upload-time = 2025-06-02T17:36:27Z, size = 363797, hashes = { sha256 = "f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e" } }] + +[[packages]] +name = "pyyaml" +version = "6.0.2" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", upload-time = 2024-08-06T20:33:50Z, size = 130631, hashes = { sha256 = "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e" } } +wheels = [ + { name = "pyyaml-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:32:25Z, size = 183873, hashes = { sha256 = "c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab" } }, + { name = "pyyaml-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:26Z, size = 173302, hashes = { sha256 = "ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725" } }, + { name = "pyyaml-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:28Z, size = 739154, hashes = { sha256 = "1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5" } }, + { name = "pyyaml-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:30Z, size = 766223, hashes = { sha256 = "9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425" } }, + { name = "pyyaml-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:31Z, size = 767542, hashes = { sha256 = "80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476" } }, + { name = "pyyaml-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:37Z, size = 731164, hashes = { sha256 = "0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48" } }, + { name = "pyyaml-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:38Z, size = 756611, hashes = { sha256 = "8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b" } }, + { name = "pyyaml-6.0.2-cp312-cp312-win32.whl", url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", upload-time = 2024-08-06T20:32:40Z, size = 140591, hashes = { sha256 = "ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4" } }, + { name = "pyyaml-6.0.2-cp312-cp312-win_amd64.whl", url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", upload-time = 2024-08-06T20:32:41Z, size = 156338, hashes = { sha256 = "7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8" } }, + { name = "pyyaml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", upload-time = 2024-08-06T20:32:43Z, size = 181309, hashes = { sha256 = "efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba" } }, + { name = "pyyaml-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:44Z, size = 171679, hashes = { sha256 = "50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1" } }, + { name = "pyyaml-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:46Z, size = 733428, hashes = { sha256 = "0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133" } }, + { name = "pyyaml-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:51Z, size = 763361, hashes = { sha256 = "17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484" } }, + { name = "pyyaml-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:53Z, size = 759523, hashes = { sha256 = "70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5" } }, + { name = "pyyaml-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:54Z, size = 726660, hashes = { sha256 = "41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc" } }, + { name = "pyyaml-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:56Z, size = 751597, hashes = { sha256 = "68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652" } }, + { name = "pyyaml-6.0.2-cp313-cp313-win32.whl", url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", upload-time = 2024-08-06T20:33:03Z, size = 140527, hashes = { sha256 = "bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183" } }, + { name = "pyyaml-6.0.2-cp313-cp313-win_amd64.whl", url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", upload-time = 2024-08-06T20:33:04Z, size = 156446, hashes = { sha256 = "8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563" } }, +] + +[[packages]] +name = "ruff" +version = "0.11.13" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", upload-time = 2025-06-05T21:00:15Z, size = 4282054, hashes = { sha256 = "26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", upload-time = 2025-06-05T20:59:32Z, size = 10292516, hashes = { sha256 = "4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46" } }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", upload-time = 2025-06-05T20:59:37Z, size = 11106083, hashes = { sha256 = "aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48" } }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", upload-time = 2025-06-05T20:59:39Z, size = 10436024, hashes = { sha256 = "53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b" } }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-06-05T20:59:42Z, size = 10646324, hashes = { sha256 = "ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a" } }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-06-05T20:59:44Z, size = 10174416, hashes = { sha256 = "6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc" } }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-06-05T20:59:46Z, size = 11724197, hashes = { sha256 = "1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629" } }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", upload-time = 2025-06-05T20:59:49Z, size = 12511615, hashes = { sha256 = "d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933" } }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-06-05T20:59:51Z, size = 12117080, hashes = { sha256 = "55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165" } }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-06-05T20:59:54Z, size = 11326315, hashes = { sha256 = "633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71" } }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-06-05T20:59:56Z, size = 11555640, hashes = { sha256 = "4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9" } }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", upload-time = 2025-06-05T20:59:59Z, size = 10507364, hashes = { sha256 = "4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc" } }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", upload-time = 2025-06-05T21:00:01Z, size = 10141462, hashes = { sha256 = "d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7" } }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", upload-time = 2025-06-05T21:00:04Z, size = 11121028, hashes = { sha256 = "26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432" } }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", upload-time = 2025-06-05T21:00:06Z, size = 11602992, hashes = { sha256 = "51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492" } }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", upload-time = 2025-06-05T21:00:08Z, size = 10474944, hashes = { sha256 = "96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250" } }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", upload-time = 2025-06-05T21:00:11Z, size = 11548669, hashes = { sha256 = "29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3" } }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", upload-time = 2025-06-05T21:00:13Z, size = 10683928, hashes = { sha256 = "b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b" } }, +] + +[[packages]] +name = "smmap" +version = "5.0.2" +index = "https://pypi.org/simple" +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", upload-time = 2025-01-02T07:14:40Z, size = 22329, hashes = { sha256 = "26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", upload-time = 2025-01-02T07:14:38Z, size = 24303, hashes = { sha256 = "b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e" } }] diff --git a/infra/tools/beman-tidy/pyproject.toml b/infra/tools/beman-tidy/pyproject.toml new file mode 100644 index 00000000..fdc1d372 --- /dev/null +++ b/infra/tools/beman-tidy/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "beman-tidy" +version = "0.1.0" +description = "The Codebase Bemanification Tool" +readme = "README.md" +requires-python = ">=3.12" +authors = [{ name = "Darius Neațu", email = "neatudarius@gmail.com" }] +maintainers = [{ name = "Rishyak", email = "hello@rishyak.com" }] +dependencies = ["gitpython==3.1.44", "pyyaml==6.0.2"] + +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[dependency-groups] +dev = ["pytest>=8.4.0", "ruff>=0.11.13"] + +[project.scripts] +beman-tidy = "beman_tidy.cli:main" + +[tool.pytest.ini_options] +addopts = "-v" diff --git a/infra/tools/beman-tidy/tests/__init__.py b/infra/tools/beman-tidy/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/conftest.py b/infra/tools/beman-tidy/tests/conftest.py new file mode 100644 index 00000000..430a7be8 --- /dev/null +++ b/infra/tools/beman-tidy/tests/conftest.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import pytest +import os +from pathlib import Path + + +def pytest_configure(config): + """ + Add custom markers to pytest. + """ + config.addinivalue_line( + "markers", "use_test_repo: mark test to use test repository instead of exemplar" + ) + + +@pytest.fixture(autouse=True) +def _setup_test_environment(): + """ + Setup test environment variables and paths. + This runs automatically for all tests. + """ + # Get the root directory of the project + root_dir = Path(__file__).parent.parent + + # Add the project root to PYTHONPATH if not already there + if str(root_dir) not in os.environ.get("PYTHONPATH", ""): + os.environ["PYTHONPATH"] = f"{root_dir}:{os.environ.get('PYTHONPATH', '')}" + + yield diff --git a/infra/tools/beman-tidy/tests/lib/__init__.py b/infra/tools/beman-tidy/tests/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/lib/checks/__init__.py b/infra/tools/beman-tidy/tests/lib/checks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/__init__.py b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/__init__.py b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/conftest.py b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/conftest.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import pytest + +from tests.utils.conftest import mock_repo_info, mock_beman_standard_check_config # noqa: F401 + + +@pytest.fixture(autouse=True) +def repo_info(mock_repo_info): # noqa: F811 + return mock_repo_info + + +@pytest.fixture +def beman_standard_check_config(mock_beman_standard_check_config): # noqa: F811 + return mock_beman_standard_check_config diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md new file mode 100644 index 00000000..5be8ed82 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md @@ -0,0 +1,16 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library typo Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: typos in badges. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v2.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v2.md new file mode 100644 index 00000000..4aa668e9 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v2.md @@ -0,0 +1,15 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Other display text](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: invalid badge display text. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v3.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v3.md new file mode 100644 index 00000000..ffc08c34 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v3.md @@ -0,0 +1,15 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/mylogo) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: invalid badge URL. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v1.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v1.md new file mode 100644 index 00000000..bdef5791 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v1.md @@ -0,0 +1,15 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: Under development and not yet ready for production use. + +This is NOT a valid README.md according to the Beman Standard: the library status is not properly formatted. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v2.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v2.md new file mode 100644 index 00000000..a4da50fc --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v2.md @@ -0,0 +1,16 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + + +**Status**: [under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md# TYPO HERE under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: the library status line has typos. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v3.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v3.md new file mode 100644 index 00000000..8d5e2bdd --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v3.md @@ -0,0 +1,18 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use), +**Status**: [Production ready. API may undergo changes.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-api-may-undergo-changes), +**Status**: [Production ready. Stable API.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-stable-api), +**Status**: [Retired. No longer maintained or actively developed.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#retired-no-longer-maintained-or-actively-developed), + +This is NOT a valid README.md according to the Beman Standard: the library status is duplicated. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v1.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v1.md new file mode 100644 index 00000000..7f91a7a3 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v1.md @@ -0,0 +1,15 @@ +# beman exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: missing beman.exemplar. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v2.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v2.md new file mode 100644 index 00000000..e92b46f4 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v2.md @@ -0,0 +1,15 @@ +# beman.exemplar A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: missing : diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v3.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v3.md new file mode 100644 index 00000000..6ae059f0 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v3.md @@ -0,0 +1,16 @@ +# beman.optional: C++26 Optional Library + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: wrong library name. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v4.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v4.md new file mode 100644 index 00000000..eea29e4b --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v4.md @@ -0,0 +1,15 @@ +# beman.optional: C++26 Optional Library + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + +This is NOT a valid README.md according to the Beman Standard: wrong library name. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid.md new file mode 100644 index 00000000..7048d188 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid.md @@ -0,0 +1,11 @@ +# Wrong Title Format + +This is an invalid README.md file that doesn't follow the Beman Standard: the title doesn't have the correct format. + +This is an invalid README.md file that doesn't follow the Beman Standard: the badges are missing. + +This is an invalid README.md file that doesn't follow the Beman Standard: the purpose is missing. + +This is an invalid README.md file that doesn't follow the Beman Standard: the library status is missing. + +This is an invalid README.md file that doesn't follow the Beman Standard: the license is missing. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v1.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v1.md new file mode 100644 index 00000000..403859dd --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v1.md @@ -0,0 +1,26 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#under-development-and-not-yet-ready-for-production-use) + + +This is a valid README.md file that follows the Beman Standard: the title is properly formatted with the library name and a short description. + +This is a valid README.md file that follows the Beman Standard: the badges are properly formatted. + +This is a valid README.md file that follows the Beman Standard: the purpose is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the implements is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the library status is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the license is properly formatted. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v2.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v2.md new file mode 100644 index 00000000..56e5a729 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v2.md @@ -0,0 +1,26 @@ +# beman.exemplar: Another Beman Library + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Production ready. API may undergo changes.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-api-may-undergo-changes) + + +This is a valid README.md file that follows the Beman Standard: the title is properly formatted with the library name and a short description. + +This is a valid README.md file that follows the Beman Standard: the badges are properly formatted. + +This is a valid README.md file that follows the Beman Standard: the purpose is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the implements is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the library status is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the license is properly formatted. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v3.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v3.md new file mode 100644 index 00000000..dbfbacee --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v3.md @@ -0,0 +1,27 @@ +# beman.exemplar: Awesome Beman Library + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + + +**Status**: [Production ready. Stable API.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#production-ready-stable-api) + + +This is a valid README.md file that follows the Beman Standard: the title is properly formatted with the library name and a short description. + +This is a valid README.md file that follows the Beman Standard: the badges are properly formatted. + +This is a valid README.md file that follows the Beman Standard: the purpose is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the implements is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the library status is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the license is properly formatted. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v4.md b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v4.md new file mode 100644 index 00000000..c33c6037 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v4.md @@ -0,0 +1,26 @@ +# beman.exemplar: The Most Awesome Beman Library + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit.yml/badge.svg) + + +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. + +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + +**Status**: [Retired. No longer maintained or actively developed.](https://github.com/bemanproject/beman/blob/main/docs/BEMAN_LIBRARY_MATURITY_MODEL.md#retired-no-longer-maintained-or-actively-developed) + + +This is a valid README.md file that follows the Beman Standard: the title is properly formatted with the library name and a short description. + +This is a valid README.md file that follows the Beman Standard: the badges are properly formatted. + +This is a valid README.md file that follows the Beman Standard: the purpose is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the implements is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the library status is properly formatted. + +This is a valid README.md file that follows the Beman Standard: the license is properly formatted. diff --git a/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py new file mode 100644 index 00000000..476a1804 --- /dev/null +++ b/infra/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import pytest +from pathlib import Path + +from tests.utils.path_runners import ( + run_check_for_each_path, + run_fix_inplace_for_each_file_path, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.readme import ( + ReadmeTitleCheck, + ReadmeBadgesCheck, + ReadmeLibraryStatusCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/readme/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__README_TITLE__valid(repo_info, beman_standard_check_config): + """ + Test that a valid README.md title passes the check. + """ + valid_readme_paths = [ + # Title: # beman.exemplar: A Beman Library Exemplar + Path(f"{valid_prefix}/README-v1.md"), + # Title: # beman.exemplar: Another Beman Library + Path(f"{valid_prefix}/README-v2.md"), + # Title: # beman.exemplar: Awesome Beman Library + Path(f"{valid_prefix}/README-v3.md"), + # Title: # beman.exemplar: The Most Awesome Beman Library + Path(f"{valid_prefix}/README-v4.md"), + ] + + run_check_for_each_path( + True, + valid_readme_paths, + ReadmeTitleCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__README_TITLE__invalid(repo_info, beman_standard_check_config): + """ + Test that an invalid README.md title fails the check. + """ + invalid_readme_paths = [ + Path(f"{invalid_prefix}/invalid.md"), + Path(f"{invalid_prefix}/invalid-title-v1.md"), + Path(f"{invalid_prefix}/invalid-title-v2.md"), + Path(f"{invalid_prefix}/invalid-title-v3.md"), + Path(f"{invalid_prefix}/invalid-title-v4.md"), + ] + + run_check_for_each_path( + False, + invalid_readme_paths, + ReadmeTitleCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__README_TITLE__fix_inplace(repo_info, beman_standard_check_config): + """ + Test that the fix method corrects an invalid README.md title. + """ + invalid_readme_paths = [ + Path(f"{invalid_prefix}/invalid-title-v1.md"), + Path(f"{invalid_prefix}/invalid-title-v2.md"), + Path(f"{invalid_prefix}/invalid-title-v3.md"), + Path(f"{invalid_prefix}/invalid-title-v4.md"), + ] + + run_fix_inplace_for_each_file_path( + invalid_readme_paths, ReadmeTitleCheck, repo_info, beman_standard_check_config + ) + + +def test__README_BADGES__valid(repo_info, beman_standard_check_config): + """ + Test that a valid README.md badges passes the check. + """ + valid_readme_paths = [ + Path(f"{valid_prefix}/README-v1.md"), + Path(f"{valid_prefix}/README-v2.md"), + Path(f"{valid_prefix}/README-v3.md"), + Path(f"{valid_prefix}/README-v4.md"), + ] + + run_check_for_each_path( + True, + valid_readme_paths, + ReadmeBadgesCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__README_BADGES__invalid(repo_info, beman_standard_check_config): + """ + Test that an invalid README.md badges fails the check. + """ + invalid_readme_paths = [ + Path(f"{invalid_prefix}/invalid.md"), + Path(f"{invalid_prefix}/invalid-badge-v1.md"), + Path(f"{invalid_prefix}/invalid-badge-v2.md"), + Path(f"{invalid_prefix}/invalid-badge-v3.md"), + ] + + run_check_for_each_path( + False, + invalid_readme_paths, + ReadmeBadgesCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__README_BADGES__fix_inplace(repo_info, beman_standard_check_config): + """ + Test that the fix method corrects an invalid README.md badges. + """ + pass + + +def test__README_LIBRARY_STATUS__valid(repo_info, beman_standard_check_config): + """ + Test that a valid README.md library status passes the check. + """ + valid_readme_paths = [ + Path(f"{valid_prefix}/README-v1.md"), + Path(f"{valid_prefix}/README-v2.md"), + Path(f"{valid_prefix}/README-v3.md"), + Path(f"{valid_prefix}/README-v4.md"), + ] + + run_check_for_each_path( + True, + valid_readme_paths, + ReadmeLibraryStatusCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__README_LIBRARY_STATUS__invalid(repo_info, beman_standard_check_config): + """ + Test that an invalid README.md library status fails the check. + """ + invalid_readme_paths = [ + Path(f"{invalid_prefix}/invalid.md"), + Path(f"{invalid_prefix}/invalid-status-line-v1.md"), + Path(f"{invalid_prefix}/invalid-status-line-v2.md"), + Path(f"{invalid_prefix}/invalid-status-line-v3.md"), + ] + + run_check_for_each_path( + False, + invalid_readme_paths, + ReadmeLibraryStatusCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__README_LIBRARY_STATUS__fix_inplace(repo_info, beman_standard_check_config): + """ + Test that the fix method corrects an invalid README.md library status. + """ + pass diff --git a/infra/tools/beman-tidy/tests/utils/__init__.py b/infra/tools/beman-tidy/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infra/tools/beman-tidy/tests/utils/conftest.py b/infra/tools/beman-tidy/tests/utils/conftest.py new file mode 100644 index 00000000..71d82c06 --- /dev/null +++ b/infra/tools/beman-tidy/tests/utils/conftest.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import pytest +from beman_tidy.lib.utils.git import load_beman_standard_config + + +@pytest.fixture +def mock_repo_info(): + """Return repository information for beman.exemplar library""" + return { + "top_level": ".", + "name": "exemplar", + "remote_url": "https://github.com/bemanproject/exemplar", + "current_branch": "main", + "commit_hash": 0, + "status": "", + "unstaged_changes": "", + } + + +@pytest.fixture +def mock_beman_standard_check_config(): + """Parse the Beman Standard YAML file and return a dictionary of check configurations""" + + return load_beman_standard_config() diff --git a/infra/tools/beman-tidy/tests/utils/path_runners.py b/infra/tools/beman-tidy/tests/utils/path_runners.py new file mode 100644 index 00000000..e7b09ec0 --- /dev/null +++ b/infra/tools/beman-tidy/tests/utils/path_runners.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import os +from pathlib import Path + + +def run_check_for_each_path( + expected_result, paths, check_class, repo_info, beman_standard_check_config +): + """ + Run path-based check (check_class) for each given path: evaluate check_class(paths[i]). + + Example: + expected_result = True / False + paths = [ + "tests/lib/checks/beman_standard/readme/data/valid/README-v1.md", + "tests/lib/checks/beman_standard/readme/data/valid/README-v2.md", + ] + check_class = ReadmeTitleCheck or DirectorySourcesCheck + repo_info = "beman.exemplar" + beman_standard_check_config = "/path/to/.beman-standard.yml" + """ + for path in paths: + check_instance = check_class(repo_info, beman_standard_check_config) + check_instance.path = Path(path) + check_instance.log_level = True + + assert check_instance.pre_check() is True, ( + f"[{check_instance.__class__.__name__}] pre_check() failed for {path}" + ) + assert check_instance.check() is expected_result, ( + f"[{check_instance.__class__.__name__}] check() failed for {path}" + ) + + +def run_fix_inplace_for_each_file_path( + invalid_file_paths, check_class, repo_info, beman_standard_check_config +): + """ + Run multiple testcases for a file-based check, for each file starting with a file that is invalid, + and then fixing it. + + Example: + invalid_file_paths = [ + "tests/lib/checks/beman_standard/readme/data/invalid/README-v1.md", + "tests/lib/checks/beman_standard/readme/data/invalid/README-v2.md", + ] + check_class = ReadmeTitleCheck + repo_info = "beman.exemplar" + beman_standard_check_config = "beman_tidy/.beman-standard.yml" + """ + for invalid_path in invalid_file_paths: + check_instance = check_class(repo_info, beman_standard_check_config) + check_instance.path = Path(f"{invalid_path}.delete_me") + check_instance.write(invalid_path.read_text()) + + assert check_instance.pre_check() is True + assert check_instance.check() is False + + assert check_instance.fix() is True + + assert check_instance.pre_check() is True + assert check_instance.check() is True + + # Delete the temporary file + os.remove(f"{invalid_path}.delete_me") + + +def run_fix_inplace_for_each_directory_path( + invalid_directory_paths, check_class, repo_info, beman_standard_check_config +): + # TODO: We may not provide a fix_inplace method for directory-based checks. + pass diff --git a/infra/tools/beman-tidy/uv.lock b/infra/tools/beman-tidy/uv.lock new file mode 100644 index 00000000..da07ff4a --- /dev/null +++ b/infra/tools/beman-tidy/uv.lock @@ -0,0 +1,175 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "beman-tidy" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "gitpython" }, + { name = "pyyaml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "gitpython", specifier = "==3.1.44" }, + { name = "pyyaml", specifier = "==6.0.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.0" }, + { name = "ruff", specifier = ">=0.11.13" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] diff --git a/lockfile.json b/lockfile.json new file mode 100644 index 00000000..4208a985 --- /dev/null +++ b/lockfile.json @@ -0,0 +1,3 @@ +{ + "dependencies": [] +}