diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..4ff90a43 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ednolan @neatudarius @rishyak @wusatosi @JeffGarland diff --git a/.github/workflows/beman-submodule.yml b/.github/workflows/beman-submodule.yml new file mode 100644 index 00000000..8435086c --- /dev/null +++ b/.github/workflows/beman-submodule.yml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: beman-submodule tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + beman-submodule-script-ci: + name: beman_module.py ci + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install pytest + run: | + python3 -m pip install pytest + + - name: Run pytest + run: | + cd tools/beman-submodule/ + pytest diff --git a/.github/workflows/beman-tidy.yml b/.github/workflows/beman-tidy.yml new file mode 100644 index 00000000..46221e6d --- /dev/null +++ b/.github/workflows/beman-tidy.yml @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: beman-tidy tests + +on: + push: + branches: + - main + pull_request: + workflow_call: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' # 09:00AM EEST (@neatudarius' timezone) + +jobs: + run_linter: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tools/beman-tidy + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Sync environment + run: | + uv sync + + - name: Run linter + run: | + uv run ruff check --diff + + run_tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tools/beman-tidy + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Sync environment + run: | + uv sync + + - name: Run tests + run: | + uv run pytest tests/ -v + + build_and_install: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tools/beman-tidy + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Sync environment + run: | + uv sync + + - name: Build and install beman-tidy + run: | + uv clean + uv build + python3 -m pip install dist/beman_tidy-0.1.0-py3-none-any.whl --force-reinstall + beman-tidy --help + + run_on_exemplar: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tools/beman-tidy + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Sync environment + run: | + uv sync + + - name: Build and install beman-tidy + run: | + uv clean + uv build + python3 -m pip install dist/beman_tidy-0.1.0-py3-none-any.whl --force-reinstall + beman-tidy --help + + - name: Run installed beman-tidy on exemplar repo + run: | + git clone https://github.com/bemanproject/exemplar.git + cd exemplar/ # Testing that beman-tidy can be run from any path, e.g. from the exemplar repo. + beman-tidy --verbose --require-all . + + create-issue-when-fault: + needs: [run_linter, run_tests, build_and_install, run_on_exemplar] + if: failure() && (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') + uses: ./.github/workflows/reusable-beman-create-issue-when-fault.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..96468311 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,78 @@ +name: Lint Check (pre-commit) + +on: + # 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-push: + name: Pre-Commit check on Push + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # We wish to run pre-commit on all files instead of the changes + # only made in the push commit. + # + # So linting error persists when there's formatting problem. + - uses: pre-commit/action@v3.0.1 + + pre-commit-pr: + name: Pre-Commit check on PR + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request_target' }} + + permissions: + contents: read + checks: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # pull_request_target checkout the base of the repo + # We need to checkout the actual pr to lint the changes. + - name: Checkout pr + run: gh pr checkout ${{ github.event.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # we only lint on the changed file in PR. + - 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 + id: run-pre-commit + with: + extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} + + # Review dog posts the suggested change from pre-commit to the pr. + - name: suggester / pre-commit + uses: reviewdog/action-suggester@v1 + if: ${{ failure() && steps.run-pre-commit.conclusion == 'failure' }} + with: + tool_name: pre-commit + level: warning + reviewdog_flags: "-fail-level=error" diff --git a/.github/workflows/reusable-beman-create-issue-when-fault.yml b/.github/workflows/reusable-beman-create-issue-when-fault.yml new file mode 100644 index 00000000..024a51f4 --- /dev/null +++ b/.github/workflows/reusable-beman-create-issue-when-fault.yml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: 'Beman issue creation workflow' +on: + workflow_call: + workflow_dispatch: +jobs: + create-issue: + runs-on: ubuntu-latest + steps: + # See https://github.com/cli/cli/issues/5075 + - uses: actions/checkout@v4 + - name: Create issue + run: | + issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] infra repo CI job failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') + body="**CI job failure Report** + - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') + - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + The scheduled job triggered by cron has failed. + Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." + if [[ $issue_num -eq -1 ]]; then + gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] infra repo CI job failure" --body "$body" --assignee ${{ github.actor }} + else + gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" + fi + env: + GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b7cdbb52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Python +__pycache__/ +.pytest_cache/ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +*.pyzwz +*.delete_me + +# MAC OS +*.DS_Store + +# Editor files +.vscode/ +.idea/ + +# Build directories +infra.egg-info/ +beman_tidy.egg-info/ +*.egg-info/ +build/ +dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8641cfad --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.15.1 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories + + # Python linting and formatting + # config file: ruff.toml (not currently present but add if needed) + # https://docs.astral.sh/ruff/configuration/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.1 + hooks: + - id: ruff-check + files: ^tools/beman-tidy/ + - id: ruff-format + files: ^tools/beman-tidy/ diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 00000000..d327587c --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: beman-tidy + name: "beman-tidy: bemanification your repo" + entry: ./tools/beman-tidy/beman-tidy + language: script + pass_filenames: false + always_run: true + args: [".", "--verbose"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0873f35a --- /dev/null +++ b/LICENSE @@ -0,0 +1,234 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + 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/README.md b/README.md new file mode 100644 index 00000000..f40ebc1d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Beman Project Infrastructure Repository + + + +[![beman-tidy tests](https://github.com/bemanproject/infra/actions/workflows/beman-tidy.yml/badge.svg)](https://github.com/bemanproject/infra/actions/workflows/beman-tidy.yml) + +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/cmake/appleclang-toolchain.cmake b/cmake/appleclang-toolchain.cmake new file mode 100644 index 00000000..5f44e802 --- /dev/null +++ b/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/cmake/gnu-toolchain.cmake b/cmake/gnu-toolchain.cmake new file mode 100644 index 00000000..b6dddf6a --- /dev/null +++ b/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/cmake/llvm-libc++-toolchain.cmake b/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 00000000..76264c69 --- /dev/null +++ b/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/cmake/llvm-toolchain.cmake b/cmake/llvm-toolchain.cmake new file mode 100644 index 00000000..5f5ee4b8 --- /dev/null +++ b/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/cmake/msvc-toolchain.cmake b/cmake/msvc-toolchain.cmake new file mode 100644 index 00000000..c2fffa79 --- /dev/null +++ b/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/cmake/use-fetch-content.cmake b/cmake/use-fetch-content.cmake new file mode 100644 index 00000000..07c1a15d --- /dev/null +++ b/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/tools/beman-submodule/README.md b/tools/beman-submodule/README.md new file mode 100644 index 00000000..36883ada --- /dev/null +++ b/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/tools/beman-submodule/beman-submodule b/tools/beman-submodule/beman-submodule new file mode 100755 index 00000000..66cb96e1 --- /dev/null +++ b/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/tools/beman-submodule/test/test_beman_submodule.py b/tools/beman-submodule/test/test_beman_submodule.py new file mode 100644 index 00000000..600fc070 --- /dev/null +++ b/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/tools/beman-tidy/.markdownlintignore b/tools/beman-tidy/.markdownlintignore new file mode 100644 index 00000000..2bfa6a4d --- /dev/null +++ b/tools/beman-tidy/.markdownlintignore @@ -0,0 +1 @@ +tests/ diff --git a/tools/beman-tidy/.python-version b/tools/beman-tidy/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/tools/beman-tidy/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/tools/beman-tidy/README.md b/tools/beman-tidy/README.md new file mode 100644 index 00000000..c01045e8 --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/.beman-standard.yml b/tools/beman-tidy/beman_tidy/.beman-standard.yml new file mode 100644 index 00000000..6df8996f --- /dev/null +++ b/tools/beman-tidy/beman_tidy/.beman-standard.yml @@ -0,0 +1,157 @@ +# 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 +REPOSITORY.DEFAULT_BRANCH: + - type: REQUIREMENT +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: [ + "![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)" + ] + - standard_target: [ + "![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.svg)", + "![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.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.IMPLEMENTATION_HEADERS: + - type: REQUIREMENT +DIRECTORY.SOURCES: + - type: REQUIREMENT +DIRECTORY.TESTS: + - type: REQUIREMENT +DIRECTORY.EXAMPLES: + - type: REQUIREMENT +DIRECTORY.DOCS: + - type: REQUIREMENT +DIRECTORY.PAPERS: + - type: REQUIREMENT + +# 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/tools/beman-tidy/beman_tidy/LICENSE b/tools/beman-tidy/beman_tidy/LICENSE new file mode 100644 index 00000000..0873f35a --- /dev/null +++ b/tools/beman-tidy/beman_tidy/LICENSE @@ -0,0 +1,234 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + 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/tools/beman-tidy/beman_tidy/__init__.py b/tools/beman-tidy/beman_tidy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/cli.py b/tools/beman-tidy/beman_tidy/cli.py new file mode 100755 index 00000000..de648110 --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/__init__.py b/tools/beman-tidy/beman_tidy/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/checks/__init__.py b/tools/beman-tidy/beman_tidy/lib/checks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/checks/base/__init__.py b/tools/beman-tidy/beman_tidy/lib/checks/base/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py b/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py new file mode 100644 index 00000000..13ee53ae --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/base/base_check.py @@ -0,0 +1,131 @@ +#!/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] + if "INTERNAL." not in self.name + else None + ) + + # set type - e.g. "REQUIREMENT" or "RECOMMENDATION" + self.type = ( + beman_standard_check_config[self.name]["type"] + if "INTERNAL." not in self.name + else "REQUIREMENT" + ) + 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"] + if "INTERNAL." not in self.name + else "" + ) + 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/tools/beman-tidy/beman_tidy/lib/checks/base/directory_base_check.py b/tools/beman-tidy/beman_tidy/lib/checks/base/directory_base_check.py new file mode 100644 index 00000000..d43a39fe --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/checks/base/file_base_check.py b/tools/beman-tidy/beman_tidy/lib/checks/base/file_base_check.py new file mode 100644 index 00000000..bea8eeee --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/__init__.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cmake.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cmake.py new file mode 100644 index 00000000..49d9d24c --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cpp.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/cpp.py new file mode 100644 index 00000000..6868f23e --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py new file mode 100644 index 00000000..95009090 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/directory.py @@ -0,0 +1,197 @@ +#!/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. +# All checks in this file extend the DirectoryBaseCheck class. +# +# Note: DirectoryBaseCheck is not a registered check! +class BemanTreeDirectoryCheck(DirectoryBaseCheck): + """ + Beman tree: ${prefix_path}/beman/${short_name}. + Available via member: self.path + + Examples for a repo named "exemplar": + - include/beman/exemplar + - tests/beman/exemplar + - src/beman/exemplar + + Note: A path can be optional. Actual implementation will be in the derived's check(). + """ + + 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']}", + ) + + +@register_beman_standard_check("DIRECTORY.INTERFACE_HEADERS") +class DirectoryInterfaceHeadersCheck(BemanTreeDirectoryCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "include") + + def pre_check(self): + # Need to override this, because DIRECTORY.INTERFACE_HEADERS is conditional + # (a repo without any interface header files is still valid). + return True + + def check(self): + """ + Check that all public header files reside within the include/beman// directory. + """ + # Exclude certain directories. + exclude_dirs = ["src"] + if self.path.exists(): + exclude_dirs.append(f"include/beman/{self.repo_name}") + if self.repo_name == "exemplar": + exclude_dirs.append("cookiecutter") + + # Find all misplaced public header files in the repository. + header_extensions = [".hpp", ".hxx", ".h", ".hh"] + misplaced_header_files = [] + + for extension in header_extensions: + for p in self.repo_path.rglob(f"*{extension}"): + # Check if any part of the path contains an excluded directory + if not any(excluded in str(p) for excluded in exclude_dirs): + misplaced_header_files.append(p) + + if len(misplaced_header_files) > 0: + for misplaced_header_file in misplaced_header_files: + self.log(f"Misplaced header file found: {misplaced_header_file}") + + self.log( + "Please move all public header files within the include/beman// directory. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directoryinterface_headers for more information." + ) + return False + + # Check passes if the include/beman// directory does not exist + # or all header files are within include/beman// directory. + return True + + def fix(self): + self.log( + f"Please manually move interface header files to include/beman/{self.repo_name}. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directoryinterface_headers for more information." + ) + + +# TODO DIRECTORY.IMPLEMENTATION_HEADERS + + +@register_beman_standard_check("DIRECTORY.SOURCES") +class DirectorySourcesCheck(BemanTreeDirectoryCheck): + """ + Check if the sources directory is src/beman/. + Note: Allow header-only libraries (missing any source files location). + + Example for a repo named "exemplar": src/beman/exemplar + """ + + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "src") + + def pre_check(self): + # Need to override this, because DIRECTORY.SOURCES is conditional + # (a repo without any source files location is still valid - header only libraries) + return True + + def check(self): + # TODO: This is a temporary implementation. Use CMakeLists.txt to actually get the source files location. + # Should not allow other known source locations. + forbidden_source_locations = ["source/", "sources/", "lib/", "library/"] + for forbidden_prefix in forbidden_source_locations: + forbidden_prefix = self.repo_path / forbidden_prefix + if forbidden_prefix.exists(): + self.log( + f"Please move source files from {forbidden_prefix} to src/beman/{self.repo_name}. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directorysources for more information." + ) + return False + + # If `src/` exists, src/beman/ also should exist. + if (self.repo_path / "src/").exists() and not self.path.exists(): + self.log( + f"Please use the required source files location: src/beman/{self.repo_name}. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directorysources for more information." + ) + return False + + # Valid source file location or missing -> Beman Standard compliant. + return True + + def fix(self): + # Because we don't know which is the actually invalid source file locations, + # we cannot do a proper implementation for fix(). + if not self.check(): + self.log( + f"Please manually move sources to src/beman/{self.repo_name}. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directorysources for more information." + ) + + +# TODO DIRECTORY.TESTS + + +# TODO DIRECTORY.EXAMPLES + + +@register_beman_standard_check("DIRECTORY.DOCS") +class DirectoryDocsCheck(DirectoryBaseCheck): + """ + Check if the all documentation files reside within docs/ directory. + Exception: root README.md file. + """ + + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, "docs") + + def pre_check(self): + # Need to override this, because DIRECTORY.DOCS is conditional + # (a repo without any documentation is still valid). + return True + + def check(self): + # Exclude directories that are not part of the documentation. + exclude_dirs = ["papers", ".github"] + if self.path.exists(): + exclude_dirs.append("docs") + if self.repo_name == "exemplar": + exclude_dirs.extend(["cookiecutter", "infra"]) + + # Find all MD files in the repository. + misplaced_md_files = [ + p + for p in self.repo_path.rglob("*.md") + if not any( + excluded in p.parts for excluded in exclude_dirs + ) # exclude files in excluded directories + and p != self.repo_path / "README.md" # exclude root README.md + ] + + # Check if any MD files are misplaced. + if len(misplaced_md_files) > 0: + for misplaced_md_file in misplaced_md_files: + self.log(f"Misplaced MD file found: {misplaced_md_file}") + + self.log( + "Please move all documentation files within the docs/ directory, except for the root README.md file. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directorydocs for more information." + ) + + return False + + # Check passes if there is no docs/ directory or no misplaced MD files are found + return True + + def fix(self): + self.log( + "Please manually move documentation files to the docs/ directory, except for the root README.md file. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#directorydocs for more information." + ) + + +# TODO DIRECTORY.PAPERS diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/file.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/file.py new file mode 100644 index 00000000..b66e8bec --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py new file mode 100644 index 00000000..1071aa89 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/general.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + + +# [GENERAL.*] checks category. +# All checks in this file extend the GeneralBaseCheck class. +# +# Note: GeneralBaseCheck is not a registered check! + + +# TODO LIBRARY.NAMES diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py new file mode 100644 index 00000000..5cfc6e45 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/license.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re +import filecmp +import textwrap + +from ..base.file_base_check import FileBaseCheck +from ..system.registry import register_beman_standard_check +from beman_tidy.lib.utils.git import get_beman_recommendated_license_path + +# [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") + + +@register_beman_standard_check("LICENSE.APPROVED") +class LicenseApprovedCheck(LicenseBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + content = self.read() + + # Regex for LICENSE check + # - fixed header matching + # - non empty body matching + # - fixed footer matching + regex = re.compile( + textwrap.dedent(r""" + ^={78} + The Beman Project is under the (Apache License v2\.0 with LLVM Exceptions|Boost Software License 1\.0|MIT License): + ={78} + + (.+?) + + ={78} + Software from third parties included in the Beman Project: + ={78} + 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\. + """).strip(), + re.DOTALL, + ) + + if not regex.match(content): + self.log( + "LICENSE file does not match the required format. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licenseapproved for more information." + ) + return False + + return True + + def fix(self): + self.log( + "Please update the LICENSE file to include an approved license. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licenseapproved for more information." + ) + + +@register_beman_standard_check("LICENSE.APACHE_LLVM") +class LicenseApacheLLVMCheck(LicenseBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + # Compare LICENSE file stored at self.path with the reference one. + target_license = self.path + ref_license = get_beman_recommendated_license_path() + if not filecmp.cmp(target_license, ref_license, shallow=False): + self.log( + "Please update the LICENSE file to include the Apache License v2.0 with LLVM Exceptions. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licenseapache_llvm for more information." + ) + return False + + return True + + def fix(self): + self.log( + "Please update the LICENSE file to include the Apache License v2.0 with LLVM Exceptions. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licenseapache_llvm for more information." + ) + + +@register_beman_standard_check("LICENSE.CRITERIA") +class LicenseCriteriaCheck(LicenseBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + self.log( + "beman-tidy cannot actually check LICENSE.CRITERIA. Please ignore this message if LICENSE.APPROVED has passed. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licensecriteria for more information." + ) + return True + + def fix(self): + self.log( + "beman-tidy cannot actually check LICENSE.CRITERIA. Please ignore this message if LICENSE.APPROVED has passed. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#licensecriteria for more information." + ) diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py new file mode 100644 index 00000000..5a231cae --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/readme.py @@ -0,0 +1,154 @@ +#!/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, + check .beman-standard.yml for the desired format. + """ + + def validate_badges(category, badges): + if category == "library_status": + assert len(badges) == 4 # The number of library maturity model states. + elif category == "standard_target": + assert ( + len(badges) == 2 + ) # The number of standard targets specified in the Beman Standard. + + def count_badges(badges): + return len([badge for badge in badges if self.has_content(badge)]) + + count_failed = 0 + for category_data in self.config["values"]: + category = list(category_data.keys())[0] + badges = category_data[category] + validate_badges(category, badges) + + if count_badges(badges) != 1: + self.log( + f"The file '{self.path}' does not contain exactly one required badge of category '{category}'." + ) + count_failed += 1 + + return count_failed == 0 + + def fix(self): + self.log( + "Please add required badges in README.md file. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#readmebadges for the desired format." + ) + return True + + +@register_beman_standard_check("README.IMPLEMENTS") +class ReadmeImplementsCheck(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() + + # Match the pattern to start with "Implements:" and then have a paper reference and a wg21.link URL. + # Examples of valid lines: + # **Implements**: [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). + # **Implements**: [Give *std::optional* Range Support (P3168R2)](https://wg21.link/P3168R2) and [`std::optional` (P2988R5)](https://wg21.link/P2988R5) + # **Implements**: [.... (PxyzwRr)](https://wg21.link/PxyzwRr), [.... (PabcdRr)](https://wg21.link/PabcdRr), and [.... (PijklRr)](https://wg21.link/PijklRr), + regex = r"^\*\*Implements\*\*:\s+.*\bP\d{4}R\d+\b.*wg21\.link/\S+" + + # Count how many lines match the regex + implement_lines = 0 + for line in lines: + if re.match(regex, line): + implement_lines += 1 + + # If there is exactly one "Implements:" line, it is valid + if implement_lines == 1: + return True + + # Invalid/missing/duplicate "Implements:" line + self.log( + f"Invalid/missing/duplicate 'Implements:' line in '{self.path}'. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#readmeimplements for more information." + ) + return False + + def fix(self): + self.log( + "Please write a Implements line in README.md file. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#readmeimplements for the desired format." + ) + return True + + +@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): + self.log( + "Please write a Status line in README.md file. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#readmelibrary_status for the desired format." + ) + return True diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py new file mode 100644 index 00000000..1d306b8f --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/release.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re +from beman_tidy.lib.checks.base.base_check import BaseCheck +from beman_tidy.lib.checks.beman_standard.readme import ReadmeBaseCheck +from ..system.registry import register_beman_standard_check + +# [RELEASE.*] checks category. +# Note: Data is stored online - e.g. https://github.com/bemanproject/exemplar/releases +# beman-tidy is an offline tool, so it cannot check these issues, +# but for some of them it will try some heuristics. + + +@register_beman_standard_check("RELEASE.GITHUB") +class ReleaseGithubCheck(BaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + # Need to always return True, as beman-tidy is an offline tool + # that does not have access to the GitHub API. + self.log( + "beman-tidy cannot check this issue. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#releasegithub." + ) + return True + + def fix(self): + self.log( + "beman-tidy cannot fix this issue. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#releasegithub." + ) + + +# TODO RELEASE.NOTES +@register_beman_standard_check("RELEASE.NOTES") +class ReleaseNotesCheck(BaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + # Need to always return True, as beman-tidy is an offline tool + # that does not have access to the GitHub API. + self.log( + "beman-tidy cannot check this issue. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#releasenotes." + ) + return True + + def fix(self): + self.log( + "beman-tidy cannot fix this issue. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#releasenotes." + ) + + +@register_beman_standard_check("RELEASE.GODBOLT_TRUNK_VERSION") +class ReleaseGodboltTrunkVersionCheck(ReadmeBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + """ + Check that the Godbolt badge is present in the root README.md file. + If present, this assumes that the trunk version is available on Godbolt. + + e.g. [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/Gc6Y9j6zf) + Note: Only the suffix of the https://godbolt.org/z/* has dynamic content. + """ + + content = self.read() + regex = re.compile( + r"\[!\[Compiler Explorer Example\]\(https://img\.shields\.io/badge/Try%20it%20on%20Compiler%20Explorer-grey\?logo=compilerexplorer&logoColor=67c52a\)\]\(https://godbolt\.org/z/([a-zA-Z0-9]+)\)" + ) + if not re.search(regex, content): + self.log( + f"The file '{self.path}' does not contain a Compiler Explorer badge - trunk version assumed to be missing." + ) + return False + + return True + + def fix(self): + self.log( + "beman-tidy cannot fix this issue. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#releasegodbolt_trunk_version." + ) diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/repository.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/repository.py new file mode 100644 index 00000000..c043c5d0 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/repository.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import re +import textwrap + +from ..base.file_base_check import FileBaseCheck +from ..base.base_check import BaseCheck +from ..system.registry import register_beman_standard_check +from ...utils.string import is_beman_snake_case + +# [REPOSITORY.*] checks category. +# All checks in this file extend the FileBaseCheck class. +# +# Note: FileBaseCheck is not a registered check! + + +@register_beman_standard_check("REPOSITORY.NAME") +class RepositoryNameCheck(BaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + repo_name = self.repo_info["name"] + if not is_beman_snake_case(repo_name): + self.log( + "The repository should be named after the library name excluding the 'beman.' prefix. It should not contain a target C++ version. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositoryname for more information." + ) + return False + + return True + + def fix(self): + self.log( + "beman-tidy can't automatically fix the repository name. " + "Please see https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositoryname for more information." + ) + pass + + +@register_beman_standard_check("REPOSITORY.CODEOWNERS") +class RepositoryCodeownersCheck(FileBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, ".github/CODEOWNERS") + + def check(self): + # Since this class simply checks for the existence of a CODEOWNERS file, + # there's nothing more to do than the default pre-check. + return super().pre_check() + + def fix(self): + self.log( + "Please add a CODEOWNERS file to the repository. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositorycodeowners for more information." + ) + + +@register_beman_standard_check("REPOSITORY.DEFAULT_BRANCH") +class RepositoryDefaultBranchCheck(BaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config) + + def check(self): + default_branch = self.repo_info["default_branch"] + if default_branch != "main": + self.log(f"Invalid default branch in repo: {default_branch} vs 'main'.") + return False + + return True + + def fix(self): + self.log( + "Please set `main` as default branch in the repository. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositorydefault_branch for more information." + ) + + +@register_beman_standard_check("REPOSITORY.DISALLOW_GIT_SUBMODULES") +class RepositoryDisallowGitSubmodulesCheck(FileBaseCheck): + def __init__(self, repo_info, beman_standard_check_config): + super().__init__(repo_info, beman_standard_check_config, ".gitmodules") + + def pre_check(self): + # Need to override this, because REPOSITORY.DISALLOW_GIT_SUBMODULES is conditional + return True + + def check(self): + if self.path.exists(): + content = self.read() + + # Regex pattern to match "wg21" submodule + regex = re.compile( + textwrap.dedent(r""" + ^\[submodule \"(?:.+?/)?wg21\"] + \tpath = papers/(.+?) + \turl = https://github.com/mpark/wg21.git$ + """).strip() + ) + + if not regex.match(content): + self.log( + "The repository should not use git submodules. Please remove them. " + "Known exception: wg21. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositorydisallow_git_submodules for more information." + ) + return False + + # Check passes if .gitmodules file doesn't exist or if it only contains wg21 submodule. + return True + + def fix(self): + self.log( + "beman-tidy can't automatically fix the repository submodules. " + "See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#repositorydisallow_git_submodules for more information." + ) diff --git a/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py new file mode 100644 index 00000000..8e4bfbb1 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/beman_standard/toplevel.py @@ -0,0 +1,60 @@ +#!/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("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): + # Since this class simply checks for the existence of a CMakeLists.txt file, + # there's nothing more to do than the default pre-check. + return super().pre_check() + + def fix(self): + self.log( + "Please add a CMakeLists.txt file to the repository. See https://github.com/bemanproject/beman/blob/main/docs/BEMAN_STANDARD.md#toplevelcmake for the desired format." + ) + + +@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#toplevellicense 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#toplevelreadme for the desired format." + ) diff --git a/tools/beman-tidy/beman_tidy/lib/checks/system/__init__.py b/tools/beman-tidy/beman_tidy/lib/checks/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/checks/system/git.py b/tools/beman-tidy/beman_tidy/lib/checks/system/git.py new file mode 100644 index 00000000..368cb6db --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/checks/system/git.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import sys + +from ..base.base_check import BaseCheck + + +class DisallowFixInplaceAndUnstagedChangesCheck(BaseCheck): + """ + --fix-inplace requires no unstaged changes. + """ + + def __init__(self, repo_info, beman_standard_check_config): + super().__init__( + repo_info, beman_standard_check_config, "INTERNAL.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-inplace requires no unstaged changes. Please commit or stash your changes. STOP." + ) + sys.exit(1) diff --git a/tools/beman-tidy/beman_tidy/lib/checks/system/registry.py b/tools/beman-tidy/beman_tidy/lib/checks/system/registry.py new file mode 100644 index 00000000..cabb8ac2 --- /dev/null +++ b/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/tools/beman-tidy/beman_tidy/lib/pipeline.py b/tools/beman-tidy/beman_tidy/lib/pipeline.py new file mode 100644 index 00000000..772f065f --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/pipeline.py @@ -0,0 +1,194 @@ +#!/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.repository 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=True) + + 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/tools/beman-tidy/beman_tidy/lib/utils/__init__.py b/tools/beman-tidy/beman_tidy/lib/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/beman_tidy/lib/utils/git.py b/tools/beman-tidy/beman_tidy/lib/utils/git.py new file mode 100644 index 00000000..a37e362c --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/utils/git.py @@ -0,0 +1,133 @@ +#!/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 default branch + split_head = repo.git.symbolic_ref("refs/remotes/origin/HEAD").split("/") + default_branch = split_head[-1] + + # 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, + "default_branch": default_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 get_beman_recommendated_license_path(): + """ + Get the path to the Beman recommended license file. + """ + return Path(__file__).parent.parent.parent / "LICENSE" + + +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/tools/beman-tidy/beman_tidy/lib/utils/string.py b/tools/beman-tidy/beman_tidy/lib/utils/string.py new file mode 100644 index 00000000..e9ccf705 --- /dev/null +++ b/tools/beman-tidy/beman_tidy/lib/utils/string.py @@ -0,0 +1,45 @@ +#!/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) and not re.match(".*[0-9]+$", name) + ) + + +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/tools/beman-tidy/beman_tidy/lib/utils/terminal.py b/tools/beman-tidy/beman_tidy/lib/utils/terminal.py new file mode 100644 index 00000000..6ba12977 --- /dev/null +++ b/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/tools/beman-tidy/docs/dev-guide.md b/tools/beman-tidy/docs/dev-guide.md new file mode 100644 index 00000000..a6e3bc1b --- /dev/null +++ b/tools/beman-tidy/docs/dev-guide.md @@ -0,0 +1,136 @@ +# 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. +* `beman-tidy` can access configuration files shipped with the tool itself (e.g., `.beman-standard.yml` or `LICENSE`). All such files must be in the `beman_tidy/` directory to be automatically available in exported packages. It cannot access files from the repository itself (e.g., `infra/LICENSE` or `infra/tools/beman-tidy/README.md`). + +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/tools/beman-tidy/pylock.toml b/tools/beman-tidy/pylock.toml new file mode 100644 index 00000000..8fb1d543 --- /dev/null +++ b/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/tools/beman-tidy/pyproject.toml b/tools/beman-tidy/pyproject.toml new file mode 100644 index 00000000..fdc1d372 --- /dev/null +++ b/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/tools/beman-tidy/tests/__init__.py b/tools/beman-tidy/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/conftest.py b/tools/beman-tidy/tests/conftest.py new file mode 100644 index 00000000..430a7be8 --- /dev/null +++ b/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/tools/beman-tidy/tests/lib/__init__.py b/tools/beman-tidy/tests/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/__init__.py b/tools/beman-tidy/tests/lib/checks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/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/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/ci.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/ci.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/ci.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/include/beman/optional/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/include/beman/optional/identity.hpp new file mode 100644 index 00000000..cc08c754 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/include/beman/optional/identity.hpp @@ -0,0 +1 @@ +// Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/src/beman/optional/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/src/beman/optional/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v1/src/beman/optional/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/debug/ci.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/debug/ci.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/debug/ci.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/include/beman/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/include/beman/identity.hpp new file mode 100644 index 00000000..cc08c754 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/include/beman/identity.hpp @@ -0,0 +1 @@ +// Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/src/beman/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/src/beman/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v2/src/beman/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/ci.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/ci.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/ci.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/dev/lint.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/dev/lint.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/dev/lint.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/identity.hpp new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/include/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/include/identity.hpp new file mode 100644 index 00000000..cc08c754 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/include/identity.hpp @@ -0,0 +1 @@ +// Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/src/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/src/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v3/src/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/debug/ci.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/debug/ci.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/debug/ci.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/dev/lint.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/dev/lint.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/dev/lint.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/local.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/local.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/local.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/optional.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/optional.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/documentation/optional.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/identity.hpp new file mode 100644 index 00000000..cc08c754 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/identity.hpp @@ -0,0 +1 @@ +// Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/sources/beman/exemplar/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/sources/beman/exemplar/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v4/sources/beman/exemplar/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v5/source/beman/exemplar/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v5/source/beman/exemplar/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v5/source/beman/exemplar/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v6/lib/beman/exemplar/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v6/lib/beman/exemplar/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v6/lib/beman/exemplar/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v7/library/beman/exemplar/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v7/library/beman/exemplar/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/invalid/repo-exemplar-v7/library/beman/exemplar/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/include/beman/exemplar/identity.hpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/include/beman/exemplar/identity.hpp new file mode 100644 index 00000000..cc08c754 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/include/beman/exemplar/identity.hpp @@ -0,0 +1 @@ +// Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/src/beman/exemplar/exemplar.cpp b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/src/beman/exemplar/exemplar.cpp new file mode 100644 index 00000000..fe827578 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v1/src/beman/exemplar/exemplar.cpp @@ -0,0 +1 @@ +// Dummy content, this file is used only for testing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/.keep b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/debug/ci.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/debug/ci.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/debug/ci.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/dev/lint.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/dev/lint.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/dev/lint.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/local.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/local.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/local.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/optional.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/optional.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v2/docs/optional.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/README.md new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/README.md @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/abstract.bst b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/abstract.bst new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/abstract.bst @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/dummy.tex b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/dummy.tex new file mode 100644 index 00000000..2acefc8e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/data/valid/repo-exemplar-v3/papers/P2988/dummy.tex @@ -0,0 +1 @@ +Dummy content. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/directory/test_directory.py b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/test_directory.py new file mode 100644 index 00000000..5bb83818 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/directory/test_directory.py @@ -0,0 +1,177 @@ +#!/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, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.directory import ( + DirectorySourcesCheck, + DirectoryInterfaceHeadersCheck, + DirectoryDocsCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/directory/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__DIRECTORY_INTERFACE_HEADERS__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with interface headers reside within include/beman//. + """ + valid_interface_headers_paths = [ + # exemplar/ repo with include/beman/exemplar/ and a header file. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with no include/beman/exemplar/ + Path(f"{valid_prefix}/repo-exemplar-v2/"), + ] + + run_check_for_each_path( + True, + valid_interface_headers_paths, + DirectoryInterfaceHeadersCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__DIRECTORY_INTERFACE_HEADERS__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with interface headers don't reside within include/beman//. + """ + invalid_interface_headers_paths = [ + # Headers in include/beman/optional - wrong inner directory. + Path(f"{invalid_prefix}/repo-exemplar-v1"), + # Headers in include/beman/ - missing 2nd subdirectory. + Path(f"{invalid_prefix}/repo-exemplar-v2"), + # Headers in include/ - missing 1st and 2nd subdirectories. + Path(f"{invalid_prefix}/repo-exemplar-v3"), + # Headers in toplevel directory. + Path(f"{invalid_prefix}/repo-exemplar-v4"), + ] + + run_check_for_each_path( + False, + invalid_interface_headers_paths, + DirectoryInterfaceHeadersCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__DIRECTORY_INTERFACE_HEADERS__fix_inplace( + repo_info, beman_standard_check_config +): + pass + + +def test__DIRECTORY_SOURCES__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with sources and headers not part of the public interface reside in src/ + """ + valid_source_paths = [ + # exemplar/ repo with src/beman/exemplar/ - valid source tree. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo without src/ - no source files (header-only). + Path(f"{valid_prefix}/repo-exemplar-v2/"), + ] + + run_check_for_each_path( + True, + valid_source_paths, + DirectorySourcesCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__DIRECTORY_SOURCES__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with sources and headers not part of the public interface don't reside in src/ + """ + invalid_source_paths = [ + # Sources in src/beman/optional - wrong inner directory. + Path(f"{invalid_prefix}/repo-exemplar-v1"), + # Sources in src/beman/ - missing 3rd subdirectory. + Path(f"{invalid_prefix}/repo-exemplar-v2"), + # Sources in src/ - missing 2nd and 3rd subdirectories. + Path(f"{invalid_prefix}/repo-exemplar-v3"), + # Sources in sources/ - wrong prefix. + Path(f"{invalid_prefix}/repo-exemplar-v4"), + # Sources in source/ - wrong prefix. + Path(f"{invalid_prefix}/repo-exemplar-v5"), + # Sources in lib/ - wrong prefix. + Path(f"{invalid_prefix}/repo-exemplar-v6"), + # Sources in library/ - wrong prefix. + Path(f"{invalid_prefix}/repo-exemplar-v7"), + ] + + run_check_for_each_path( + False, + invalid_source_paths, + DirectorySourcesCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__DIRECTORY_SOURCES__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__DIRECTORY_DOCS__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid documentation structure pass the check. + """ + valid_docs_paths = [ + # exemplar/ repo without docs/ dir and with root README.md. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with docs/ dir and root README.md. + Path(f"{valid_prefix}/repo-exemplar-v2/"), + # exemplar/ repo with papers/ dir and root README.md. + Path(f"{valid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + True, + valid_docs_paths, + DirectoryDocsCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__DIRECTORY_DOCS__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid documentation structure fail the check. + """ + invalid_docs_paths = [ + # Misplaced MD files in root directory. + Path(f"{invalid_prefix}/repo-exemplar-v1"), + # Misplaced MD files in root subdirectories. + Path(f"{invalid_prefix}/repo-exemplar-v2"), + # Misplaced MD files in root directory and root subdirectories. + Path(f"{invalid_prefix}/repo-exemplar-v3"), + # Wrong name for docs/ directory. + Path(f"{invalid_prefix}/repo-exemplar-v4"), + ] + + run_check_for_each_path( + False, + invalid_docs_paths, + DirectoryDocsCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__DIRECTORY_DOCS__fix_inplace(repo_info, beman_standard_check_config): + pass diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/license/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/license/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/license/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/license/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v1 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v1 new file mode 100644 index 00000000..e608a181 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v1 @@ -0,0 +1,14 @@ +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v2 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v2 new file mode 100644 index 00000000..5000cedd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v2 @@ -0,0 +1,6 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v3 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v3 new file mode 100644 index 00000000..57d4582d --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v3 @@ -0,0 +1,15 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v4 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v4 new file mode 100644 index 00000000..993db3e2 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v4 @@ -0,0 +1,18 @@ +============================================================================== +The "Extra" Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v5 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v5 new file mode 100644 index 00000000..87d0a42a --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v5 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +"Extra" 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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v6 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v6 new file mode 100644 index 00000000..d0c84391 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v6 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the Boost Software License 1.0: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v7 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v7 new file mode 100644 index 00000000..63876eff --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/invalid/invalid-LICENSE-v7 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the MIT License: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v1 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v1 new file mode 100644 index 00000000..1f2a2cf2 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v1 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v2 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v2 new file mode 100644 index 00000000..d0c84391 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v2 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the Boost Software License 1.0: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v3 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v3 new file mode 100644 index 00000000..63876eff --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v3 @@ -0,0 +1,18 @@ +============================================================================== +The Beman Project is under the MIT License: +============================================================================== + +# Dummy LICENSE lines. +# beman-tidy does not actually check the content between the header and the footer. + +============================================================================== +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/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v4 b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v4 new file mode 100644 index 00000000..0873f35a --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/data/valid/valid-LICENSE-v4 @@ -0,0 +1,234 @@ +============================================================================== +The Beman Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + 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/tools/beman-tidy/tests/lib/checks/beman_standard/license/test_license.py b/tools/beman-tidy/tests/lib/checks/beman_standard/license/test_license.py new file mode 100644 index 00000000..4a6fab93 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/license/test_license.py @@ -0,0 +1,145 @@ +#!/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, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.license import ( + LicenseApprovedCheck, + LicenseApacheLLVMCheck, + LicenseCriteriaCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/license/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__LICENSE_APPROVED__valid(repo_info, beman_standard_check_config): + """ + Test that a valid LICENSE file passes the check. + """ + valid_license_paths = [ + # Apache License v2.0 with LLVM Exceptions + Path(f"{valid_prefix}/valid-LICENSE-v1"), + # Boost Software License 1.0 + Path(f"{valid_prefix}/valid-LICENSE-v2"), + # MIT License + Path(f"{valid_prefix}/valid-LICENSE-v3"), + ] + + run_check_for_each_path( + True, + valid_license_paths, + LicenseApprovedCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__LICENSE_APPROVED__invalid(repo_info, beman_standard_check_config): + """ + Test that an invalid LICENSE file fails the check. + """ + invalid_license_paths = [ + # Almost valid LICENSE, but without header + Path(f"{invalid_prefix}/invalid-LICENSE-v1"), + # Almost valid LICENSE, but without footer + Path(f"{invalid_prefix}/invalid-LICENSE-v2"), + # Almost valid LICENSE, but not content between header and footer + Path(f"{invalid_prefix}/invalid-LICENSE-v3"), + # Almost valid LICENSE, but not exact header + Path(f"{invalid_prefix}/invalid-LICENSE-v4"), + # Almost valid LICENSE, but not exact footer + Path(f"{invalid_prefix}/invalid-LICENSE-v5"), + ] + + run_check_for_each_path( + False, + invalid_license_paths, + LicenseApprovedCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__LICENSE_APPROVED__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__LICENSE_APACHE_LLVM__valid(repo_info, beman_standard_check_config): + """ + Test that a LICENSE file with Apache LLVM passes the check. + """ + valid_license_paths = [ + # Apache License v2.0 with LLVM Exceptions is the only one compatible with LICENSE.APACHE_LLVM. + Path(f"{valid_prefix}/valid-LICENSE-v4"), + ] + + run_check_for_each_path( + True, + valid_license_paths, + LicenseApacheLLVMCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__LICENSE_APACHE_LLVM__invalid(repo_info, beman_standard_check_config): + """ + Test that a LICENSE file without Apache LLVM fails the check. + """ + invalid_license_paths = [ + # Boost Software License 1.0 is LICENSE.APPROVED compatible, but not compatible with LICENSE.APACHE_LLVM. + Path(f"{invalid_prefix}/invalid-LICENSE-v6"), + # MIT License is LICENSE.APPROVED compatible, but not compatible with LICENSE.APACHE_LLVM. + Path(f"{invalid_prefix}/invalid-LICENSE-v7"), + ] + + run_check_for_each_path( + False, + invalid_license_paths, + LicenseApacheLLVMCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__LICENSE_APACHE_LLVM__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__LICENSE_CRITERIA__valid(repo_info, beman_standard_check_config): + valid_license_paths = [ + # LICENSE.CRITERIA is always true, e.g. for valid file with Apache License v2.0 with LLVM Exceptions. + Path(f"{valid_prefix}/valid-LICENSE-v1"), + # LICENSE.CRITERIA is always true, e.g. for invalid file. + Path(f"{invalid_prefix}/invalid-LICENSE-v1"), + ] + + run_check_for_each_path( + True, + valid_license_paths, + LicenseCriteriaCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__LICENSE_CRITERIA__invalid(repo_info, beman_standard_check_config): + # LICENSE.CRITERIA cannot be invalid. Check license.py. + pass + + +@pytest.mark.skip(reason="NOT implemented") +def test__LICENSE_CRITERIA__fix_inplace(repo_info, beman_standard_check_config): + # LICENSE.CRITERIA cannot be invalid, so no need for fix inplace. Check license.py. + pass diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md new file mode 100644 index 00000000..a322a55a --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v1.md @@ -0,0 +1,15 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![Library typo 1 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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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 badge for library status. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v2.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v2.md new file mode 100644 index 00000000..13016f51 --- /dev/null +++ b/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 + + + + +![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) ![Standard typo 2 Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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 badge for standard target. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v3.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v3.md new file mode 100644 index 00000000..128e89ea --- /dev/null +++ b/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 + + + + +![other description 1](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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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: other description in badge for library status. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v4.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v4.md new file mode 100644 index 00000000..fc816d01 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v4.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) ![other description 2](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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: other description in badge for standard target. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v5.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v5.md new file mode 100644 index 00000000..e5a11728 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v5.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_non-sense.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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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: non-sense badge for library status (broken badge URL). diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v6.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v6.md new file mode 100644 index 00000000..466d651a --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v6.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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp-non-sense.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: non-sense badge for standard target (broken badge URL). diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v7.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v7.md new file mode 100644 index 00000000..2e3d6f8f --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v7.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: 1/2 badges are missing (standard target). diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v8.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v8.md new file mode 100644 index 00000000..11d4cc46 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-badge-v8.md @@ -0,0 +1,15 @@ +# beman.exemplar: A Beman Library Exemplar + + + + +![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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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: 1/2 badges are missing (library status). diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v1.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v1.md new file mode 100644 index 00000000..80cc4cea --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v1.md @@ -0,0 +1,29 @@ +# 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](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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v2.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v2.md new file mode 100644 index 00000000..effdfcf7 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v2.md @@ -0,0 +1,29 @@ +# 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)]. + +**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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v3.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v3.md new file mode 100644 index 00000000..d403ac9c --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v3.md @@ -0,0 +1,29 @@ +# 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](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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v4.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v4.md new file mode 100644 index 00000000..93ada4cd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-implements-v4.md @@ -0,0 +1,31 @@ +# 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). + +**Implements**: [Give _std::optional_ Range Support (P3168R2)](https://wg21.link/P3168R2) and [`std::optional` (P2988R5)](https://wg21.link/P2988R5) + +**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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v1.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v2.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-status-line-v3.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v1.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v2.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid-title-v3.md b/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/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/invalid/invalid.md new file mode 100644 index 00000000..7048d188 --- /dev/null +++ b/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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v1.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v1.md new file mode 100644 index 00000000..87e25182 --- /dev/null +++ b/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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v2.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v2.md new file mode 100644 index 00000000..f476b4b0 --- /dev/null +++ b/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_production_ready_api_may_undergo_changes.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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v3.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v3.md new file mode 100644 index 00000000..d742db1e --- /dev/null +++ b/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_production_ready_stable_api.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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v4.md b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/data/valid/README-v4.md new file mode 100644 index 00000000..bf9f2038 --- /dev/null +++ b/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_retired.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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py new file mode 100644 index 00000000..1f3e5452 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/readme/test_readme.py @@ -0,0 +1,250 @@ +#!/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, + ReadmeImplementsCheck, + 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 = [ + # Title: Wrong Title Format + Path(f"{invalid_prefix}/invalid.md"), + # Title: Missing . in beman.exemplar + Path(f"{invalid_prefix}/invalid-title-v1.md"), + # Title: Missing : after beman.exemplar + Path(f"{invalid_prefix}/invalid-title-v2.md"), + # Title: Wromg name beman.exemaplar vs beman.optional + Path(f"{invalid_prefix}/invalid-title-v3.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"), + ] + + 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 = [ + # Badges: under development status and cpp26 target + Path(f"{valid_prefix}/README-v1.md"), + # Badges: production ready (api may undergo changes) status and cpp26 target + Path(f"{valid_prefix}/README-v2.md"), + # Badges: production ready (stable api) status and cpp29 target + Path(f"{valid_prefix}/README-v3.md"), + # Badges: retired status and cpp26 target + 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"), + # Badges: typos in badge for library status + Path(f"{invalid_prefix}/invalid-badge-v1.md"), + # Badges: typos in badge for standard target + Path(f"{invalid_prefix}/invalid-badge-v2.md"), + # Badges: other description in badge for library status + Path(f"{invalid_prefix}/invalid-badge-v3.md"), + # Badges: other description in badge for standard target + Path(f"{invalid_prefix}/invalid-badge-v4.md"), + # Badges: non-sense badge for library status (broken badge URL) + Path(f"{invalid_prefix}/invalid-badge-v5.md"), + # Badges: non-sense badge for standard target (broken badge URL) + Path(f"{invalid_prefix}/invalid-badge-v6.md"), + # Badges: 1/2 badges are missing (standard target) + Path(f"{invalid_prefix}/invalid-badge-v7.md"), + # Badges: 1/2 badges are missing (library status) + Path(f"{invalid_prefix}/invalid-badge-v8.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. + """ + # Cannot determine what badges to create. fix() is not implemented. + pass + + +def test__README_IMPLEMENTS__valid(repo_info, beman_standard_check_config): + """ + Test that a valid README.md "Implements" 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, + ReadmeImplementsCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__README_IMPLEMENTS__invalid(repo_info, beman_standard_check_config): + """ + Test that an invalid README.md "Implements" fails the check + """ + invalid_readme_paths = [ + Path(f"{invalid_prefix}/invalid.md"), + Path(f"{invalid_prefix}/invalid-implements-v1.md"), + Path(f"{invalid_prefix}/invalid-implements-v2.md"), + Path(f"{invalid_prefix}/invalid-implements-v3.md"), + Path(f"{invalid_prefix}/invalid-implements-v4.md"), + ] + + run_check_for_each_path( + False, + invalid_readme_paths, + ReadmeImplementsCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__README_IMPLEMENTS__fix_inplace(repo_info, beman_standard_check_config): + """ + Test that the fix method corrects an invalid README.md "Implements". + """ + # Cannot determine what implements to create. fix() is not implemented. + 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. + """ + # Cannot determine what library status to create. fix() is not implemented. + pass diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/release/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/release/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/release/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/release/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/release/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/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/invalid/repo-exemplar-v1/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/invalid/repo-exemplar-v1/README.md new file mode 100644 index 00000000..a4326df7 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/invalid/repo-exemplar-v1/README.md @@ -0,0 +1,28 @@ +# 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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.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. + +This is an invalid README.md file that follows the Beman Standard: the Godbolt badge is missing. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/valid/repo-exemplar-v1/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/valid/repo-exemplar-v1/README.md new file mode 100644 index 00000000..c1ddce1f --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/release/data/valid/repo-exemplar-v1/README.md @@ -0,0 +1,28 @@ +# 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) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.svg) [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/Gc6Y9j6zf) + + +`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. + +This is a valid README.md file that follows the Beman Standard: the Godbolt badge is present. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/release/test_release.py b/tools/beman-tidy/tests/lib/checks/beman_standard/release/test_release.py new file mode 100644 index 00000000..ebc199d3 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/release/test_release.py @@ -0,0 +1,140 @@ +#!/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, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.release import ( + ReleaseGithubCheck, + ReleaseGodboltTrunkVersionCheck, + ReleaseNotesCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/release/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__RELEASE_GITHUB__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with GitHub release pass the check. + """ + valid_repo_paths = [ + # RELEASE.GITHUB always passes, as beman-tidy is an offline tool. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_repo_paths, + ReleaseGithubCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__RELEASE_GITHUB__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with missing GitHub release fail the check. + """ + # RELEASE.GITHUB always passes, as beman-tidy is an offline tool. + pass + + +@pytest.mark.skip(reason="NOT implemented") +def test__RELEASE_GITHUB__fix_inplace(repo_info, beman_standard_check_config): + # RELEASE.GITHUB always passes, as beman-tidy is an offline tool. + pass + + +def test__RELEASE_NOTES__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with release notes pass the check. + """ + valid_repo_paths = [ + # RELEASE.NOTES always passes, as beman-tidy is an offline tool. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_repo_paths, + ReleaseNotesCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__RELEASE_NOTES__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with missing release notes fail the check. + """ + # RELEASE.NOTES always passes, as beman-tidy is an offline tool. + pass + + +@pytest.mark.skip(reason="NOT implemented") +def test__RELEASE_NOTES__fix_inplace(repo_info, beman_standard_check_config): + """ + Test that repositories with missing release notes fail the check. + """ + # RELEASE.NOTES always passes, as beman-tidy is an offline tool. + pass + + +def test__RELEASE_GODBOLT_TRUNK_VERSION__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with present Godbolt trunk version pass the check. + """ + valid_godbolt_trunk_version_paths = [ + # exemplar/ repo with root README.md containing valid Godbolt trunk version. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_godbolt_trunk_version_paths, + ReleaseGodboltTrunkVersionCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__RELEASE_GODBOLT_TRUNK_VERSION__invalid( + repo_info, beman_standard_check_config +): + """ + Test that repositories with missing Godbolt trunk version fail the check. + """ + invalid_godbolt_trunk_version_paths = [ + # exemplar/ repo root README.md without Godbolt trunk version. + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + False, + invalid_godbolt_trunk_version_paths, + ReleaseGodboltTrunkVersionCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__RELEASE_GODBOLT_TRUNK_VERSION__fix_inplace( + repo_info, beman_standard_check_config +): + """ + Test that the fix method corrects an invalid README.md Godbolt trunk version. + """ + # Cannot determine what Godbolt trunk version to create. fix() is not implemented. + pass diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/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/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v1/.github/.keep b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v1/.github/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v1/.gitmodules b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v1/.gitmodules new file mode 100644 index 00000000..d438b89b --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v1/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dummy"] + path = dummy + url = https://github.com/cplusplus/dummy diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v2/.gitmodules b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v2/.gitmodules new file mode 100644 index 00000000..cb06c9ef --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v2/.gitmodules @@ -0,0 +1,9 @@ +[submodule "dummy1"] + path = dummy1 + url = https://github.com/dummy1.git +[submodule "wg21"] + path = wg21 + url = https://github.com/mpark/wg21.git +[submodule "dummy2"] + path = dummy2 + url = https://github.com/cplusplus/dummy2 diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v2/CODEOWNERS b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v2/CODEOWNERS new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v3/.gihub/CODEOWNERS b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v3/.gihub/CODEOWNERS new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v3/.gitmodules b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v3/.gitmodules new file mode 100644 index 00000000..50ecb2b4 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/invalid/repo-exemplar-v3/.gitmodules @@ -0,0 +1,6 @@ +[submodule "dummy1"] + path = dummy1 + url = https://github.com/dummy1.git +[submodule "dummy2"] + path = dummy2 + url = https://github.com/cplusplus/dummy2 diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v1/.github/CODEOWNERS b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v1/.github/CODEOWNERS new file mode 100644 index 00000000..dcd78f59 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v1/.github/CODEOWNERS @@ -0,0 +1 @@ +* @neatudarius diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v2/.gitmodules b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v2/.gitmodules new file mode 100644 index 00000000..3211848a --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v2/.gitmodules @@ -0,0 +1,3 @@ +[submodule "wg21"] + path = papers/wg21 + url = https://github.com/mpark/wg21.git diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v3/.gitmodules b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v3/.gitmodules new file mode 100644 index 00000000..b44d8c09 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/data/valid/repo-exemplar-v3/.gitmodules @@ -0,0 +1,3 @@ +[submodule "papers/wg21"] + path = papers/P2988/wg21 + url = https://github.com/mpark/wg21.git diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/repository/test_repository.py b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/test_repository.py new file mode 100644 index 00000000..7538f77f --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/repository/test_repository.py @@ -0,0 +1,225 @@ +#!/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_check_for_each_repo_info, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.repository import ( + RepositoryNameCheck, + RepositoryCodeownersCheck, + RepositoryDefaultBranchCheck, + RepositoryDisallowGitSubmodulesCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/repository/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__REPOSITORY_NAME__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid repository names pass the check. + """ + # Create mock repo info with valid repository names + valid_repo_infos = [ + repo_info.copy() | {"name": "exemplar"}, + repo_info.copy() | {"name": "optional"}, + repo_info.copy() | {"name": "smart_pointer"}, + repo_info.copy() | {"name": "execution"}, + repo_info.copy() | {"name": "utf_view"}, + repo_info.copy() | {"name": "net"}, + ] + + run_check_for_each_repo_info( + True, + RepositoryNameCheck, + valid_repo_infos, + beman_standard_check_config, + ) + + +def test__REPOSITORY_NAME__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid repository names fail the check. + """ + # Create mock repo info with invalid repository names + invalid_repo_infos = [ + repo_info.copy() | {"name": "beman.exemplar"}, + repo_info.copy() | {"name": "exemplar26"}, + repo_info.copy() | {"name": "beman.exemplar26"}, + repo_info.copy() | {"name": "exemplar_"}, + repo_info.copy() | {"name": "_exemplar"}, + repo_info.copy() | {"name": "optional26"}, + repo_info.copy() | {"name": "execution26"}, + repo_info.copy() | {"name": "net29"}, + ] + + run_check_for_each_repo_info( + False, + RepositoryNameCheck, + invalid_repo_infos, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__REPOSITORY_NAME__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__REPOSITORY_CODEOWNERS__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid CODEOWNERS pass the check. + """ + valid_codeowners_paths = [ + # exemplar/ repo with valid .github/CODEOWNERS file. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_codeowners_paths, + RepositoryCodeownersCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__REPOSITORY_CODEOWNERS__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid CODEOWNERS fail the check. + """ + invalid_codeowners_paths = [ + # exemplar/ repo without CODEOWNERS file inside .github/. + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with CODEOWNERS in root. + Path(f"{invalid_prefix}/repo-exemplar-v2/"), + # exemplar/ repo with empty .github/CODEOWNERS. + Path(f"{invalid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + False, + invalid_codeowners_paths, + RepositoryCodeownersCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__REPOSITORY_CODEOWNERS__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__REPOSITORY_DEFAULT_BRANCH__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid default branch pass the check. + """ + # Create mock repo info with valid default branch + valid_repo_infos = [ + repo_info.copy() | {"default_branch": "main"}, + ] + + run_check_for_each_repo_info( + True, + RepositoryDefaultBranchCheck, + valid_repo_infos, + beman_standard_check_config, + ) + + +def test__REPOSITORY_DEFAULT_BRANCH__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid default branch fail the check. + """ + # Test various invalid branch names + invalid_repo_infos = [ + repo_info.copy() | {"default_branch": "master"}, + repo_info.copy() | {"default_branch": "develop"}, + repo_info.copy() | {"default_branch": "dev"}, + repo_info.copy() | {"default_branch": "trunk"}, + repo_info.copy() | {"default_branch": "default"}, + repo_info.copy() | {"default_branch": "production"}, + repo_info.copy() | {"default_branch": "release"}, + repo_info.copy() | {"default_branch": "stable"}, + repo_info.copy() | {"default_branch": "testing"}, + repo_info.copy() | {"default_branch": "alpha"}, + repo_info.copy() | {"default_branch": "beta"}, + repo_info.copy() | {"default_branch": "experimental"}, + ] + + run_check_for_each_repo_info( + False, + RepositoryDefaultBranchCheck, + invalid_repo_infos, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__REPOSITORY_DEFAULT_BRANCH__fix_inplace( + repo_info, beman_standard_check_config +): + pass + + +def test__REPOSITORY_DISALLOW_GIT_SUBMODULES__valid( + repo_info, beman_standard_check_config +): + """ + Test that repositories with valid git submodules pass the check. + """ + valid_submodules_paths = [ + # Repo with no .gitsubmodules file + Path(f"{valid_prefix}/repo-exemplar-v1/"), + # Repo with wg21 git submodule + Path(f"{valid_prefix}/repo-exemplar-v2/"), + # Repo with wg21 submodule but different path + Path(f"{valid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + True, + valid_submodules_paths, + RepositoryDisallowGitSubmodulesCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__REPOSITORY_DISALLOW_GIT_SUBMODULES__invalid( + repo_info, beman_standard_check_config +): + """ + Test that repositories with invalid git submodules fail the check. + """ + invalid_submodules_paths = [ + # Repository with a single non-wg21 submodule + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + # Repository with multiple submodules including wg21 + Path(f"{invalid_prefix}/repo-exemplar-v2/"), + # Repository with multiple non-wg21 submodules + Path(f"{invalid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + False, + invalid_submodules_paths, + RepositoryDisallowGitSubmodulesCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__REPOSITORY_DISALLOW_GIT_SUBMODULES__inplace( + repo_info, beman_standard_check_config +): + pass diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/__init__.py b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/conftest.py b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/conftest.py new file mode 100644 index 00000000..515dc2bd --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/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/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/CMakeLists.txt b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/CMakeLists.txt new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/LICENSE b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v1/README.md new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/bla/LICENSE b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/bla/LICENSE new file mode 100644 index 00000000..e4fa6a84 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/bla/LICENSE @@ -0,0 +1 @@ +Dummy content. Test that LICENSE is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/cmake/CMakeLists.txt b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/cmake/CMakeLists.txt new file mode 100644 index 00000000..a4d12589 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/cmake/CMakeLists.txt @@ -0,0 +1 @@ +# Dummy content. Test that CMakeLists.txt is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/license/LICENSE b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/license/LICENSE new file mode 100644 index 00000000..e4fa6a84 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/license/LICENSE @@ -0,0 +1 @@ +Dummy content. Test that LICENSE is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/readme/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/readme/README.md new file mode 100644 index 00000000..19e661f0 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v2/readme/README.md @@ -0,0 +1 @@ +Dummy content. Test that README.md is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v3/.keep b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/invalid/repo-exemplar-v3/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/CMakeLists.txt b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/CMakeLists.txt new file mode 100644 index 00000000..a4d12589 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/CMakeLists.txt @@ -0,0 +1 @@ +# Dummy content. Test that CMakeLists.txt is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/LICENSE b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/LICENSE new file mode 100644 index 00000000..e4fa6a84 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/LICENSE @@ -0,0 +1 @@ +Dummy content. Test that LICENSE is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/README.md b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/README.md new file mode 100644 index 00000000..19e661f0 --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/data/valid/repo-exemplar-v1/README.md @@ -0,0 +1 @@ +Dummy content. Test that README.md is non-empty. diff --git a/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/test_toplevel.py b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/test_toplevel.py new file mode 100644 index 00000000..9fdeac5e --- /dev/null +++ b/tools/beman-tidy/tests/lib/checks/beman_standard/toplevel/test_toplevel.py @@ -0,0 +1,155 @@ +#!/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, +) + +# Actual tested checks. +from beman_tidy.lib.checks.beman_standard.toplevel import ( + ToplevelCmakeCheck, + ToplevelLicenseCheck, + ToplevelReadmeCheck, +) + +test_data_prefix = "tests/lib/checks/beman_standard/toplevel/data" +valid_prefix = f"{test_data_prefix}/valid" +invalid_prefix = f"{test_data_prefix}/invalid" + + +def test__TOPLEVEL_CMAKE__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid CMakeLists.txt pass the check. + """ + valid_cmake_paths = [ + # exemplar/ repo with valid CMakeLists.txt file. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_cmake_paths, + ToplevelCmakeCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__TOPLEVEL_CMAKE__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid CMakeLists.txt fail the check. + """ + invalid_cmake_paths = [ + # exemplar/ repo with empty CMakeLists.txt file. + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with CMakeLists.txt in non-root location. + Path(f"{invalid_prefix}/repo-exemplar-v2/"), + # exemplar/ repo without CMakeLists.txt file. + Path(f"{invalid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + False, + invalid_cmake_paths, + ToplevelCmakeCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__TOPLEVEL_CMAKE__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__TOPLEVEL_LICENSE__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid LICENSE pass the check. + """ + valid_license_paths = [ + # exemplar/ repo with valid LICENSE file. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_license_paths, + ToplevelLicenseCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__TOPLEVEL_LICENSE__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid LICENSE fail the check. + """ + invalid_license_paths = [ + # exemplar/ repo with empty LICENSE file. + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with LICENSE in non-root location. + Path(f"{invalid_prefix}/repo-exemplar-v2/"), + # exemplar/ repo without LICENSE file. + Path(f"{invalid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + False, + invalid_license_paths, + ToplevelLicenseCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__TOPLEVEL_LICENSE__fix_inplace(repo_info, beman_standard_check_config): + pass + + +def test__TOPLEVEL_README__valid(repo_info, beman_standard_check_config): + """ + Test that repositories with valid README.md pass the check. + """ + valid_readme_paths = [ + # exemplar/ repo with valid README.md file. + Path(f"{valid_prefix}/repo-exemplar-v1/"), + ] + + run_check_for_each_path( + True, + valid_readme_paths, + ToplevelReadmeCheck, + repo_info, + beman_standard_check_config, + ) + + +def test__TOPLEVEL_README__invalid(repo_info, beman_standard_check_config): + """ + Test that repositories with invalid README.md fail the check. + """ + invalid_readme_paths = [ + # exemplar/ repo with empty README.md file. + Path(f"{invalid_prefix}/repo-exemplar-v1/"), + # exemplar/ repo with README.md in non-root location. + Path(f"{invalid_prefix}/repo-exemplar-v2/"), + # exemplar/ repo without README.md file. + Path(f"{invalid_prefix}/repo-exemplar-v3/"), + ] + + run_check_for_each_path( + False, + invalid_readme_paths, + ToplevelReadmeCheck, + repo_info, + beman_standard_check_config, + ) + + +@pytest.mark.skip(reason="NOT implemented") +def test__TOPLEVEL_README__fix_inplace(repo_info, beman_standard_check_config): + pass diff --git a/tools/beman-tidy/tests/utils/__init__.py b/tools/beman-tidy/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/beman-tidy/tests/utils/conftest.py b/tools/beman-tidy/tests/utils/conftest.py new file mode 100644 index 00000000..99b6ed40 --- /dev/null +++ b/tools/beman-tidy/tests/utils/conftest.py @@ -0,0 +1,27 @@ +#!/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", + "default_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/tools/beman-tidy/tests/utils/path_runners.py b/tools/beman-tidy/tests/utils/path_runners.py new file mode 100644 index 00000000..64535d60 --- /dev/null +++ b/tools/beman-tidy/tests/utils/path_runners.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import os +from pathlib import Path + + +def run_check_for_each_repo_info( + expected_result, check_class, repo_infos, beman_standard_check_config +): + """ + Run repo-info-based check (check_class) for each given repo_info: evaluate check_class(repo_infos[i]). + + This is useful for checks that work with repository metadata rather than file paths. + + Example: + expected_result = True / False + repo_infos = [ + {"default_branch": "main", ...}, + {"default_branch": "master", ...}, + ] + check_class = RepositoryDefaultBranchCheck + beman_standard_check_config = "/path/to/.beman-standard.yml" + """ + for repo_info in repo_infos: + check_instance = check_class(repo_info, beman_standard_check_config) + check_instance.log_level = True + + # Run the main check + assert check_instance.check() is expected_result, ( + f"[{check_instance.__class__.__name__}] check() failed for repo_info: {repo_info}" + ) + + +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: + if os.path.isdir(path): + # For repo checks, modify the repo_info to point to the test directory + repo_info["top_level"] = Path(path) + + check_instance = check_class(repo_info, beman_standard_check_config) + + if os.path.isfile(path): + # For file checks, set the path directly on the check instance + check_instance.path = Path(path) + + check_instance.log_level = True + + if os.path.isfile(path): + # For file-based checks, pre_check is expected to pass + assert check_instance.pre_check() is True, ( + f"[{check_instance.__class__.__name__}] pre_check() failed for {path}" + ) + + # Run the main check + 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/tools/beman-tidy/uv.lock b/tools/beman-tidy/uv.lock new file mode 100644 index 00000000..da07ff4a --- /dev/null +++ b/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" }, +]