diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 4df0677..952c9a2 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -1,5 +1,5 @@ -name: verify_cpp_template -run-name: Verify C++ project template +name: build_cpplinux +run-name: Build and test C++ library on: workflow_dispatch: @@ -28,9 +28,7 @@ on: - CMakeLists.txt - build_lib.sh - generate_version.sh - - tailor_template_cleanup.sh - .github/workflows/build_linux.yml - - .github/workflows/*.yml.tpl pull_request: branches: - "master" @@ -46,9 +44,7 @@ on: - CMakeLists.txt - build_lib.sh - generate_version.sh - - tailor_template_cleanup.sh - .github/workflows/build_linux.yml - - .github/workflows/*.yml.tpl jobs: build: @@ -68,7 +64,7 @@ jobs: if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.runner == 'github-hosted' }} run: | sudo apt update - sudo apt install -y cmake ninja-build g++ ccache libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest python3-yaml doxygen graphviz + sudo apt install -y cmake ninja-build g++ ccache libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest - name: Validate self-hosted prerequisites if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' }} @@ -79,22 +75,18 @@ jobs: command -v ccache command -v python3 python3 -m pytest --version - python3 -c 'import yaml' - command -v doxygen - command -v dot - name: Restore compiler cache uses: actions/cache@v4 with: path: ~/.ccache - key: ccache-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**', 'tailor_template_cleanup.sh', '.github/workflows/*.yml.tpl') }} + key: ccache-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**') }} restore-keys: | ccache-${{ runner.os }}-${{ github.ref_name }}- ccache-${{ runner.os }}- - name: Configure run: | - # Build artifacts are tested in a separate job, so keep CPU flags portable. cmake -S . -B "${BUILD_DIR}" -GNinja \ -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ -DENABLE_TESTS=ON \ @@ -136,7 +128,7 @@ jobs: if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.runner == 'github-hosted' }} run: | sudo apt update - sudo apt install -y cmake ninja-build g++ libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest python3-yaml doxygen graphviz + sudo apt install -y cmake ninja-build g++ libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest - name: Validate self-hosted prerequisites if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' }} @@ -145,9 +137,6 @@ jobs: command -v ctest command -v python3 python3 -m pytest --version - python3 -c 'import yaml' - command -v doxygen - command -v dot - name: Download build artifacts uses: actions/download-artifact@v4 @@ -169,41 +158,7 @@ jobs: - name: Restore test executable permissions run: | - # GitHub artifact download may drop executable bits from binaries. find "${CTEST_DIR}" -type f -path "*/tests/*" -exec chmod +x {} + || true - name: Test run: ctest --test-dir "${CTEST_DIR}" --output-on-failure --parallel 2 --no-tests=error - - tailored-project-validation: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install tailored-project dependencies - run: | - sudo apt update - sudo apt install -y cmake ninja-build g++ libboost-all-dev libeigen3-dev python3-dev python3-pytest python3-yaml doxygen graphviz - - - name: Materialize and exercise project workflows - shell: bash - run: | - set -Eeuo pipefail - scratch_dir="$(mktemp -d "${RUNNER_TEMP}/tailored-project-validation.XXXXXX")" - target_dir="${scratch_dir}/target" - git clone --no-local "${GITHUB_WORKSPACE}" "${target_dir}" - git -C "${target_dir}" checkout --detach "${GITHUB_SHA}" - - ( - cd "${target_dir}" - ./tailor_template_cleanup.sh --apply --yes --project-namespace tailored_project - test -z "$(find .github/workflows -maxdepth 1 -name '*.tpl' -print -quit)" - python3 -c 'import pathlib, yaml; [yaml.safe_load(path.read_text()) for path in pathlib.Path(".github/workflows").glob("*.yml")]' - ./build_lib.sh -B build_tailored_ci - cmake --preset docs - cmake --build --preset docs - ) diff --git a/.github/workflows/build_linux.yml.tpl b/.github/workflows/build_linux.yml.tpl deleted file mode 100644 index 3e535b0..0000000 --- a/.github/workflows/build_linux.yml.tpl +++ /dev/null @@ -1,165 +0,0 @@ -# project-ci-template: generic -name: build_cpplinux -run-name: Build and test C++ library - -on: - workflow_dispatch: - inputs: - runner: - description: "Runner type to use" - required: true - default: github-hosted - type: choice - options: - - github-hosted - - self-hosted - push: - branches: - - "master" - - "main" - - "develop" - tags: - - "v*.*.*" - paths: - - src/** - - examples/** - - tests/** - - lib/** - - cmake/** - - CMakeLists.txt - - build_lib.sh - - generate_version.sh - - .github/workflows/build_linux.yml - pull_request: - branches: - - "master" - - "main" - - "develop" - - "dev*" - paths: - - src/** - - examples/** - - tests/** - - lib/** - - cmake/** - - CMakeLists.txt - - build_lib.sh - - generate_version.sh - - .github/workflows/build_linux.yml - -jobs: - build: - runs-on: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' && fromJSON('["self-hosted","Linux","X64"]') || 'ubuntu-latest' }} - env: - BUILD_DIR: build_ci - BUILD_TYPE: RelWithDebInfo - ENABLE_TBB: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' && 'OFF' || 'ON' }} - CTEST_OUTPUT_ON_FAILURE: 1 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install dependencies - if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.runner == 'github-hosted' }} - run: | - sudo apt update - sudo apt install -y cmake ninja-build g++ ccache libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest - - - name: Validate self-hosted prerequisites - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' }} - run: | - command -v cmake - command -v ninja - command -v g++ - command -v ccache - command -v python3 - python3 -m pytest --version - - - name: Restore compiler cache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: ccache-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**') }} - restore-keys: | - ccache-${{ runner.os }}-${{ github.ref_name }}- - ccache-${{ runner.os }}- - - - name: Configure - run: | - cmake -S . -B "${BUILD_DIR}" -GNinja \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -DENABLE_TESTS=ON \ - -DENABLE_TBB="${ENABLE_TBB}" \ - -DENABLE_CUDA=OFF \ - -DENABLE_OPTIX=OFF \ - -DENABLE_OPENGL=OFF \ - -DCPU_ENABLE_NATIVE_TUNING=OFF \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - - name: Build - run: cmake --build "${BUILD_DIR}" --parallel 4 - - - name: Show ccache stats - run: ccache --show-stats || true - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: cmake-build-tree - path: ${{ env.BUILD_DIR }} - if-no-files-found: error - retention-days: 3 - - test: - needs: build - runs-on: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' && fromJSON('["self-hosted","Linux","X64"]') || 'ubuntu-latest' }} - env: - BUILD_DIR: build_ci - CTEST_OUTPUT_ON_FAILURE: 1 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install test dependencies - if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.runner == 'github-hosted' }} - run: | - sudo apt update - sudo apt install -y cmake ninja-build g++ libboost-all-dev libeigen3-dev libtbb-dev python3-dev python3-pytest - - - name: Validate self-hosted prerequisites - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'self-hosted' }} - run: | - command -v cmake - command -v ctest - command -v python3 - python3 -m pytest --version - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: cmake-build-tree - path: ${{ env.BUILD_DIR }} - - - name: Resolve downloaded build tree - run: | - if [ -d "${BUILD_DIR}/CMakeFiles" ]; then - echo "CTEST_DIR=${BUILD_DIR}" >> "$GITHUB_ENV" - elif [ -d "${BUILD_DIR}/${BUILD_DIR}/CMakeFiles" ]; then - echo "CTEST_DIR=${BUILD_DIR}/${BUILD_DIR}" >> "$GITHUB_ENV" - else - echo "Could not find CMakeFiles under '${BUILD_DIR}' after artifact download." - find "${BUILD_DIR}" -maxdepth 3 -type d | sort || true - exit 1 - fi - - - name: Restore test executable permissions - run: | - find "${CTEST_DIR}" -type f -path "*/tests/*" -exec chmod +x {} + || true - - - name: Test - run: ctest --test-dir "${CTEST_DIR}" --output-on-failure --parallel 2 --no-tests=error diff --git a/.github/workflows/build_linux_cuda.yml b/.github/workflows/build_linux_cuda.yml index 2624a1d..0825dde 100644 --- a/.github/workflows/build_linux_cuda.yml +++ b/.github/workflows/build_linux_cuda.yml @@ -1,5 +1,5 @@ -name: verify_cpp_cuda_template -run-name: Verify C++ project template with CUDA +name: build_cpplinux_cuda +run-name: Build and test C++ library (CUDA) on: workflow_dispatch: @@ -60,40 +60,22 @@ jobs: command -v nvidia-smi command -v python3 python3 -m pytest --version - python3 -c 'import yaml' - command -v doxygen - command -v dot nvidia-smi nvcc --version - - name: Verify dormant workflow templates - id: workflow_contracts - run: python3 -m pytest -q tests/template_test/testWorkflowTemplates.py - - name: Restore compiler cache uses: actions/cache@v4 with: path: ~/.ccache - key: ccache-cuda-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**', '.github/workflows/build_linux_cuda.yml', '.github/workflows/build_linux_cuda.yml.tpl') }} + key: ccache-cuda-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**', '.github/workflows/build_linux_cuda.yml') }} restore-keys: | ccache-cuda-${{ runner.os }}-${{ github.ref_name }}- ccache-cuda-${{ runner.os }}- - - name: Materialize tailored CUDA project - id: materialize_tailored_project - shell: bash - run: | - set -Eeuo pipefail - ./tailor_template_cleanup.sh --apply --yes --project-namespace tailored_project - test -z "$(find .github/workflows -maxdepth 1 -name '*.tpl' -print -quit)" - python3 -c 'import pathlib, yaml; [yaml.safe_load(path.read_text()) for path in pathlib.Path(".github/workflows").glob("*.yml")]' - - name: Configure run: | - # Build artifacts are tested in a separate job, so keep CPU flags portable. cmake -S . -B "${BUILD_DIR}" -GNinja \ -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DENABLE_TESTS=ON \ -DENABLE_CUDA=ON \ -DENABLE_OPTIX="${ENABLE_OPTIX}" \ @@ -108,21 +90,6 @@ jobs: - name: Build run: cmake --build "${BUILD_DIR}" --parallel 4 - - name: Verify project CUDA source graph - shell: bash - run: | - set -Eeuo pipefail - compile_commands="${BUILD_DIR}/compile_commands.json" - test -f "${compile_commands}" - if ! grep -Fq "src/template_src_kernels/placeholder.cu" "${compile_commands}"; then - echo "Project CUDA source is absent from the compiled target graph." >&2 - exit 1 - fi - if grep -Fq "placeholder_to_ptx.ptx.cu" "${compile_commands}"; then - echo "OptiX PTX input entered ordinary CUDA compilation." >&2 - exit 1 - fi - - name: Show ccache stats run: ccache --show-stats || true @@ -158,16 +125,6 @@ jobs: command -v ctest command -v python3 python3 -m pytest --version - python3 -c 'import yaml' - command -v doxygen - command -v dot - - - name: Match tailored project source tree - shell: bash - run: | - set -Eeuo pipefail - ./tailor_template_cleanup.sh --apply --yes --project-namespace tailored_project - test -z "$(find .github/workflows -maxdepth 1 -name '*.tpl' -print -quit)" - name: Download build artifacts uses: actions/download-artifact@v4 @@ -189,7 +146,6 @@ jobs: - name: Restore test executable permissions run: | - # GitHub artifact download may drop executable bits from binaries. find "${CTEST_DIR}" -type f -path "*/tests/*" -exec chmod +x {} + || true - name: Test diff --git a/.github/workflows/build_linux_cuda.yml.tpl b/.github/workflows/build_linux_cuda.yml.tpl deleted file mode 100644 index 3d230da..0000000 --- a/.github/workflows/build_linux_cuda.yml.tpl +++ /dev/null @@ -1,153 +0,0 @@ -# project-ci-template: generic -name: build_cpplinux_cuda -run-name: Build and test C++ library (CUDA) - -on: - workflow_dispatch: - inputs: - enable_optix: - description: "Enable OptiX (requires SDK on runner)" - required: true - default: "false" - type: choice - options: - - "false" - - "true" - enable_tbb: - description: "Enable oneTBB (requires TBB on runner)" - required: true - default: "false" - type: choice - options: - - "false" - - "true" - push: - tags: - - "v*.*.*" - -concurrency: - group: cuda-ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - if: ${{ vars.CI_USE_SELF_HOSTED == 'true' }} - runs-on: - - self-hosted - - Linux - - X64 - - gpu - - cuda - timeout-minutes: 90 - env: - BUILD_DIR: build_cuda_ci - BUILD_TYPE: RelWithDebInfo - CTEST_OUTPUT_ON_FAILURE: 1 - ENABLE_OPTIX: ${{ github.event.inputs.enable_optix == 'true' && 'ON' || 'OFF' }} - ENABLE_TBB: ${{ github.event.inputs.enable_tbb == 'true' && 'ON' || 'OFF' }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Validate CUDA prerequisites - run: | - command -v cmake - command -v ninja - command -v g++ - command -v ccache - command -v nvcc - command -v nvidia-smi - command -v python3 - python3 -m pytest --version - nvidia-smi - nvcc --version - - - name: Restore compiler cache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: ccache-cuda-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'tests/**', '.github/workflows/build_linux_cuda.yml') }} - restore-keys: | - ccache-cuda-${{ runner.os }}-${{ github.ref_name }}- - ccache-cuda-${{ runner.os }}- - - - name: Configure - run: | - cmake -S . -B "${BUILD_DIR}" -GNinja \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -DENABLE_TESTS=ON \ - -DENABLE_CUDA=ON \ - -DENABLE_OPTIX="${ENABLE_OPTIX}" \ - -DENABLE_TBB="${ENABLE_TBB}" \ - -DENABLE_OPENGL=OFF \ - -DCPU_ENABLE_NATIVE_TUNING=OFF \ - -DCUDA_ENABLE_FMAD=ON \ - -DCUDA_ENABLE_EXTRA_DEVICE_VECTORIZATION=ON \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - - name: Build - run: cmake --build "${BUILD_DIR}" --parallel 4 - - - name: Show ccache stats - run: ccache --show-stats || true - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: cmake-build-tree-cuda - path: ${{ env.BUILD_DIR }} - if-no-files-found: error - retention-days: 3 - - test: - needs: build - if: ${{ vars.CI_USE_SELF_HOSTED == 'true' }} - runs-on: - - self-hosted - - Linux - - X64 - - gpu - - cuda - timeout-minutes: 45 - env: - BUILD_DIR: build_cuda_ci - CTEST_OUTPUT_ON_FAILURE: 1 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Validate test prerequisites - run: | - command -v ctest - command -v python3 - python3 -m pytest --version - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: cmake-build-tree-cuda - path: ${{ env.BUILD_DIR }} - - - name: Resolve downloaded build tree - run: | - if [ -d "${BUILD_DIR}/CMakeFiles" ]; then - echo "CTEST_DIR=${BUILD_DIR}" >> "$GITHUB_ENV" - elif [ -d "${BUILD_DIR}/${BUILD_DIR}/CMakeFiles" ]; then - echo "CTEST_DIR=${BUILD_DIR}/${BUILD_DIR}" >> "$GITHUB_ENV" - else - echo "Could not find CMakeFiles under '${BUILD_DIR}' after artifact download." - find "${BUILD_DIR}" -maxdepth 3 -type d | sort || true - exit 1 - fi - - - name: Restore test executable permissions - run: | - find "${CTEST_DIR}" -type f -path "*/tests/*" -exec chmod +x {} + || true - - - name: Test - run: ctest --test-dir "${CTEST_DIR}" --output-on-failure --parallel 2 --no-tests=error diff --git a/.github/workflows/build_ros2_overlay.yml b/.github/workflows/build_ros2_overlay.yml index b093a94..4ef57ee 100644 --- a/.github/workflows/build_ros2_overlay.yml +++ b/.github/workflows/build_ros2_overlay.yml @@ -1,5 +1,5 @@ -name: verify_template_ros2_overlay -run-name: Verify optional ROS 2 overlay template +name: build_ros2_overlay +run-name: Build optional ROS 2 overlay on: workflow_dispatch: @@ -14,28 +14,11 @@ on: - CMakeLists.txt - cmake/** - src/** + - lib/** - ros2/** - build_ros2.sh - - add_ros2_support.sh - generate_version.sh - - tailor_template_cleanup.sh - - doc/ros2_overlay.md - - doc/template_usage.md - - doc/bootstrap_prompts.md - - README.md - - AGENTS.md - - CLAUDE.md - - python/COLCON_IGNORE - - lib/COLCON_IGNORE - - examples/COLCON_IGNORE - - tests/COLCON_IGNORE - - tests/cmake/VerifyTemplateProjectRos2Overlay.cmake - - tests/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake - - tests/cmake/VerifyTemplateProjectCudaSources.cmake - - tests/template_test/testRos2OverlayStatic.py - - tests/template_test/testWorkflowTemplates.py - .github/workflows/build_ros2_overlay.yml - - .github/workflows/build_ros2_overlay.yml.tpl pull_request: branches: - "master" @@ -46,28 +29,11 @@ on: - CMakeLists.txt - cmake/** - src/** + - lib/** - ros2/** - build_ros2.sh - - add_ros2_support.sh - generate_version.sh - - tailor_template_cleanup.sh - - doc/ros2_overlay.md - - doc/template_usage.md - - doc/bootstrap_prompts.md - - README.md - - AGENTS.md - - CLAUDE.md - - python/COLCON_IGNORE - - lib/COLCON_IGNORE - - examples/COLCON_IGNORE - - tests/COLCON_IGNORE - - tests/cmake/VerifyTemplateProjectRos2Overlay.cmake - - tests/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake - - tests/cmake/VerifyTemplateProjectCudaSources.cmake - - tests/template_test/testRos2OverlayStatic.py - - tests/template_test/testWorkflowTemplates.py - .github/workflows/build_ros2_overlay.yml - - .github/workflows/build_ros2_overlay.yml.tpl jobs: overlay-build: @@ -92,18 +58,20 @@ jobs: id: install_dependencies run: | apt-get update - apt-get install -y --no-install-recommends build-essential cmake git libeigen3-dev python3-colcon-common-extensions python3-pytest python3-yaml ros-dev-tools + apt-get install -y --no-install-recommends build-essential cmake git libeigen3-dev python3-colcon-common-extensions ros-dev-tools - name: Synchronize ROS package metadata id: sync_metadata shell: bash run: | - test -x ./generate_version.sh || { - echo "::error::generate_version.sh is missing or not executable." - exit 1 - } - ./generate_version.sh --sync-ros2 - git diff --exit-code -- ros2/*/package.xml + if [[ -x ./generate_version.sh ]] \ + && grep -q -- "--sync-ros2" ./generate_version.sh \ + && grep -q -- "ROS2_PROJECT_METADATA_SYNC=1" ./generate_version.sh; then + ./generate_version.sh --sync-ros2 + git diff --exit-code -- ros2/*/package.xml + else + echo "::warning::Skipping ROS package metadata sync; generate_version.sh is missing or predates full project metadata sync." + fi - name: Resolve ROS 2 package dependencies id: resolve_dependencies @@ -115,138 +83,3 @@ jobs: id: build_overlay shell: bash run: ./build_ros2.sh --clean --no-version-sync - - - name: Verify installed core header layout - id: verify_install_layout - shell: bash - run: | - metadata_dir_="${RUNNER_TEMP}/ros2-install-metadata" - cmake -S . -B "${metadata_dir_}" -DPROJECT_METADATA_ONLY=ON - core_cmake_name_="$(python3 - "${metadata_dir_}/CMakeCache.txt" <<'PY' - from pathlib import Path - import sys - - values_ = {} - for line_ in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines(): - key_and_type_, separator_, value_ = line_.partition("=") - if not separator_: - continue - key_, type_separator_, _ = key_and_type_.partition(":") - if type_separator_: - values_[key_] = value_ - print(values_["CMAKE_PROJECT_NAME"]) - PY - )" - mapfile -t core_headers_ < <( - find ros2/install -type f \ - -path "*/include/${core_cmake_name_}/wrapped_impl/CWrapperPlaceholder.h" \ - -print - ) - if [[ "${#core_headers_[@]}" -ne 1 ]]; then - echo "Expected exactly one installed core header, found ${#core_headers_[@]}." >&2 - exit 1 - fi - core_relative_path_="${core_headers_[0]#ros2/install/}" - core_ros_package_="${core_relative_path_%%/*}" - test ! -e "ros2/install/${core_ros_package_}/wrapped_impl/CWrapperPlaceholder.h" || { - echo "Core header leaked to the package install-prefix root." >&2 - exit 1 - } - - - name: Run static ROS 2 overlay checks - id: static_contracts - shell: bash - run: | - python3 -m pytest -q \ - tests/template_test/testRos2OverlayStatic.py \ - tests/template_test/testWorkflowTemplates.py - expected_version="$(python3 - VERSION <<'PY' - from pathlib import Path - import sys - - fields_ = {} - for line_ in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines(): - key_, separator_, value_ = line_.partition(":") - if separator_: - fields_[key_.strip()] = value_.strip() - version_ = fields_["Project version core"] - components_ = version_.split(".") - if len(components_) != 3 or not all(component_.isdecimal() for component_ in components_): - raise SystemExit("VERSION lacks a strict X.Y.Z project version core") - print(version_) - PY - )" - cmake \ - -DTEST_TEMPLATE_SOURCE_DIR="${PWD}" \ - -DTEST_BINARY_ROOT="${RUNNER_TEMP}/ros2_overlay_static" \ - -DEXPECTED_VERSION="${expected_version}" \ - -P tests/cmake/VerifyTemplateProjectRos2Overlay.cmake - - rollout-rehearsal: - runs-on: ubuntu-24.04 - container: - image: ros:jazzy - steps: - - name: Checkout repository - id: checkout_repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Trust checked-out Git worktree - id: trust_worktree - shell: bash - run: | - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - git -C "${GITHUB_WORKSPACE}" rev-parse --is-inside-work-tree - - - name: Install ROS 2 overlay dependencies - id: install_dependencies - run: | - apt-get update - apt-get install -y --no-install-recommends build-essential cmake git libeigen3-dev python3-colcon-common-extensions python3-pytest python3-yaml ros-dev-tools - - - name: Synchronize ROS package metadata - id: sync_metadata - shell: bash - run: | - test -x ./generate_version.sh || { - echo "::error::generate_version.sh is missing or not executable." - exit 1 - } - ./generate_version.sh --sync-ros2 - git diff --exit-code -- ros2/*/package.xml - - - name: Resolve ROS 2 package dependencies - id: resolve_dependencies - run: | - rosdep update - rosdep install --from-paths ros2 -i -r -y --rosdistro jazzy - - - name: Rehearse default-tailored overlay - id: tailored_overlay - shell: bash - run: | - set -Eeuo pipefail - scratch_dir="$(mktemp -d)" - target_dir="${scratch_dir}/target" - git clone --no-local "${GITHUB_WORKSPACE}" "${target_dir}" - git -C "${target_dir}" checkout --detach "${GITHUB_SHA}" - (cd "${target_dir}" && ./tailor_template_cleanup.sh --apply --yes --project-namespace tailored_project) - test -f "${target_dir}/.github/workflows/build_ros2_overlay.yml" - test ! -e "${target_dir}/.github/workflows/build_ros2_overlay.yml.tpl" - (cd "${target_dir}" && ./build_ros2.sh --clean --no-version-sync) - - - name: Rehearse additive rollout - id: additive_rollout - shell: bash - run: | - set -Eeuo pipefail - scratch_dir="$(mktemp -d)" - target_dir="${scratch_dir}/target" - git clone --no-local "${GITHUB_WORKSPACE}" "${target_dir}" - git -C "${target_dir}" checkout --detach "${GITHUB_SHA}" - (cd "${target_dir}" && ./tailor_template_cleanup.sh --apply --yes --project-namespace tailored_project --remove-ros2) - ./add_ros2_support.sh --root "${target_dir}" --apply --yes --verify - (cd "${target_dir}" && ./build_ros2.sh --clean --no-version-sync) - (cd "${target_dir}" && cmake -S . -B build_plain -DENABLE_TESTS=OFF && cmake --build build_plain -j2) diff --git a/.github/workflows/build_ros2_overlay.yml.tpl b/.github/workflows/build_ros2_overlay.yml.tpl deleted file mode 100644 index ae74f41..0000000 --- a/.github/workflows/build_ros2_overlay.yml.tpl +++ /dev/null @@ -1,86 +0,0 @@ -# project-ci-template: generic -name: build_ros2_overlay -run-name: Build optional ROS 2 overlay - -on: - workflow_dispatch: - push: - branches: - - "master" - - "main" - - "develop" - tags: - - "v*.*.*" - paths: - - CMakeLists.txt - - cmake/** - - src/** - - lib/** - - ros2/** - - build_ros2.sh - - generate_version.sh - - .github/workflows/build_ros2_overlay.yml - pull_request: - branches: - - "master" - - "main" - - "develop" - - "dev*" - paths: - - CMakeLists.txt - - cmake/** - - src/** - - lib/** - - ros2/** - - build_ros2.sh - - generate_version.sh - - .github/workflows/build_ros2_overlay.yml - -jobs: - overlay-build: - runs-on: ubuntu-24.04 - container: - image: ros:jazzy - steps: - - name: Checkout repository - id: checkout_repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Trust checked-out Git worktree - id: trust_worktree - shell: bash - run: | - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - git -C "${GITHUB_WORKSPACE}" rev-parse --is-inside-work-tree - - - name: Install ROS 2 overlay dependencies - id: install_dependencies - run: | - apt-get update - apt-get install -y --no-install-recommends build-essential cmake git libeigen3-dev python3-colcon-common-extensions ros-dev-tools - - - name: Synchronize ROS package metadata - id: sync_metadata - shell: bash - run: | - if [[ -x ./generate_version.sh ]] \ - && grep -q -- "--sync-ros2" ./generate_version.sh \ - && grep -q -- "ROS2_PROJECT_METADATA_SYNC=1" ./generate_version.sh; then - ./generate_version.sh --sync-ros2 - git diff --exit-code -- ros2/*/package.xml - else - echo "::warning::Skipping ROS package metadata sync; generate_version.sh is missing or predates full project metadata sync." - fi - - - name: Resolve ROS 2 package dependencies - id: resolve_dependencies - run: | - rosdep update - rosdep install --from-paths ros2 -i -r -y --rosdistro jazzy - - - name: Build and test ROS 2 overlay - id: build_overlay - shell: bash - run: ./build_ros2.sh --clean --no-version-sync diff --git a/.github/workflows/docs_pages.yml b/.github/workflows/docs_pages.yml index c760ce8..1c4cee3 100644 --- a/.github/workflows/docs_pages.yml +++ b/.github/workflows/docs_pages.yml @@ -1,5 +1,5 @@ -name: verify_template_docs -run-name: Build and publish template documentation +name: docs_pages +run-name: Build and publish Doxygen documentation on: workflow_dispatch: @@ -22,8 +22,6 @@ on: - CMakeLists.txt - CMakePresets.json - .github/workflows/docs_pages.yml - - .github/workflows/docs_pages.yml.tpl - - tests/template_test/testWorkflowTemplates.py pull_request: branches: - "master" @@ -38,8 +36,6 @@ on: - CMakeLists.txt - CMakePresets.json - .github/workflows/docs_pages.yml - - .github/workflows/docs_pages.yml.tpl - - tests/template_test/testWorkflowTemplates.py permissions: contents: read @@ -62,11 +58,7 @@ jobs: - name: Install documentation dependencies run: | sudo apt update - sudo apt install -y cmake ninja-build g++ doxygen graphviz libeigen3-dev python3-pytest python3-yaml - - - name: Run template workflow contracts - id: workflow_contracts - run: python3 -m pytest -q tests/template_test/testWorkflowTemplates.py + sudo apt install -y cmake ninja-build g++ doxygen graphviz libeigen3-dev - name: Configure documentation build run: | @@ -79,9 +71,7 @@ jobs: -DBUILD_DOC_HTML=ON \ -DBUILD_DOC_XML=ON \ -DBUILD_DOC_LATEX=OFF \ - -DDOC_WARN_AS_ERROR=OFF \ - -Dtemplate_project_BUILD_PROGRAMS=OFF \ - -Dtemplate_project_BUILD_EXAMPLES=OFF + -DDOC_WARN_AS_ERROR=OFF - name: Build documentation run: cmake --build "${BUILD_DIR}" --target doc --parallel 2 @@ -119,6 +109,4 @@ jobs: curl --fail --location --retry 5 --retry-delay 5 \ "${{ steps.deployment.outputs.page_url }}" \ --output /tmp/docs-pages-index.html - grep -F "Template usage" /tmp/docs-pages-index.html - grep -F "Documentation workflow" /tmp/docs-pages-index.html - grep -F "Versioning" /tmp/docs-pages-index.html + test -s /tmp/docs-pages-index.html diff --git a/.github/workflows/docs_pages.yml.tpl b/.github/workflows/docs_pages.yml.tpl deleted file mode 100644 index 64b6bb0..0000000 --- a/.github/workflows/docs_pages.yml.tpl +++ /dev/null @@ -1,113 +0,0 @@ -# project-ci-template: generic -name: docs_pages -run-name: Build and publish Doxygen documentation - -on: - workflow_dispatch: - inputs: - deploy_pages: - description: "Deploy to GitHub Pages after building docs" - required: true - default: false - type: boolean - push: - branches: - - "master" - - "main" - - "develop" - paths: - - README.md - - src/** - - doc/** - - cmake/** - - CMakeLists.txt - - CMakePresets.json - - .github/workflows/docs_pages.yml - pull_request: - branches: - - "master" - - "main" - - "develop" - - "dev*" - paths: - - README.md - - src/** - - doc/** - - cmake/** - - CMakeLists.txt - - CMakePresets.json - - .github/workflows/docs_pages.yml - -permissions: - contents: read - -concurrency: - group: pages-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-docs: - runs-on: ubuntu-latest - env: - BUILD_DIR: build_docs - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Install documentation dependencies - run: | - sudo apt update - sudo apt install -y cmake ninja-build g++ doxygen graphviz libeigen3-dev - - - name: Configure documentation build - run: | - cmake -S . -B "${BUILD_DIR}" -GNinja \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DENABLE_TESTS=OFF \ - -DENABLE_CUDA=OFF \ - -DENABLE_OPTIX=OFF \ - -DENABLE_OPENGL=OFF \ - -DBUILD_DOC_HTML=ON \ - -DBUILD_DOC_XML=ON \ - -DBUILD_DOC_LATEX=OFF \ - -DDOC_WARN_AS_ERROR=OFF - - - name: Build documentation - run: cmake --build "${BUILD_DIR}" --target doc --parallel 2 - - - name: Verify generated site - run: | - test -f "${BUILD_DIR}/doc/html/index.html" - test -d "${BUILD_DIR}/doc/xml" - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 - with: - path: ${{ env.BUILD_DIR }}/doc/html - - deploy: - if: ${{ (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) || (github.event_name == 'workflow_dispatch' && inputs.deploy_pages) }} - needs: build-docs - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Configure Pages - uses: actions/configure-pages@v6 - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - - - name: Verify published Pages output - run: | - curl --fail --location --retry 5 --retry-delay 5 \ - "${{ steps.deployment.outputs.page_url }}" \ - --output /tmp/docs-pages-index.html - test -s /tmp/docs-pages-index.html diff --git a/AGENTS.md b/AGENTS.md index 55febbb..c80f8f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,10 +9,87 @@ See `doc/ros2_overlay.md` before changing the optional ROS 2 overlay. `./build_l Keep ROS-related changes confined to `ros2/` plus the documented root helpers, docs, tests, markers, and the single ROS overlay workflow. -For python: Use python standard >= 3.12, matplotlib is the backend for most plots, but for images PIL and opencv are also used. For any statistics-like plot prefer seaborn, my default choice. Use pytorch for machine learning applications, supported by sklearn. Function names beings with Capital letter, snake case, methods not. Classes Similarly. Internal methods (not public API) must start with _, local scope variables end with _. All methods of classes shall start with small letter. Prefer dataclasses instead of dicts and enums instead of Literals if more than two entries. Type Hints Must Always Be Present. Onnx Export Compatibility Is Generally Required. When Writing New Classes Or Functions, A Runnable Example Should Always Be Present With Output To Show Results. -For C++/CUDA: C++17 and C++20 are the core standards. CUDA mainly >12.6. Answers should be on point without too many digressions, technical (for intermediate and advanced users) but simple enough to explain the concepts. Prefer using concepts over SFINAE. Unit tests using Catch2. Check files to see convention of names. Prefer Classes over structs. -### CMake and derived-project test policy +## Language and programming standards + +### Language-agnostic software engineering guidelines + +- Follow the owning component's established conventions and keep each change + within the smallest coherent scope that satisfies the requested behavior. +- Prefer small, cohesive functions and classes with explicit contracts. Add an + abstraction only when it clarifies ownership, reuse, or a stable interface. +- Use descriptive names and keep one authoritative source for each policy or + piece of state. Avoid hidden coupling and duplicated decision logic. +- Validate external inputs at system boundaries and report actionable failures. + Do not silently fall back to behavior that changes the advertised contract. +- Test observable behavior, invariants, and failure modes rather than internal + implementation details or tunable defaults. +- During review and optimization, actively seek behavior-preserving ways to + reduce complexity and improve performance, maintainability, readability, and + implementation clarity. Simplify unnecessary nested loops, helper functions, + conditional branches, indirection, and abstractions that do not enforce a + useful contract. +- Keep refactoring within the reviewed scope and preserve public behavior unless + a contract change is explicitly requested. Make performance optimization + evidence-driven through profiling, measurement, or algorithmic analysis, and + document any tradeoff that increases complexity. +- Use 100 columns as a soft limit. Keep assignments and function calls on one + line when they remain readable; otherwise wrap at semantic boundaries and + align continuation lines with the expression they continue. +- Treat newlines as logical separators. Keep statements that implement the same + small step together, and use a blank line between distinct steps. +- Introduce each non-obvious logical block with a concise comment describing its + purpose, rationale, or invariant. Do not translate individual statements into + prose. + +### Python + +- Use Python 3.12 or newer and follow PEP 8 for naming and formatting. Use + `snake_case` for functions, methods, and variables, `PascalCase` for classes, + and a leading underscore for internal APIs. Use a trailing underscore only to + avoid a keyword or name collision. +- Follow PEP 257 and use Google-style docstrings for modules, public classes, + public methods, and public functions. Document arguments, returns, raised + exceptions, important invariants, and examples where applicable. +- Add precise type annotations to every function and method signature, class + attribute, and dataclass field. Keep code suitable for static checking, avoid + untyped definitions, and isolate or justify any unavoidable `Any` boundary. +- Prefer dataclasses to unstructured dictionaries for stable records. Prefer an + enum to string or integer literals when a choice has more than two values. +- Prefer functions for stateless transformations and classes when state, + ownership, or a durable behavioral interface is required. +- Use Matplotlib for general plots and prefer seaborn for statistical plots. + Use Pillow or OpenCV for image-specific work as appropriate. +- Use PyTorch for machine-learning implementations, with scikit-learn for + supporting workflows where useful. Preserve ONNX export compatibility for model APIs + unless the task explicitly excludes it. +- When building libraries and complex functionalities, provide examples/demos with expected output to show usage, with relevant visualization/output data to verify it. + +### C++ and CUDA + +- Use the repository-configured C++20 standard by default and retain C++17 + compatibility only where the owning target explicitly requires it. Target + CUDA 12.6 or newer unless a supported platform imposes another version. +- Use Doxygen file headers and Doxygen documentation for public classes, + functions, and methods. Cover parameters, return values, template parameters, + exceptions, ownership, and invariants where applicable. +- Prefer concepts over SFINAE. Prefer classes when invariants, ownership, or + behavior must be enforced; use simple aggregate types only when aggregate + semantics are the intended contract. +- Use Catch2 for C++ and CUDA unit tests and follow the naming conventions in + the surrounding component. +- Keep an assignment and the beginning of its right-hand expression on the same + line when the complete statement is readable within the soft limit. Apply the + same rule to function names and their first arguments. +- For long expressions or argument lists, wrap at semantic operators or argument + groups and align continuation lines. Do not mechanically place every term or + argument on a separate line. +- Keep technical explanations concise and aimed at intermediate or advanced + readers while defining ideas, practices and syntax when they affect the decision. +- Justify design choices when proposing them including choice of language featreus to implement a certain functionality among the considered alternatives. +- Follow C++ standards best practices and guidelines and Jason Turner suggested best practices when designing implementation. + +## CMake and derived-project test policy Do not copy template-conformance CMake verifiers into a derived project merely because the donor template has them. In particular, do not register tests that @@ -34,11 +111,11 @@ For a derived project: - never import `VerifyTemplateProject*` or other donor self-validation tests as product tests. -The template repository may retain broader conformance tests because it owns -generic generation and tailoring behavior. That exception does not make those -tests part of the derived-project contract. +Generic template conformance is owned by the standalone harness in +`cpp_cuda_template_testfield`. The template repository itself keeps the same +runtime-oriented test layout inherited by derived projects. -### Build cleanup and wrapper packaging safety +## Build cleanup and wrapper packaging safety - `build_lib.sh --clean` may remove only a conventional in-repository build path. An existing target must contain a `CMakeCache.txt` whose @@ -75,7 +152,71 @@ For MATLAB: Use classes a lot also in MATLAB, with a python style, but do it onl %% Function code -## Staged-Code Review Quality Gate +## Commit and staged-review workflow + +### Commit-message style + +- Do not use Conventional Commits prefixes such as `feat:`, `fix:`, or + `docs:`. +- Write the subject in the imperative mood and sentence case, with no trailing + period. Aim for approximately 50-70 characters when the change can be + described clearly within that range. +- Optionally end the subject with a short parenthetical scope when it adds + useful context, for example `Constrain cleanup to owned build trees + (build_lib.sh)`. +- Use an optional leading tag only when its meaning applies: + - `[MAJOR]` for a significant capability, architectural change, or broad + contract or workflow change; + - `[BUGFIX]` for a correctness defect or regression; + - `[HOTFIX]` for an urgent, narrowly targeted correction; + - no tag for routine enhancements, tests, documentation, or maintenance. +- Use an optional body for changes that need rationale or a behavioral summary. + Format it as imperative `-` bullets, put one blank line between bullets, omit + terminal periods, and indent wrapped continuation lines beneath the bullet + text. +- Describe intent, important design decisions, and behavioral consequences in + the body instead of merely listing changed files. +- Never add `Co-Authored-By` or other AI-attribution trailers. + +### Authorization and batch sequence + +1. Never create or amend a commit unless the user explicitly instructs the + agent to commit. Requests to implement, finish, stage, or continue, including + the keyword `next`, do not grant commit permission. +2. Treat commit, tag, and push authorization independently. Permission to + commit does not imply permission to tag or push. +3. Inspect the worktree and current index before preparing a batch. Preserve and + report unrelated user-owned staged or unstaged work; never reset, overwrite, + or absorb it merely to simplify the batch. +4. Partition completed work into coherent functional batches. Include directly + dependent tests and necessary documentation with their implementation unless + a concrete review or ownership boundary requires separation. Do not create + micro-batches that are too small to review meaningfully. +5. A mixed batch is allowed when a few small changes do not justify independent + review units. Label it clearly as mixed, explain why the items belong + together, and never use it to hide a substantial independent feature or fix. +6. Before staging, review the complete candidate diff for correctness, scope, + formatting, comments, and documentation. Run proportionate tests and static + checks, and apply the staged-code quality gate below to every new or + substantially modified source file. +7. Stage only the reviewed batch with an explicit path or hunk allowlist. Then + inspect the complete index with `git diff --cached` and run + `git diff --cached --check`. Repeat relevant validation against the staged + result when index contents or generated inputs can affect the outcome. +8. Report the staged paths, functional purpose, verification evidence, caveats, + exclusions, and exact proposed commit subject and body. Stop for user review + without preparing or staging another batch. +9. Advance only after the user responds with the exact keyword `next`. Interpret + `next` as permission to prepare the following batch, never as permission to + commit the current batch. +10. Before advancing, confirm that the previous batch is no longer staged. If + the index is still populated, stop and ask the user to commit or clear it, + or to give a separate explicit instruction for the agent to commit. +11. When the user explicitly requests both actions, such as `commit and next`, + commit the approved batch with the reviewed message, verify that the index + is clear, and only then prepare the following batch. + +## Staged-code review quality gate Before handing staged changes to the user for commit review, inspect the complete Git index with `git diff --cached`. Apply this gate to files staged by either the @@ -106,11 +247,12 @@ readability cleanup performed during the pass. ### C++ and CUDA pattern -Use Doxygen for both the file header and public API documentation: +Use Doxygen for both the file header and public API documentation. Apply the +shared compact-line and logical-block rules consistently: - Preserve compact grouped formatting when related call arguments or arithmetic - terms remain readable on one continuation line. Wrap at semantic expression - boundaries; do not mechanically place every argument on a separate line. + terms remain readable together. Wrap at semantic expression boundaries; do + not mechanically place every argument on a separate line. - In a multiline function declaration, definition, or call, keep the first argument on the same line as the function name and align later arguments with it. Put the opening parenthesis at the end of a line only for a genuinely @@ -121,14 +263,10 @@ Use Doxygen for both the file header and public API documentation: - Prefer this compact grouped layout: ```cpp -const float gx = - 0.5F * (PixelOrZero(image, width, height, x + 1, y) - - PixelOrZero(image, width, height, x - 1, y)); - -SPhotometricPatch(int id, - const cv::Point2d ¢er, - int64_t t_us, - int patch_size); +const float gx = 0.5F * (PixelOrZero(image, width, height, x + 1, y) - + PixelOrZero(image, width, height, x - 1, y)); + +SPhotometricPatch(int id, const cv::Point2d ¢er, int64_t timestampUs, int patchSize); ``` Do not expand the same calls into one line per argument unless an individual @@ -144,20 +282,17 @@ SPhotometricPatch(int id, /// @param inputPath Path to the delimited observation file. /// @return Valid observations in input order. /// @throws std::runtime_error When the file cannot be parsed. -std::vector LoadValidObservations( - const std::filesystem::path& inputPath) +std::vector LoadValidObservations(const std::filesystem::path& inputPath) { // Parse the complete file first so malformed rows produce one consistent // diagnostic path. - const std::vector parsedObservations = - ParseObservations(inputPath); + const std::vector parsedObservations = ParseObservations(inputPath); // Retain only observations satisfying the domain validity contract while // preserving their original order. std::vector validObservations; validObservations.reserve(parsedObservations.size()); - std::ranges::copy_if(parsedObservations, - std::back_inserter(validObservations), + std::ranges::copy_if(parsedObservations, std::back_inserter(validObservations), IsObservationValid); return validObservations; @@ -166,8 +301,8 @@ std::vector LoadValidObservations( ### Python pattern -Use Google-style module, class, method, and function docstrings. Keep type hints -on every callable and follow the repository naming conventions: +Use Google-style module, class, method, and function docstrings. Keep precise +type annotations on every callable and follow PEP 8 naming conventions: ```python """Load and validate observation records. @@ -176,19 +311,19 @@ This module owns file parsing and domain validation. Selection policy remains with the caller. Example: - observations_ = Load_valid_observations(Path("observations.csv")) - print(len(observations_)) + observations = load_valid_observations(Path("observations.csv")) + print(len(observations)) Output: 3 """ -def Load_valid_observations(input_path_: Path) -> list[Observation]: +def load_valid_observations(input_path: Path) -> list[Observation]: """Load valid observations while preserving their input order. Args: - input_path_: Path to the delimited observation file. + input_path: Path to the delimited observation file. Returns: Valid observations in input order. @@ -197,24 +332,24 @@ def Load_valid_observations(input_path_: Path) -> list[Observation]: ValueError: If an input row cannot be parsed. Example: - observations_ = Load_valid_observations(Path("observations.csv")) - print(len(observations_)) + observations = load_valid_observations(Path("observations.csv")) + print(len(observations)) Output: 3 """ # Parse all rows through one path so malformed input produces consistent # diagnostics. - parsed_observations_ = Parse_observations(input_path_) + parsed_observations = parse_observations(input_path) # Enforce the domain validity contract without changing source ordering. - valid_observations_ = [ - observation_ - for observation_ in parsed_observations_ - if observation_.isValid() + valid_observations = [ + observation + for observation in parsed_observations + if observation.is_valid() ] - return valid_observations_ + return valid_observations ``` ### MATLAB pattern diff --git a/CLAUDE.md b/CLAUDE.md index e120d0d..cb2e5f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,11 @@ All optional features default to OFF: `ENABLE_CUDA`, `ENABLE_OPTIX`, `ENABLE_OPE ### Testing -Uses Catch2 (auto-fetched if not found). Tests live in `tests/template_test/`, fixtures in `tests/template_fixtures/`. Test targets are created via the `add_tests()` macro from `cmake_utils.cmake`. +Uses Catch2 (auto-fetched if not found). Inherited runtime tests live in +`tests/template_test/`, CUDA runtime tests in `tests/template_cuda/`, and +fixtures in `tests/template_fixtures/`. Test targets are created via the +`add_tests()` macro from `cmake_utils.cmake`; generic template conformance is +owned externally by `cpp_cuda_template_testfield`. ### Consumer Pattern diff --git a/CMakeLists.txt b/CMakeLists.txt index 231d101..b713109 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ endif() set(METADATA_ONLY_OPTION_NAME "${project_name}_METADATA_ONLY") set(ENABLE_OPTIX_OPTION_NAME "${project_name}_ENABLE_OPTIX") set(ENABLE_CUDA_OPTION_NAME "${project_name}_ENABLE_CUDA") +set(ENABLE_TENSORRT_OPTION_NAME "${project_name}_ENABLE_TENSORRT") function(_template_project_migrate_top_level_bool_option_alias legacy_option canonical_option option_help) @@ -64,28 +65,34 @@ _template_project_migrate_top_level_bool_option_alias( ENABLE_OPTIX "${ENABLE_OPTIX_OPTION_NAME}" "Enable OptiX") _template_project_migrate_top_level_bool_option_alias( ENABLE_CUDA "${ENABLE_CUDA_OPTION_NAME}" "Enable CUDA") +_template_project_migrate_top_level_bool_option_alias( + ENABLE_TENSORRT "${ENABLE_TENSORRT_OPTION_NAME}" "Enable TensorRT") option(${METADATA_ONLY_OPTION_NAME} "Configure only project identity and version metadata" OFF) option(${ENABLE_OPTIX_OPTION_NAME} "Enable OptiX" OFF) option(${ENABLE_CUDA_OPTION_NAME} "Enable CUDA" OFF) +option(${ENABLE_TENSORRT_OPTION_NAME} "Enable TensorRT" OFF) # Retained modules consume the historical local variable names. Normal # directory-scope assignments isolate them from a parent's cache entries. set(PROJECT_METADATA_ONLY "${${METADATA_ONLY_OPTION_NAME}}") set(ENABLE_OPTIX "${${ENABLE_OPTIX_OPTION_NAME}}") set(ENABLE_CUDA "${${ENABLE_CUDA_OPTION_NAME}}") +set(ENABLE_TENSORRT "${${ENABLE_TENSORRT_OPTION_NAME}}") + +# OptiX and TensorRT both require the project CUDA feature when languages and +# dependency targets are configured. +if(ENABLE_OPTIX OR ENABLE_TENSORRT) + set(ENABLE_CUDA ON) +endif() if(PROJECT_METADATA_ONLY) set(languages NONE) else() set(languages CXX) # ACHTUNG: PTX code requires C language! - if(ENABLE_OPTIX) - set(ENABLE_CUDA ON) - endif() - if(ENABLE_CUDA) list(APPEND languages CUDA) if(ENABLE_OPTIX) @@ -141,6 +148,7 @@ if (DEFINED LIB_NAMESPACE_OVERRIDE) endif() set(CUDA_COMPILE_TARGET "${LIB_NAMESPACE}_cuda_compile_interface") set(OPTIX_COMPILE_TARGET "${LIB_NAMESPACE}_optix_compile_interface") +set(TENSORRT_COMPILE_TARGET "${LIB_NAMESPACE}_tensorrt_compile_interface") set(OPENGL_COMPILE_TARGET "${LIB_NAMESPACE}_opengl_compile_interface") set(LIB_COMPILE_TARGET "${LIB_NAMESPACE}_lib_compile_interface") set(SANITIZER_TARGET "${LIB_NAMESPACE}_sanitizer_target_interface") @@ -177,9 +185,10 @@ include(GNUInstallDirs) # Add cmake modules include(ExternalProject) -# Add CUDA/OptiX tools +# Add GPU dependency handlers include(cmake/HandleCUDA.cmake) include(cmake/HandleOptiX.cmake) +include(cmake/HandleTensorRT.cmake) include(cmake/HandleTBB.cmake) # Add handle of python @@ -354,11 +363,13 @@ if(BUILD_AS_MAIN_PROJECT) endif() endif() -# Configure CUDA/OptiX (creates interface targets even when disabled) +# Configure GPU dependency targets in dependency order. Each handler creates a +# stable interface target even when its feature is disabled. handle_cuda(TARGET ${CUDA_COMPILE_TARGET} MIN_VERSION 12.0) +handle_tensorrt(TARGET ${TENSORRT_COMPILE_TARGET}) handle_optix(TARGET ${OPTIX_COMPILE_TARGET} - CUDA_TARGET ${CUDA_COMPILE_TARGET}) + CUDA_TARGET ${CUDA_COMPILE_TARGET}) if (ENABLE_CUDA) # Add tools to manage cuda and ptx @@ -521,6 +532,7 @@ if (ENABLE_CUDA) message(STATUS "CUDA fast math (PTX compile) : ${CUDA_PTX_USE_FAST_MATH}") message(STATUS "CUDA extra NVCC flags : ${CUDA_NVCC_EXTRA_FLAGS}") endif() +message(STATUS "TensorRT enabled : ${ENABLE_TENSORRT}") message(STATUS "TBB enabled : ${ENABLE_TBB}") message(STATUS "CPU native tuning : ${CPU_ENABLE_NATIVE_TUNING}") message(STATUS "CPU SIMD enabled : ${CPU_ENABLE_SIMD}") @@ -589,46 +601,24 @@ endif() set(CPACK_GENERATOR "TGZ") set(CPACK_SOURCE_GENERATOR "TGZ") -# VERSION is generated in the build tree so configured package metadata remains -# authoritative even when the ignored source-tree fallback is stale. Stage that -# exact file through CPack's private install prefix for binary and source TGZs. -set(_cpack_stage_version_script - "${PROJECT_BINARY_DIR}/StagePackageVersion.cmake") -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/StagePackageVersion.cmake.in" - "${_cpack_stage_version_script}" - @ONLY) -if(CMAKE_VERSION VERSION_LESS "3.16") - set(CPACK_INSTALL_SCRIPT "${_cpack_stage_version_script}") -else() - list(APPEND CPACK_INSTALL_SCRIPTS "${_cpack_stage_version_script}") -endif() - -# Preserve source-ignore regexes verbatim and anchor known generated outputs -# beneath this checkout rather than matching adjacent directories. The ignored -# source VERSION must not overwrite the generated file staged above. +# Package the prepared source tree without claiming caller-owned CPack hooks. +# Anchor deterministic generated outputs and the exact active build below their +# physical paths so similarly named source directories remain package input. set(CPACK_VERBATIM_VARIABLES YES) set(_cpack_source_root_regex "${CMAKE_CURRENT_SOURCE_DIR}") string( REGEX REPLACE "([][+.*^$()|?\\\\])" "\\\\\\1" _cpack_source_root_regex "${_cpack_source_root_regex}") -set(CPACK_SOURCE_IGNORE_FILES +set(_cpack_binary_root_regex "${CMAKE_BINARY_DIR}") +string( + REGEX REPLACE "([][+.*^$()|?\\\\])" "\\\\\\1" + _cpack_binary_root_regex "${_cpack_binary_root_regex}") +list(APPEND CPACK_SOURCE_IGNORE_FILES "^${_cpack_source_root_regex}/(.*/)?\\.git(/|$)" - "^${_cpack_source_root_regex}/VERSION$" "^${_cpack_source_root_regex}/install/" "^${_cpack_source_root_regex}/ros2/(build|install|log)/" "^${_cpack_source_root_regex}/(.*/)?\\.pytest_cache/" "^${_cpack_source_root_regex}/(.*/)?__pycache__/" - "^${_cpack_source_root_regex}/.*\\.py[cod]$") - -# Refresh checkout-owned build exclusions when CPack runs. A generated build -# can appear after this configure, while path names alone remain insufficient -# evidence because build-prefixed directories may be legitimate sources. -set(_cpack_refresh_source_ignores_script - "${PROJECT_BINARY_DIR}/RefreshCPackSourceIgnores.cmake") -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/RefreshCPackSourceIgnores.cmake.in" - "${_cpack_refresh_source_ignores_script}" - @ONLY) -set(CPACK_PROJECT_CONFIG_FILE "${_cpack_refresh_source_ignores_script}") + "^${_cpack_source_root_regex}/.*\\.py[cod]$" + "^${_cpack_binary_root_regex}(/|$)") include(CPack) diff --git a/README.md b/README.md index d27471f..9a69683 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # cpp_cuda_template_project -A CMake template for building GPU-accelerated C++ libraries with optional CUDA/OptiX, Python/MATLAB bindings, and profiling support. Shared builds are the default, and static builds are selectable through standard CMake `BUILD_SHARED_LIBS`. Designed to be cloned and renamed into a real project. +A CMake template for building GPU-accelerated C++ libraries with optional CUDA, +OptiX, TensorRT, Python/MATLAB bindings, and profiling support. Shared builds +are the default, and static builds are selectable through standard CMake +`BUILD_SHARED_LIBS`. Designed to be cloned and renamed into a real project. ## Documentation Map - [`doc/template_usage.md`](doc/template_usage.md): cloning, renaming, source layout, nested consumers, and test placement. - [`doc/bootstrap_prompts.md`](doc/bootstrap_prompts.md): interactive agent prompt for tailoring the template into a fresh library. -- [`doc/cpp_cuda_build.md`](doc/cpp_cuda_build.md): C++ build modes, CUDA, OptiX, toolchains, CPU tuning, and profiling toggles. +- [`doc/cpp_cuda_build.md`](doc/cpp_cuda_build.md): C++ build modes, CUDA, + OptiX, TensorRT, toolchains, CPU tuning, and profiling toggles. - [`doc/wrappers.md`](doc/wrappers.md): gtwrap setup, Python package workflow, MATLAB wrappers, and wrapper docstrings. - [`doc/versioning.md`](doc/versioning.md): git tags, source/build/install `VERSION` files, C++ config macros, Python metadata, and packages. - [`doc/logging.md`](doc/logging.md): dependency-free component logging, level configuration, stream routing, and capture. @@ -20,7 +24,16 @@ Tailoring helper: ./tailor_template_cleanup.sh --apply --yes --project-namespace my_project ``` -The required namespace option replaces `template_project::logging` in the reusable logger sources and examples. Run the cleanup before a broad `template_project` replacement, because the script contains template-specific cleanup paths. After cleanup succeeds, delete `tailor_template_cleanup.sh` or exclude it from the rename pass. `profiling/` is removed by default. Add `--keep-profiling` when the new project should keep the Valgrind/perf helper scripts. +The required namespace option replaces `template_project::logging` in the +reusable logger sources and examples. Run the cleanup before a broad +`template_project` replacement, because the script contains template-specific +cleanup paths. Root/test CMake files, starter tests, and project workflows +remain unchanged. After cleanup succeeds, delete `tailor_template_cleanup.sh` +or exclude it from the rename pass. `profiling/` is removed by default. Add +`--keep-profiling` when the new project should keep the Valgrind/perf helper +scripts. TensorRT discovery and integration remain available to tailored +projects but stay dependency-neutral while `template_project_ENABLE_TENSORRT` +is `OFF`. ## Optional ROS 2 Overlay @@ -42,6 +55,7 @@ Use `./tailor_template_cleanup.sh --apply --yes --project-namespace my_project - | Eigen3 | ≥ 3.4 | Required | | CUDA Toolkit | ≥ 12.0 | Optional (`-DENABLE_CUDA=ON`) | | OptiX SDK | any | Optional (`-DENABLE_OPTIX=ON`), requires CUDA | +| TensorRT SDK | any | Optional (`-DENABLE_TENSORRT=ON`), requires CUDA | | oneTBB | any | Optional (`-DENABLE_TBB=ON`) | | Catch2 | 3.x | Auto-fetched from GitHub if not found | | pytest | any | Required when `ENABLE_PYTHON_TESTS=ON` and `test*.py` files are present | @@ -200,6 +214,7 @@ ignored with `--rebuild-only`. |---|---|---| | `template_project_ENABLE_CUDA` | OFF | CUDA GPU acceleration | | `template_project_ENABLE_OPTIX` | OFF | NVIDIA OptiX (enables CUDA automatically) | +| `template_project_ENABLE_TENSORRT` | OFF | NVIDIA TensorRT (enables CUDA automatically) | | `template_project_METADATA_ONLY` | OFF | Configure project identity/version without compiler languages | | `ENABLE_TBB` | OFF | Intel oneTBB support (`find_package(TBB)`) | | `ENABLE_OPENGL` | OFF | OpenGL support | @@ -232,12 +247,12 @@ ignored with `--rebuild-only`. | `WARNINGS_ARE_ERRORS` | OFF | Treat all warnings as errors (`-Werror`) | Replace the `template_project` prefix during tailoring. The historical -`ENABLE_CUDA`, `ENABLE_OPTIX`, and `PROJECT_METADATA_ONLY` options remain -top-level compatibility aliases; nested consumers must use the project-qualified -forms so parent cache options cannot change the library configuration. A legacy -alias supplied to a top-level configure wins for that invocation, is copied to -the canonical option, and is then removed from the cache so later reconfigures -cannot retain two conflicting sources of truth. +`ENABLE_CUDA`, `ENABLE_OPTIX`, `ENABLE_TENSORRT`, and `PROJECT_METADATA_ONLY` +remain top-level compatibility aliases; nested consumers must use the +project-qualified forms so parent cache options cannot change the library +configuration. A legacy alias supplied to a top-level configure wins for that +invocation, is copied to the canonical option, and is then removed from the +cache so later reconfigures cannot retain two conflicting sources of truth. ### Build type compiler flags @@ -288,6 +303,26 @@ When `ENABLE_OPTIX=ON`, configuration also fails fast unless the project contain This template treats OptiX on a header-only library as a configuration error. +### TensorRT + +TensorRT is opt-in and enables the CUDA feature automatically. Point either +`TensorRT_ROOT` or the compatibility spelling `TENSORRT_ROOT` at an SDK root +containing `include/` and `lib/`, or at an NVIDIA archive layout containing +`targets//include` and `targets//lib`: + +```bash +./build_lib.sh -D ENABLE_TENSORRT=ON \ + -D TensorRT_ROOT=/opt/TensorRT \ + -D CUDA_ARCHITECTURES=87 +``` + +The project target propagates `TensorRT::nvinfer`, +`TensorRT::nvinfer_plugin`, `CUDA::cudart`, and +`__TENSORRT_ENABLED__=1`. Installed consumers of a TensorRT-enabled build must +make a compatible SDK discoverable through the same root hints. The installed +package resolves its own finder directly and does not modify the consumer's +`CMAKE_MODULE_PATH`. + ### TBB ```bash @@ -646,7 +681,7 @@ The docs target is created only for the top-level project. Nested template-deriv │ └── global_includes.h Shared utilities (ANSI colors, precision constants) ├── cmake/ CMake module system (Handle*.cmake) ├── profiling/ Optional Valgrind/perf wrapper scripts -├── tests/ Catch2 unit tests and fixtures +├── tests/ Inherited runtime tests and reusable fixtures ├── examples/ │ ├── template_consumer_project/ Using the library via find_package() │ └── template_examples/ Standalone usage examples diff --git a/add_ros2_support.sh b/add_ros2_support.sh index 5253554..fd0876d 100755 --- a/add_ros2_support.sh +++ b/add_ros2_support.sh @@ -14,7 +14,6 @@ LIST_ONLY=1 VERIFY=0 NO_CI=0 ROS_PREFIX_OVERRIDE="" -PROJECT_WORKFLOW_MARKER="# project-ci-template: generic" cmake_project_name="" ros_package_prefix="" @@ -115,11 +114,8 @@ validate_source() { [[ -d "${SOURCE_DIR}/ros2" ]] || die "Source checkout is missing ros2/: ${SOURCE_DIR}" [[ -f "${SOURCE_DIR}/build_ros2.sh" ]] || die "Source checkout is missing build_ros2.sh: ${SOURCE_DIR}" if ((NO_CI == 0)); then - [[ -f "${SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml.tpl" ]] \ - || die "Source checkout is missing generic ROS 2 workflow template" - grep -Fqx -- "${PROJECT_WORKFLOW_MARKER}" \ - "${SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml.tpl" \ - || die "ROS 2 workflow template is missing its generic ownership marker" + [[ -f "${SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml" ]] \ + || die "Source checkout is missing the ROS 2 project workflow" fi } @@ -164,7 +160,7 @@ target_is_clean() { optional_target_="${ROOT_DIR}/.github/workflows/build_ros2_overlay.yml" if ((NO_CI == 0)) \ - && [[ -f "${SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml.tpl" \ + && [[ -f "${SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml" \ && -d "${ROOT_DIR}/.github/workflows" \ && ( -e "${optional_target_}" || -L "${optional_target_}" ) ]]; then warn "Target already has .github/workflows/build_ros2_overlay.yml: ${optional_target_}" @@ -198,7 +194,7 @@ Required copies: Optional copies when source and target directories exist: - doc/ros2_overlay.md - - generic .github/workflows/build_ros2_overlay.yml.tpl materialized as build_ros2_overlay.yml + - .github/workflows/build_ros2_overlay.yml - python/COLCON_IGNORE, lib/COLCON_IGNORE, examples/COLCON_IGNORE, tests/COLCON_IGNORE Never copied: @@ -394,7 +390,7 @@ copy_overlay() { if ((NO_CI)); then info "skipping CI workflow because --no-ci is set" else - copy_optional_file_if_possible ".github/workflows/build_ros2_overlay.yml.tpl" ".github/workflows/build_ros2_overlay.yml" + copy_optional_file_if_possible ".github/workflows/build_ros2_overlay.yml" ".github/workflows/build_ros2_overlay.yml" fi copy_colcon_marker_if_possible "python/COLCON_IGNORE" diff --git a/cmake/FindTensorRT.cmake b/cmake/FindTensorRT.cmake index 31a8a2e..d535d77 100644 --- a/cmake/FindTensorRT.cmake +++ b/cmake/FindTensorRT.cmake @@ -2,8 +2,8 @@ FindTensorRT ------------ -Find NVIDIA TensorRT headers and runtime libraries without enabling them in -the base template. +Find NVIDIA TensorRT headers and runtime libraries for optional project +integration and downstream consumers. Input hints ^^^^^^^^^^^ @@ -30,10 +30,14 @@ Imported targets include(FindPackageHandleStandardArgs) -set(TensorRT_ROOT "" CACHE PATH - "TensorRT root directory containing include/ and lib directories.") -set(TENSORRT_ROOT "" CACHE PATH - "Compatibility TensorRT root directory hint.") +if(NOT DEFINED TensorRT_ROOT) + set(TensorRT_ROOT "" CACHE PATH + "TensorRT root directory containing include/ and lib directories.") +endif() +if(NOT DEFINED TENSORRT_ROOT) + set(TENSORRT_ROOT "" CACHE PATH + "Compatibility TensorRT root directory hint.") +endif() # Accept package-style and established compatibility hints before conventional # system locations. Environment variables follow the same precedence. diff --git a/cmake/HandleTensorRT.cmake b/cmake/HandleTensorRT.cmake new file mode 100644 index 0000000..f07cab3 --- /dev/null +++ b/cmake/HandleTensorRT.cmake @@ -0,0 +1,51 @@ +# Configure optional TensorRT usage requirements on one interface target. + +include_guard(GLOBAL) +include(CMakeParseArguments) + +# handle_tensorrt(TARGET ) +# +# Create the named interface target in every configuration. When +# ENABLE_TENSORRT is true, require the project CUDA path and TensorRT SDK, then +# propagate the vendor runtime targets and feature definition to consumers. +function(handle_tensorrt) + set(one_value_args TARGET) + cmake_parse_arguments(HTRT "" "${one_value_args}" "" ${ARGN}) + + if(NOT HTRT_TARGET) + set(HTRT_TARGET tensorrt_compile_interface) + endif() + if(NOT TARGET ${HTRT_TARGET}) + add_library(${HTRT_TARGET} INTERFACE) + endif() + + if(NOT ENABLE_TENSORRT) + return() + endif() + + if(NOT ENABLE_CUDA) + message(FATAL_ERROR "ENABLE_TENSORRT requires ENABLE_CUDA=ON.") + endif() + if(NOT TARGET CUDA::cudart) + message(FATAL_ERROR + "ENABLE_TENSORRT requires CUDA::cudart from the configured CUDA toolkit.") + endif() + + # Keep SDK discovery in the reusable find module while this handler owns + # project integration policy and validates its required imported targets. + find_package(TensorRT REQUIRED MODULE) + + foreach(_tensorrt_target IN ITEMS TensorRT::nvinfer TensorRT::nvinfer_plugin) + if(NOT TARGET ${_tensorrt_target}) + message(FATAL_ERROR + "TensorRT discovery did not define required target ${_tensorrt_target}.") + endif() + endforeach() + + target_compile_definitions(${HTRT_TARGET} INTERFACE __TENSORRT_ENABLED__=1) + target_link_libraries( + ${HTRT_TARGET} INTERFACE + TensorRT::nvinfer TensorRT::nvinfer_plugin CUDA::cudart) + + message(STATUS "TensorRT enabled: ${TensorRT_VERSION}") +endfunction() diff --git a/cmake/HandleWrapper.cmake b/cmake/HandleWrapper.cmake index b834b6e..e54a5cc 100644 --- a/cmake/HandleWrapper.cmake +++ b/cmake/HandleWrapper.cmake @@ -40,9 +40,16 @@ function(resolve_local_wrap_root OUT_VAR) set(_preferred_root "${ARGV1}") endif() - if(NOT "${_preferred_root}" STREQUAL "" AND EXISTS "${_preferred_root}/cmake/PybindWrap.cmake") - set(${OUT_VAR} "${_preferred_root}" PARENT_SCOPE) - return() + if(NOT "${_preferred_root}" STREQUAL "") + get_filename_component( + _preferred_root + "${_preferred_root}" + REALPATH + BASE_DIR "${PROJECT_SOURCE_DIR}") + if(EXISTS "${_preferred_root}/cmake/PybindWrap.cmake") + set(${OUT_VAR} "${_preferred_root}" PARENT_SCOPE) + return() + endif() endif() set(_candidates diff --git a/cmake/RefreshCPackSourceIgnores.cmake.in b/cmake/RefreshCPackSourceIgnores.cmake.in deleted file mode 100644 index 1dc60e4..0000000 --- a/cmake/RefreshCPackSourceIgnores.cmake.in +++ /dev/null @@ -1,130 +0,0 @@ -# Refresh source-package exclusions from current checkout ownership evidence. -# -# CPack loads this project configuration immediately before generating each -# package. Discovering build trees here prevents a build created after CMake -# configuration from leaking into a source archive. - -set(_template_source_root [==[@CMAKE_CURRENT_SOURCE_DIR@]==]) -set(_template_binary_root [==[@CMAKE_BINARY_DIR@]==]) - -# Keep recursive cache discovery inside the physical checkout instead of -# traversing symlinked directory trees. -cmake_policy(PUSH) -cmake_policy(SET CMP0009 NEW) - -get_filename_component( - _template_source_root_real - "${_template_source_root}" - REALPATH) -get_filename_component( - _template_binary_root_real - "${_template_binary_root}" - REALPATH) - -# The active binary tree is owned by this configure whenever it is nested -# below the source checkout. -file( - RELATIVE_PATH - _template_binary_relative_to_source - "${_template_source_root_real}" - "${_template_binary_root_real}") -set(_template_owned_build_directories) -if(NOT IS_ABSOLUTE "${_template_binary_relative_to_source}" - AND NOT "${_template_binary_relative_to_source}" MATCHES "^\\.\\.(/|$)" - AND NOT "${_template_binary_relative_to_source}" STREQUAL "") - list(APPEND - _template_owned_build_directories - "${_template_binary_root}") -endif() - -# A nested cache proves ownership only when its configured home resolves to -# this exact source checkout. Skip already-owned and fixed generated trees -# before reading caches so concurrent cleanup cannot create a read race. -file( - GLOB_RECURSE - _template_cache_candidates - LIST_DIRECTORIES FALSE - "${_template_source_root}/*/CMakeCache.txt") -foreach(_template_cache_candidate IN LISTS _template_cache_candidates) - file( - RELATIVE_PATH - _template_cache_relative_to_source - "${_template_source_root}" - "${_template_cache_candidate}") - if("${_template_cache_relative_to_source}" MATCHES "^install/" - OR "${_template_cache_relative_to_source}" - MATCHES "^ros2/(build|install|log)/") - continue() - endif() - - set(_template_cache_is_within_owned_build FALSE) - foreach(_template_known_build IN LISTS _template_owned_build_directories) - file( - RELATIVE_PATH - _template_cache_relative_to_build - "${_template_known_build}" - "${_template_cache_candidate}") - if(NOT IS_ABSOLUTE "${_template_cache_relative_to_build}" - AND NOT "${_template_cache_relative_to_build}" MATCHES "^\\.\\.(/|$)") - set(_template_cache_is_within_owned_build TRUE) - break() - endif() - endforeach() - if(_template_cache_is_within_owned_build - OR NOT EXISTS "${_template_cache_candidate}") - continue() - endif() - - file( - STRINGS - "${_template_cache_candidate}" - _template_cache_home_entries - REGEX "^CMAKE_HOME_DIRECTORY:INTERNAL=" - LIMIT_COUNT 1) - if(NOT _template_cache_home_entries) - continue() - endif() - - list(GET _template_cache_home_entries 0 _template_cache_home_entry) - string( - REGEX MATCH - "^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$" - _template_cache_home_match - "${_template_cache_home_entry}") - get_filename_component( - _template_cache_home_real - "${CMAKE_MATCH_1}" - REALPATH) - if(NOT "${_template_cache_home_real}" STREQUAL - "${_template_source_root_real}") - continue() - endif() - - get_filename_component( - _template_owned_build_directory - "${_template_cache_candidate}" - DIRECTORY) - list(APPEND - _template_owned_build_directories - "${_template_owned_build_directory}") -endforeach() - -# Source configuration maps its stable rules to CPACK_IGNORE_FILES before this -# script runs. Append each absolute expression to both the descriptive source -# list and the active generator list. -list(REMOVE_DUPLICATES _template_owned_build_directories) -foreach(_template_owned_build_directory - IN LISTS _template_owned_build_directories) - string( - REGEX REPLACE "([][+.*^$()|?\\\\])" "\\\\\\1" - _template_owned_build_regex - "${_template_owned_build_directory}") - list(APPEND - CPACK_SOURCE_IGNORE_FILES - "^${_template_owned_build_regex}(/|$)") - list(APPEND - CPACK_IGNORE_FILES - "^${_template_owned_build_regex}(/|$)") -endforeach() - -cmake_policy(POP) diff --git a/cmake/StagePackageVersion.cmake.in b/cmake/StagePackageVersion.cmake.in deleted file mode 100644 index d4a5018..0000000 --- a/cmake/StagePackageVersion.cmake.in +++ /dev/null @@ -1,12 +0,0 @@ -# Stage authoritative version metadata inside CPack's private package root. - -set(_template_package_version_file [==[@PROJECT_BINARY_DIR@/VERSION]==]) -if(NOT EXISTS "${_template_package_version_file}") - message( - FATAL_ERROR - "Generated package VERSION is missing: ${_template_package_version_file}") -endif() - -file( - COPY "${_template_package_version_file}" - DESTINATION "${CMAKE_INSTALL_PREFIX}") diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 9e8bbd4..31c65fe 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -53,7 +53,7 @@ PROJECT_NUMBER = @FULL_VERSION@ # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = "GPU-accelerated C++ shared library template with optional CUDA/OptiX and Python/MATLAB bindings" +PROJECT_BRIEF = "GPU-accelerated C++ library template with optional CUDA, OptiX, TensorRT, and Python/MATLAB bindings" # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55 diff --git a/doc/bootstrap_prompts.md b/doc/bootstrap_prompts.md index 5da27a8..34353ab 100644 --- a/doc/bootstrap_prompts.md +++ b/doc/bootstrap_prompts.md @@ -13,7 +13,9 @@ Ask only for values that cannot be inferred from the user request or repository 2. Source layout: - Main C++ module directory replacing `src/template_src/`. - Whether CUDA is needed. If yes, CUDA module directory replacing `src/template_src_kernels/`; if no, remove the CUDA skeleton and matching `src/CMakeLists.txt` entry. - - Whether OptiX, oneTBB, OpenGL, examples, and standalone programs should stay enabled as project options. + - Whether OptiX, TensorRT, oneTBB, OpenGL, examples, and standalone programs + should remain supported as project options. Retaining a disabled optional + feature does not add its SDK as a dependency. 3. Wrappers and Python: - Whether to keep Python wrappers, MATLAB wrappers, both, or neither. - Python package name and minimum Python version if different from the template default. @@ -46,6 +48,8 @@ Ask only for values that cannot be inferred from the user request or repository - `.github/workflows/*.yml`: workflow names, artifact names, Pages text checks, and renamed CMake option prefixes. - `README.md`, `doc/main_page.md`, and public docs pages. 5. Remove skeletons that are not part of the requested project. When removing a directory, remove the matching CMake registration in the same change. + Keep `FindTensorRT.cmake` and `HandleTensorRT.cmake` when TensorRT remains a + supported opt-in feature; leave its canonical option `OFF` unless selected. 6. Search for stale template identifiers: ```bash diff --git a/doc/build_script_doc.md b/doc/build_script_doc.md index 7526bc3..940bbdb 100644 --- a/doc/build_script_doc.md +++ b/doc/build_script_doc.md @@ -133,13 +133,14 @@ CPU optimization is controlled through CMake definitions: ./build_lib.sh -D CPU_ENABLE_SIMD=ON -D CPU_SIMD_LEVEL=avx2 -D CPU_ENABLE_FMA=ON ``` -## CUDA And OptiX +## CUDA, OptiX, And TensorRT -CUDA and OptiX are opt-in: +CUDA, OptiX, and TensorRT are opt-in: ```bash ./build_lib.sh -D ENABLE_CUDA=ON ./build_lib.sh -D ENABLE_CUDA=ON -D ENABLE_OPTIX=ON +./build_lib.sh -D ENABLE_TENSORRT=ON -D TensorRT_ROOT=/opt/TensorRT ``` CUDA architecture selection order: @@ -163,6 +164,11 @@ CUDA optimization options: OptiX builds require at least one compiled library source and at least one `*.ptx.cu` source under `src/`. Header-only OptiX configurations fail during configure because there is no compiled library artifact to own the generated PTX integration. +TensorRT enables CUDA automatically. `TensorRT_ROOT` and `TENSORRT_ROOT` accept +conventional SDK roots and NVIDIA `targets/` archive layouts. The +installed package retains this external SDK dependency without changing a +consumer's `CMAKE_MODULE_PATH`. + ## Python And MATLAB Wrappers | Option | Purpose | diff --git a/doc/cpp_cuda_build.md b/doc/cpp_cuda_build.md index b7612da..e81aa72 100644 --- a/doc/cpp_cuda_build.md +++ b/doc/cpp_cuda_build.md @@ -61,6 +61,24 @@ GPU architecture is detected from `nvidia-smi` on x86_64. On Jetson/Tegra aarch6 Set `OPTIX_HOME` or use the system OptiX SDK layout expected by `cmake/HandleOptiX.cmake`. +## TensorRT + +`ENABLE_TENSORRT=ON` enables CUDA and links the project through the +`HandleTensorRT.cmake` interface target. Provide `TensorRT_ROOT` or +`TENSORRT_ROOT`; both conventional `include`/`lib` roots and NVIDIA +`targets/` archive layouts are supported. + +```bash +./build_lib.sh -D ENABLE_TENSORRT=ON \ + -D TensorRT_ROOT=/opt/TensorRT \ + -D CUDA_ARCHITECTURES=87 +``` + +The build and installed package expose `TensorRT::nvinfer`, +`TensorRT::nvinfer_plugin`, and `CUDA::cudart` through the project target. +Consumers of a TensorRT-enabled install must provide a compatible SDK root; +consumers of the default disabled build do not acquire a TensorRT dependency. + ## Optional Runtime Libraries Enable oneTBB when parallel CPU code needs it: diff --git a/doc/developments/derived_project_upgrade_agent_guidelines.md b/doc/developments/derived_project_upgrade_agent_guidelines.md index fa01650..d894d7d 100644 --- a/doc/developments/derived_project_upgrade_agent_guidelines.md +++ b/doc/developments/derived_project_upgrade_agent_guidelines.md @@ -65,8 +65,8 @@ Tailoring includes, but is not limited to: - project, package, namespace, target, artifact, and workflow names; - source layout, public APIs, executable structure, and module boundaries; - removed template skeletons, helpers, tests, examples, or optional features; -- enabled or disabled CUDA, OptiX, TBB, OpenGL, profiling, wrapper, docs, and - ROS support; +- enabled or disabled CUDA, OptiX, TensorRT, TBB, OpenGL, profiling, wrapper, + docs, and ROS support; - dependency providers, minimum versions, fetch policy, and offline policy; - CMake option names, defaults, install layout, exports, and package metadata; - Python or MATLAB package names, wrapper interfaces, and environment policy; diff --git a/doc/developments/template_v2_test_ownership_plan.md b/doc/developments/template_v2_test_ownership_plan.md index 63fa4d2..8ff4c65 100644 --- a/doc/developments/template_v2_test_ownership_plan.md +++ b/doc/developments/template_v2_test_ownership_plan.md @@ -1,17 +1,42 @@ -# Template v1.12.1 Hardening and v2 Test-Ownership Migration +# Template Consolidation and v2.0.0 Reconciliation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> `superpowers:executing-plans` to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking and stop at every staged-review gate. + +**Goal:** Simplify canonical source packaging, preserve TensorRT as an optional +first-class feature, reconcile the changes into v2 and TestField, finish the +v2.0.0 release review, and only then realign derived repositories. + +**Architecture:** A canonical source archive is built from a prepared checkout: +`generate_version.sh` synchronizes `VERSION` before CMake configures and CPack +packages that unchanged tree. CPack excludes deterministic generated paths and +the exact active binary tree without scanning arbitrary caches or claiming +public extension hooks. TensorRT discovery remains in `FindTensorRT.cmake`, +while `HandleTensorRT.cmake` owns opt-in feature activation and package export. + +**Tech Stack:** CMake 3.15+, CPack, Git-derived version metadata, C++20, +optional CUDA/TensorRT, CTest, Bash, and Python 3.12+. + +**Spec:** `doc/developments/template_v2_test_ownership_plan.md`, section +"Current simplification and reconciliation design" below. ## Status - Authoritative execution tracker for the v1.12.1 prerequisite and the subsequent v2.0.0 test-ownership migration. - Tracking started: 2026-07-28. -- Current stage: Stage 1 review gate, staged for user review. -- Template release baseline: signed `v1.12.0` tag at - `b277e4b84e2f1e501d6c2e73370efe0ecd101f23`. -- TestField working baseline: - `237eb7e67e709578a0e15e812e2a7568433eb017`. -- Final commits, pushes, PR mutations, and release tags require the review gates - stated below. +- Current stage: continuation Stage 3 clean v2 reconstruction from exact signed + primary candidate `0d8b1d7507d4bf7eea22d7f20a749a8977009486`. +- Template release baseline: signed `v1.12.1` tag at + `480d10a692836040bcae2023e763c553acfcc64d`. +- TestField release baseline: signed commit and signed annotated tag `v1.12.1` + at `f632290ce1bfb1f80baeeb3da2ea6db28a998037`. +- The obsolete v2 and TestField staged trees were explicitly rejected by the + user, proven byte-identical after unstaging, quarantined, and cleared before + this reconstruction. They are audit evidence, not implementation input. +- Final commits, pushes, PR mutations, and release tags require the review + gates stated below. No `v2.0.0` tag is authorized. This file is the single source of truth. Do not create a separate design, execution-plan, stage-output, discrepancy, or final-report document. @@ -92,9 +117,10 @@ compatibility layer. - Production wrapper modules, starter tests, MATLAB wrapper tests, and downstream custom tests survive unchanged except intentional project and namespace renaming. -- Template-development reports, conformance harnesses, and active - template-validation workflows do not survive tailoring. -- Generic derived-project workflows remain independent of TestField. +- The template carries only generic derived-project workflows; tailoring + preserves them directly and removes only the ROS workflow with the overlay. +- TestField-owned workflow conformance remains external to both template and + derived-project CTest. ## Execution rules @@ -110,6 +136,499 @@ compatibility layer. - No additional subagents are authorized after the completed Stage 1 maintainability review. +## Current simplification and reconciliation design + +### Global constraints + +- Preserve the PEP 440 `PYTHON_PACKAGE_VERSION` conversion independently of + source-package simplification. +- Preserve `FindTensorRT.cmake` as the portable discovery module and add + `HandleTensorRT.cmake` as the opt-in integration owner. +- Do not use `CPACK_PROJECT_CONFIG_FILE`, `CPACK_INSTALL_SCRIPT`, or + `CPACK_INSTALL_SCRIPTS` for template-owned source-package repair. +- Require `generate_version.sh` to synchronize `VERSION` before configuring a + release build; do not repair a stale checkout during CPack execution. +- Exclude fixed generated paths and the exact active binary directory only. + Do not recursively scan `CMakeCache.txt` files to infer ownership. +- Keep legitimate build-prefixed source directories and foreign child-project + caches in source archives. +- Use at least two functional commits: source-package simplification and + TensorRT feature integration. +- Never commit, tag, push, merge, or amend without the separate authorization + required for that exact operation. +- Stage only one reviewed batch at a time and wait for the exact keyword + `next` after the user has committed or cleared the preceding index. +- Do not stage or commit any derived repository during propagation. + +### Continuation Stage 0 - Snapshot and protect repository state + +**Files:** + +- Inspect the primary template Git index, branch, HEAD, and submodules. +- Inspect `/home/peterc/devDir/dev-tools/cpp_cuda_template_project-v2` paused + merge, Git index, branch, HEAD, `MERGE_HEAD`, and submodules. +- Inspect `/home/peterc/devDir/dev-tools/cpp_cuda_template_testfield-v2` Git + index, branch, HEAD, and submodules. + +- [x] Record the primary template HEAD, branch, index hash, and worktree status + before implementation edits. +- [x] Record the v2 template HEAD, `MERGE_HEAD`, staged-tree hash, cached-diff + hash, and absence or presence of unstaged changes without altering the merge. +- [x] Record the TestField v2 HEAD, staged-tree hash, cached-diff hash, and + absence or presence of unstaged changes. +- [x] Confirm that Stage 1 modifies only the primary template checkout. + +Stage 0 evidence, recorded 2026-08-14: + +- Primary template branch `feature/harden-ownership-and-workflow` was at + `4fb3e386ac55ef423f1d59ad686a4a90116b25b5`; its index was empty + (`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`) + and only this tracker was modified to persist the plan. +- The paused v2 merge remained at HEAD + `2ed71c4786b102b3a420846cc78bb628576b9c35` with `MERGE_HEAD` + `403c223f4b5abb39779bf2dd858bb4110405f9bf`. Its staged tree was + `3ced15a64dff580011af01f8c1036c62a5c34956`, its cached binary diff hash + was `3713ba1dcd5c5d7fb5af9f7ddc9d0dd165cf619a59efdcd26ed58514b14556cc`, + and it had no unstaged tracked changes or unresolved index entries. +- TestField v2 remained at + `bc0604f65da01be0a5ba141aadabd5dd08cf260e`; its staged tree was + `44e189c21b3d0a92d590fd026e54967727381a54`, its cached binary diff hash + was `9255ddcda418a1df080fc1555456e7263822dfd76fbcfe4ecb77a68b7147728a`, + and it had no unstaged tracked changes or unresolved index entries. +- No v2 or TestField file was changed while recording this snapshot. + +### Continuation Stage 1 - Simplify canonical source-package preparation + +**Files:** + +- Modify `tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake`. +- Modify `CMakeLists.txt`. +- Delete `cmake/StagePackageVersion.cmake.in`. +- Delete `cmake/RefreshCPackSourceIgnores.cmake.in`. +- Modify `doc/versioning.md` and this tracker. + +**Produces:** A prepared-checkout release contract that preserves caller-owned +CPack extension hooks and excludes the exact active binary tree without cache +discovery. + +- [x] Change the release fixture so `generate_version.sh --sync-ros2` + synchronizes `VERSION` before configuration and no file is mutated between + configure and CPack execution. +- [x] Add a harmless caller-owned `CPACK_PROJECT_CONFIG_FILE` fixture whose + observable archive exclusion proves that the template does not overwrite the + public hook. +- [x] Retain archive assertions for the exact active nested binary directory, + ROS-generated paths, legitimate build-prefixed source content, foreign + child-project caches, no-Git validation, and rejection of missing `VERSION`. +- [x] Remove the late-created-build and stale-source-`VERSION` expectations, + because both violate the prepared-checkout contract. +- [x] Run the focused verifier before production edits and record the expected + failure showing that the existing template overwrites the caller hook: + `cmake -DTEST_TEMPLATE_SOURCE_DIR="$PWD" + -DTEST_BINARY_ROOT=/tmp/cpp_cuda_template_source_package_red + -P tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake`. +- [x] Remove package-time `VERSION` staging and package-time ignore refresh + wiring from `CMakeLists.txt`. +- [x] Stop excluding the synchronized source-tree `VERSION` file. +- [x] Keep deterministic `.git`, install, ROS output, pytest cache, Python + bytecode, and active-binary-tree exclusions anchored below this checkout. +- [x] Delete the two package-time helper templates after all call sites are + removed. +- [x] Update versioning documentation to state the clean prepared-checkout + sequence and explain why caller-owned CPack hooks remain available. +- [x] Run the focused verifier again and require success. +- [x] Run `ctest --test-dir build --output-on-failure + -R '^template_project_release_tag_sync$'`, the CMake-floor policy, + `cmake --build build --target template_project_doc --parallel 4`, and + whitespace/conflict-marker checks. +- [x] Review every candidate line for correctness, scope, CMake 3.15 + compatibility, logical-block formatting, comments, and documentation. +- [x] Stage only the Stage 1 file allowlist, inspect `git diff --cached`, run + `git diff --cached --check`, and stop for user review without committing. + +Proposed commit: + +```text +[BUGFIX] Simplify canonical source package preparation + +- Require synchronized source version metadata before packaging + +- Exclude deterministic generated paths and the active build tree + +- Stop overriding public CPack extension hooks +``` + +Stage 1 evidence before staging, recorded 2026-08-14: + +- RED: the direct release verifier exited `1` at the new archive assertion + `Canonical source archive ignored the caller's CPack project hook` while the + old root CMake still replaced `CPACK_PROJECT_CONFIG_FILE`. +- GREEN: the same direct verifier exited `0` after the simplification. It + exercised exact-tag metadata synchronization, caller policy, active nested + binary exclusion, retained source fixtures, no-Git configuration, ROS + metadata, and missing-`VERSION` rejection. +- GREEN: registered CTest `template_project_release_tag_sync` passed `1/1` in + `3.68` seconds. +- GREEN: `template_project_doc` rebuilt with Doxygen 1.9.8 and exited `0`. +- GREEN: whitespace, conflict-marker, removed-helper-reference, and forbidden + post-CMake-3.15 API scans exited `0`. +- Simplification: package-time scripts fell from 142 lines to zero; the root + packaging policy now consists of anchored fixed exclusions plus the exact + active binary path and does not claim a public CPack extension hook. + +### Continuation Stage 2 - Promote TensorRT to an optional handled feature + +**Files:** + +- Modify `CMakeLists.txt`, `cmake/FindTensorRT.cmake`, `src/CMakeLists.txt`, and + `src/cmake/template_projectConfig.cmake.in`. +- Create `cmake/HandleTensorRT.cmake`. +- Modify `tests/cmake/VerifyTemplateProjectTensorRTModule.cmake`, + `tests/cmake/VerifyTemplateProjectNestedOptionIsolation.cmake`, `README.md`, + `tailor_template_cleanup.sh`, `doc/Doxyfile.in`, + `doc/bootstrap_prompts.md`, `doc/build_script_doc.md`, + `doc/template_usage.md`, `doc/cpp_cuda_build.md`, + `doc/developments/derived_project_upgrade_agent_guidelines.md`, and this + tracker. + +**Consumes:** `FindTensorRT.cmake`, `TensorRT::nvinfer`, +`TensorRT::nvinfer_plugin`, and the existing CUDA option normalization. + +**Produces:** `template_project_ENABLE_TENSORRT`, the top-level compatibility +alias, and a target-oriented integration path owned by +`HandleTensorRT.cmake`. + +- [x] Confirm that the Stage 1 index is empty because the user committed or + cleared it; otherwise stop without preparing this batch. +- [x] Extend the TensorRT verifier first with disabled, enabled, fake aarch64, + source/build/install consumer, missing-package quiet/required, and unchanged + caller `CMAKE_MODULE_PATH` cases. +- [x] Run the focused verifier and record the expected failure for the absent + handled feature. +- [x] Add `template_project_ENABLE_TENSORRT=OFF` and a top-level legacy alias; + normalize the option before language selection so enabling TensorRT implies + CUDA. +- [x] Add `HandleTensorRT.cmake` to call `find_package(TensorRT REQUIRED)` and + expose TensorRT and `CUDA::cudart` through the owning project target without + adding global module-path state. +- [x] Define `__TENSORRT_ENABLED__=1` only for enabled consumers. +- [x] Install `FindTensorRT.cmake` and include it directly from the generated + package config only when TensorRT was enabled at build time. +- [x] Preserve the existing portable root, environment, version, imported + target, x86_64, and aarch64 behavior in `FindTensorRT.cmake`. +- [x] Retain TensorRT as an explicit tailoring choice without adding a risky + automatic removal flag; document that the disabled production modules remain + dependency-neutral. +- [x] Run the focused TensorRT verifier, CPU configuration/build/tests, package + install/consumer acceptance, documentation, and static gates. +- [x] Review and stage only the Stage 2 allowlist, inspect the complete index, + run `git diff --cached --check`, and stop for user review without committing. + +Proposed commit: + +```text +[MAJOR] Add optional TensorRT feature integration + +- Keep portable TensorRT discovery in the installed package + +- Enable CUDA and target wiring only when TensorRT is requested + +- Preserve caller module paths in build-tree and installed consumers +``` + +Stage 2 evidence before staging, recorded 2026-08-14: + +- RED: the direct TensorRT verifier first failed because + `cmake/HandleTensorRT.cmake` and `handle_tensorrt()` did not exist. After the + handler was introduced, the nested-option verifier failed because the root + did not yet expose `template_project_ENABLE_TENSORRT`. +- RED: the enabled package consumer then failed because the generated package + config did not rediscover `TensorRT::nvinfer`. The first direct-include + attempt exposed two CMake integration defects: the finder erased a normal + `TensorRT_ROOT` hint under the CMake 3.15 policy baseline, and + `FindPackageHandleStandardArgs` observed the outer package name. +- RED: the real CUDA root acceptance finally failed because the exported + project omitted its TensorRT interface target. This proved that standalone + discovery alone was insufficient for build/install consumers. +- GREEN: the direct TensorRT verifier and nested-option verifier both exited + `0`. The registered tests passed `2/2`; the TensorRT verifier covered + canonical and compatibility roots, x86_64 and aarch64 archive layouts, + disabled and enabled handlers, build/install package consumers, missing SDK + QUIET/REQUIRED behavior, and a real CUDA configure/build/install/consumer + chain using local stub TensorRT libraries. +- GREEN: `./build_lib.sh --clean -j 4` configured and built the default CPU + project with TensorRT disabled and passed `32/32` tests. Doxygen 1.9.8 built + `template_project_doc`, and whitespace, conflict-marker, CMake-floor API, and + removed-helper scans exited `0`. +- Simplification: the final handler removed an unused parent-scope configured + flag and owns one stable interface target. TensorRT and OptiX join the export + set through independent conditions, while the root owns their shared CUDA + prerequisite. The root project description remains unchanged, avoiding an + unrelated ROS manifest metadata expansion; the two affected ROS tests and + the full CPU suite passed after that scope correction. +- Tailoring: the interactive checklist now treats TensorRT as a supported + feature decision. The cleanup helper retains its production discovery and + integration modules by default and deliberately provides no automatic + cross-file removal flag. + +### Continuation Stage 3 - Consolidate and version the template repositories + +- [x] Confirm that the user created both reviewed primary-template commits and + that the primary index is empty. +- [x] Re-run clean CPU, source archive, version, TensorRT, wrapper, tailoring, + documentation, shell, Python, JSON/YAML/XML, whitespace, and conflict-marker + gates against the exact committed primary tree. +- [x] Use signed primary candidate + `0d8b1d7507d4bf7eea22d7f20a749a8977009486` directly as the v2 integration + parent; do not add an intermediate main-branch merge with identical content. +- [x] Prove the rejected v2 and TestField worktrees match their obsolete staged + trees, quarantine them, clear both repositories, and start again from their + clean committed baselines. +- [x] Reconcile v2 by removing `StagePackageVersion.cmake.in` and + `RefreshCPackSourceIgnores.cmake.in`, retaining the prepared-checkout CPack + policy, and aligning its TensorRT feature with `HandleTensorRT.cmake`. +- [x] Reconcile the v2 development tracker and all directly dependent docs. +- [x] Stage the coherent v2 reconciliation batch and stop for user review + without committing or tagging. +- [ ] After the user commits v2, synchronize release metadata with + `./generate_version.sh --sync-ros2`, validate the exact candidate with the + intended local `v2.0.0` tag present, and verify the no-Git CPack archive. +- [ ] Stop for separate user authorization before creating, pushing, or + publishing the `v2.0.0` tag. + +Stage 3 primary evidence, recorded 2026-08-14: + +- The user-created source-package and TensorRT commits are signed commits + `73ab4fe0af7216abe4c97af17880e20d646ebea7` and + `8d073197ddfbc9d0e7a25b3d63384721da9f68ff`. Signed follow-up + `0d8b1d7507d4bf7eea22d7f20a749a8977009486` is the exact primary HEAD and v2 + integration parent validated here. +- GREEN: a detached worktree whose checkout basename remained + `cpp_cuda_template_project` ran `./build_lib.sh --clean -j 4`; configuration, + compilation, and all `32/32` CTest cases passed. This includes focused source + release, TensorRT, wrapper-maintenance, Python packaging, tailoring, version, + ROS-static, cross-compile, and consumer checks. +- GREEN: Doxygen 1.9.8 built `template_project_doc`; scoped Bash syntax and + ShellCheck, tracked-Python byte compilation, workflow YAML, devcontainer and + preset JSON, ROS XML, whitespace, conflict-marker, and CMake-floor API gates + exited `0` against exact HEAD. +- DISCREPANCY: the same exact commit built successfully in a detached worktree + with a noncanonical basename, where `31/32` tests passed. The container + launcher test alone hard-coded `/workspaces/cpp_cuda_template_project` while + production correctly derived the workspace slug from the checkout basename. + The staged test-only correction derives the expected slug independently and + passed both the registered case and all four direct launcher fixtures in the + renamed checkout. +- FOLLOW-UP: `0d8b1d75` makes TensorRT an explicit tailoring decision, corrects + finder/handler ownership documentation, and includes the container-test + portability repair. Its tree and binary diff exactly matched the reviewed + primary batch before the user committed it. + +Stage 3 superseded-state ledger, recorded 2026-08-14: + +- The user rejected and unstaged the old v2 and TestField batches. Every one of + the 27 v2 paths matched obsolete tree + `3ced15a64dff580011af01f8c1036c62a5c34956` byte-for-byte; every one of the + 12 TestField paths matched obsolete tree + `44e189c21b3d0a92d590fd026e54967727381a54` byte-for-byte. No extra + non-ignored paths existed in either worktree. +- The obsolete v2 merge was quit without committing, its tracked files were + restored from clean HEAD `2ed71c4786b102b3a420846cc78bb628576b9c35`, and + its added files were moved to + `/tmp/cpp_cuda_template_v2_obsolete_merge.5FEYqm`. TestField was restored to + clean signed HEAD `bc0604f65da01be0a5ba141aadabd5dd08cf260e`, with + its obsolete files moved to + `/tmp/cpp_cuda_template_testfield_v2_obsolete.X4RvFB`. +- The discarded source-package helpers and their CPack hooks are not present in + the clean v2 baseline. A freshly rewritten TestField release contract passed + against that baseline, proving the simpler prepared-checkout behavior must be + preserved rather than reimplemented. +- Python `PYTHON_PACKAGE_VERSION` projection remains independent of CPack + simplification and will be preserved because wheel metadata still requires + the PEP 440 representation of prerelease and local-version fields. +- TensorRT reconciliation will update `FindTensorRT.cmake`, add + `HandleTensorRT.cmake`, add the canonical and top-level compatibility + options, make TensorRT imply CUDA before language selection, export the + handled interface target, and resolve installed dependencies without + mutating a consumer's `CMAKE_MODULE_PATH`. +- v2 tailoring will present TensorRT as an explicit supported-feature decision. + Disabled retained modules remain dependency-neutral; complete removal stays + a reviewed cross-file operation rather than an automatic cleanup flag. +- Fresh TestField tests now own the observable caller CPack-hook contract, the + handled TensorRT feature, and TensorRT option isolation. Against clean v2, + release packaging passed while TensorRT and nested-option checks failed for + the expected missing behavior. +- Generic `VerifyTemplateProject*` implementations remain absent from the v2 + template's ordinary tests. Their reconciled forms stay owned and registered + by the standalone TestField harness. + +Stage 3 fresh v2 reconstruction evidence, recorded 2026-08-14: + +- A new no-commit merge uses exact signed primary candidate `0d8b1d75` as its + second parent. All conflicts were resolved from the clean v2 baseline rather + than from either quarantined obsolete tree; `git ls-files -u` is empty. +- The active v2 index contains 30 paths. Production packaging, TensorRT, + wrapper, build-helper, ROS metadata, and shared documentation files match + the reviewed primary candidate byte-for-byte. Intentional v2-only deltas + retain runtime-only tests, direct reusable workflows, stable tailoring + inputs, TestField ownership language, and source-relative gtwrap resolution. +- The active CPack policy contains no `StagePackageVersion`, + `RefreshCPackSourceIgnores`, `CPACK_PROJECT_CONFIG_FILE`, CPack install hook, + or recursive cache scan. It excludes deterministic generated paths plus the + exact active binary root and leaves caller-owned hooks untouched. +- Fresh v2 CPU configuration/build passed `7/7`; CUDA 12.9.41 with explicit + `CMAKE_CUDA_ARCHITECTURES=120` and OptiX 8 passed `9/9`; Doxygen 1.9.8 built + `template_project_doc`; ROS 2 Jazzy built all four packages and reported 10 + tests with zero errors, failures, or skips. +- Fresh external TestField conformance passed CPU `29/29` with the clean local + gtwrap checkout, docs `2/2`, CUDA/OptiX `13/13`, and ROS-static `2/2`. + TestField runtime passed `5/5`, its Python harness passed `11/11`, and both + repositories passed their scoped shell, Python, whitespace, conflict-marker, + and CMake-floor static gates. +- CUDA RED/GREEN: the transferred positive OptiX preflight initially required + cache type `STRING` even though a normal command-line definition produced + `UNINITIALIZED`. The TestField-owned verifier now checks the preserved value, + accepts the cache type as an implementation detail, and consumes the single + architecture selected by its profile rather than a duplicated literal 87. + +Stage 3 disposable reconciliation rehearsal, superseded 2026-08-14: + +- The following rehearsal remains historical comparison evidence only. The + user rejected its source staged trees, and no file is copied from it into the + fresh reconstruction. +- Reconstructed the exact protected v2 staged tree + `3ced15a64dff580011af01f8c1036c62a5c34956` and exact protected TestField + staged tree `44e189c21b3d0a92d590fd026e54967727381a54` in detached temporary + worktrees. +- RED: the old v2 tree ignored a caller-owned CPack project hook, lacked + `HandleTensorRT.cmake`, and failed to migrate the canonical TensorRT option. + The corrected caller fixture repeated the CPack failure against a second + exact-baseline worktree before production changes were accepted. +- GREEN: the focused prepared-source fixture, handled TensorRT verifier, and + nested-option verifier all exited `0` after reconciliation. The production + CMake, finder, handler, source export, and package config matched the primary + candidate byte-for-byte. +- GREEN: rehearsed v2 runtime passed CPU `7/7` and CUDA 12.9/OptiX `9/9` on + `sm_120`; Doxygen and scoped Bash/ShellCheck/static checks passed. +- GREEN: rehearsed TestField runtime passed `5/5`; the CPU harness passed all + `23/23` runnable tests with four explicit missing-gtwrap disables; docs passed + `2/2`, CUDA/OptiX passed `13/13`, and ROS-static passed `2/2`. +- The reconciled v2 target tree was + `543c61a22309ff01f5b56ed96236ce6e7c7b0b4b`; its 18-path semantic delta from + the protected staged tree had binary hash + `d415df996459ee9e5f72df19a3f37314f35b322f5460414040c02540beec7e22`. +- The reconciled TestField target tree was + `ff2797369b8c2934baaaa7fe98b69f1464943b8b`; its three-verifier semantic delta + from the protected staged tree had binary hash + `f2bbb550c12a5ed312ce70fbd75c8526508a360745a0b46bdc2b2fe08d678850`. +- The Git-cloning release verifier was not run against the dirty rehearsal + source because cloning would select the committed base instead of the + rehearsed index. Exact no-Git archive validation remains mandatory after the + real v2 commit and intended local release tag exist. +- After rehearsal, the protected v2 and TestField staged-tree and cached-diff + hashes remained byte-for-byte unchanged from the Stage 0 snapshot. +- The sibling Python template is already aligned and requires no propagation + edit. `/home/peterc/devDir/dev-tools/python_template_project` is clean on + `main` at `6642f3d61ec698c3590ab2a944990003be355531`; its `AGENTS.md` preserves + Python 3.10, configured Ruff/mypy policy, Google-style documentation, strong + typing, logical-block formatting, complexity reduction, and the same + operation-specific commit/staged-review authorization rules without + importing C++/CUDA, CMake, MATLAB, or ROS guidance. + +Stage 3 primary-history readiness audit, recorded 2026-08-14: + +- Immediately before appending this readiness record, the reviewed batch was + tree `02386ee44b3aff2cd0aaf2cb861b1b1f2607d21a` with cached binary-diff hash + `3fed5353f142ed8e8bf365bd0c750e10d250110a1e7eb8aa9616af937428180f`. + It had eight staged paths and no unstaged or untracked primary changes. +- The feature branch and `origin/main` merge at + `41d341ab949703dfdd63e5a6c243a117a2221666`. That merge-base tree and + `origin/main@403c223f4b5abb39779bf2dd858bb4110405f9bf` are byte-identical at + `f1eb7054810879b622386b3faeb3c252145b6287`, so the divergence is the PR merge + topology rather than competing file content. +- Every feature-only commit from `fedb4c2bf242e1bab33a1f250950586b976b0774` + through `8d073197ddfbc9d0e7a25b3d63384721da9f68ff` reports a valid Git signature. + Their messages contain no `Co-Authored-By`, AI-attribution, or sign-off + trailers. +- The ignored source `VERSION` still records development metadata from + `490de59` and the checkout describes as `v1.12.2-7-g8d07319-dirty`. This is + intentionally not synchronized before the staged follow-up and primary + history are finalized; the prepared-release gate must run against the exact + eventual v2 history and intended local `v2.0.0` tag. + +### Continuation Stage 4 - Align TestField with the post-simplification v2 + +- [x] Reconstruct TestField changes unstaged against the fresh v2 candidate; + reserve its staged-review batch until the exact committed v2 SHA exists. +- [x] Reconcile the TestField harness with the prepared-checkout source + package contract and the handled TensorRT feature. +- [x] Remove expectations for package-time repair, late-build cache scanning, + and copied helper templates. +- [x] Preserve TestField ownership of template conformance rather than copying + those recursive verifiers into derived projects. +- [x] Run CPU, source-package, TensorRT, wrapper, tailoring, docs, CUDA/OptiX + where available, ROS 2 where available, and static validation profiles. +- [ ] After the v2 commit exists, pin any TestField workflow references to its + exact SHA and rerun the affected static and candidate-selection checks. +- [ ] Stage only the coherent TestField v2 batch and stop for user review + without committing or tagging. +- [ ] After the user commits TestField, validate the exact paired template and + TestField SHAs and stop for separate authorization before any TestField + `v2.0.0` tag or push. + +### Continuation Stage 5 - Final reconciliation before propagation + +- [ ] Confirm primary template, v2 template, and TestField histories contain + the reviewed commits and have clear indexes. +- [ ] Confirm release metadata, package filenames, ROS manifests, and no-Git + archive validation all resolve exactly to `v2.0.0`. +- [ ] Compare template and TestField conformance inventories and account for + every removed, retained, or transferred assertion. +- [ ] Record exact commands, exit codes, test counts, skips, toolchain limits, + artifact hashes, and repository SHAs in this tracker. +- [ ] Obtain the user's explicit confirmation that consolidation, commits, and + versioning are complete before beginning propagation. + +### Continuation Stage 6 - Audit and realign derived repositories + +**Roots:** + +- `/home/peterc/devDir` +- `/media/peterc/SCRATCH_PRO/devDir/event-based-repos` + +- [ ] Discover Git repositories under both roots and identify template-derived + candidates from owned CMake, wrapper, packaging, and documentation markers. +- [ ] Record each candidate repository, branch, HEAD, dirty state, imported + template version when identifiable, and matching overcomplex CPack/TensorRT + changes before editing any repository. +- [ ] Exclude the primary template, its worktrees, TestField, unrelated + repositories, generated build trees, vendored dependencies, and submodules + from propagation targets. +- [ ] For every affected derived repository, remove imported package-time + `VERSION` staging, recursive cache scanning, and template-owned public CPack + hook overrides; align its archive contract to a prepared checkout and its + exact active binary directory. +- [ ] Preserve and align TensorRT only where the derived project already owns + or requests that optional feature; use its local namespace and targets rather + than mechanically copying template names. +- [ ] Do not import template-only recursive conformance tests into ordinary + derived-project CTest suites. +- [ ] Review each derived diff for local ownership, behavior, formatting, + comments, documentation, and unnecessary complexity. +- [ ] Run the smallest honest local configure/build/test/package acceptance for + each affected repository and record environment-dependent skips. +- [ ] Leave every derived repository unstaged and uncommitted, then report the + per-repository changes and verification for user review. + +### Continuation Stage 7 - Final report + +- [ ] Confirm no unauthorized commit, tag, push, or derived-repository staging + occurred. +- [ ] Summarize the final architecture, exact repository SHAs, staged or + unstaged state, validations, caveats, and remaining user actions. +- [ ] Mark the tracked goal complete only when all authorized work and review + gates have genuinely finished. + ## Stage 0 - Baseline and plan rebase - [x] Inspect the current template and TestField branches, tags, indexes, and @@ -266,9 +785,18 @@ compatibility layer. ### Stage 1 review gate -Stop with the template staged and uncommitted. The user reviews the batch, -creates the release commit, and creates the annotated `v1.12.1` tag. Do not -push or mutate PR threads. +Completed release outcome: + +- the implementation was committed as signed commit `d398c20` with the + approved title and bulleted description; +- the four ROS 2 manifest versions were completed in signed follow-up + `480d10a`; +- signed annotated tag `v1.12.1`, the remote branch, and the remote tag all + dereference to `480d10a`; +- PR 28 and tag-triggered native, ROS 2, documentation, tailoring, and rollout + checks completed successfully; +- remote CUDA remained policy-skipped because `CI_USE_SELF_HOSTED` was not + enabled, and was covered by Stage 2 local accelerator acceptance. Proposed title: @@ -287,63 +815,349 @@ Proposed description: ## Stage 2 - TestField v1.12.1 alignment -- [ ] Verify the reviewed template tag and synchronized release metadata. -- [ ] Reconcile the existing TestField index with the exact v1.12.1 tag. -- [ ] Port production behavior while preserving TestField identity, APIs, ROS +- [x] Verify the reviewed template tag and synchronized release metadata. +- [x] Reconcile the existing TestField index with the exact v1.12.1 tag. +- [x] Port production behavior while preserving TestField identity, APIs, ROS adaptations, and local tests. -- [ ] Keep template conformance external to normal TestField CTest. -- [ ] Maintain one combined v1.12.1 sync document, design first and plan second. -- [ ] Run CPU, docs, CUDA, ROS 2, wrapper, wheel, install/consumer, cleanup, and +- [x] Keep template conformance external to normal TestField CTest. +- [x] Maintain one combined v1.12.1 sync document, design first and plan second. +- [x] Run CPU, docs, CUDA, ROS 2, wrapper, wheel, install/consumer, cleanup, and release-archive acceptance. -- [ ] Apply the complete staged-code quality gate. -- [ ] Amend the existing unpushed signed documentation commit into one coherent +- [x] Apply the complete staged-code quality gate. +- [x] Amend the existing unpushed signed documentation commit into one coherent v1.12.1 alignment commit. -- [ ] Stop without pushing for user review and TestField v1.12.1 tagging. +- [x] Create and verify the signed annotated local TestField `v1.12.1` tag, + then stop without pushing. Proposed title: `[BUGFIX] Align TestField with template v1.12.1 packaging` +### Stage 2 evidence + +- Source release: + - signed local and remote `v1.12.1` dereference to `480d10a`; + - all four template ROS 2 manifests contain `1.12.1`; + - all terminal PR/tag checks passed except the documented self-hosted CUDA + policy skip. +- RED, released wrapper contract: the exact v1.12.1 Python-packaging verifier + exited `1` against the old TestField candidate because the configure-time + analyzer rejected the collision fixture before resolved-name staging. +- GREEN, released wrapper contract: after the production-module port, the same + external verifier exited `0`. +- RED, release-cache race: the extended TestField release fixture exited `1` + when the old archive scan read a deliberately disappearing + `build_transient/CMakeCache.txt`. +- GREEN, release-cache race: active and already-owned build paths are filtered + before cache reads, and the extended release fixture exited `0`. +- RED, TestField static mode: the external installed-consumer verifier exited + `1` because the TestField target hardcoded `SHARED` and produced no + `libtemplate_project.a`. +- GREEN, TestField build modes: independent shared and static install/consumer + checks each configured, built, linked, and ran successfully. +- GREEN, final fresh CPU: + `./build_lib.sh -B build_v112_cpu --clean ...` exited `0`; CTest passed + `40/40`. +- GREEN, documentation: `template_project_doc` completed with Doxygen 1.9.8. +- GREEN, real wrapper and installs: + - `--gtwrap-root lib/wrap` built the Python wrapper successfully; + - isolated wheel and CMake-prefix imports passed without checkout loader + paths; + - the wheel excluded `_wrapper_build.py`, contained only declared native + artifacts, and both artifacts used loader-relative runtime paths. +- GREEN, accelerators: + - CUDA 12.9.41 selected `sm_120` and passed `41/41`; + - the explicit OptiX SDK build passed `17/17`. +- GREEN, ROS 2 Jazzy: + `./build_ros2.sh --clean --no-version-sync` built all four packages and + reported `10` tests, `0` errors, `0` failures, and `0` skips. +- GREEN, final focused checks: released Python packaging, clean ownership, + TestField source release, shared consumer, and static consumer verifiers each + exited `0`. +- GREEN, static hygiene: Bash syntax, ShellCheck, Python compilation, YAML/XML + parsing, whitespace, conflict-marker, and forbidden-newer-CMake-API scans + passed. The host does not provide the exact CMake 3.15 runtime. +- Signed release preparation: + - commit `f632290ce1bfb1f80baeeb3da2ea6db28a998037` uses the approved + `[BUGFIX]` title and four-paragraph bulleted body; + - signed annotated tag `v1.12.1` dereferences to the same commit and uses the + established `Release v1.12.1 testfield` message; + - exact-tag `generate_version.sh --sync-ros2` derived `1.12.1`, left the four + tracked manifests unchanged, and wrote only the ignored `VERSION`; + - TestField `main` and remote `v1.12.1^{}` now both resolve to `f632290`; + - branch and tag native CI runs `30355250583` and `30355250624` passed + `35/35` applicable hosted-runner tests each; + - branch and tag ROS 2 runs `30355250665` and `30355250725` each built four + packages and reported `10` tests, `0` errors, `0` failures, and `0` skips; + - documentation run `30355250590` passed with Doxygen 1.9.8; + - tag CUDA run `30355250623` was policy-skipped because self-hosted CI is + disabled, matching the recorded local-accelerator substitution. + ## Stage 3 - TestField-owned v2 harness -- [ ] Inventory every conformance test, label, timeout, prerequisite, resource +- [x] Inventory every conformance test, label, timeout, prerequisite, resource lock, skip, and behavioral assertion. -- [ ] Create the standalone TestField harness and explicit profile inputs. -- [ ] Move template-system conformance implementations into TestField while +- [x] Create the standalone TestField harness and explicit profile inputs. +- [x] Move template-system conformance implementations into TestField while their original template counterparts remain available for parity. -- [ ] Keep TestField local tests independent of the candidate template. -- [ ] Remove the main-project external-test facade and implicit sibling path. -- [ ] Compare old and new inventories and results against the same candidate. -- [ ] Resolve every unexplained lost or duplicated assertion. -- [ ] Update only CI wiring required for full-SHA sibling harness execution. +- [x] Keep TestField local tests independent of the candidate template. +- [x] Remove the main-project external-test facade and implicit sibling path. +- [x] Compare old and new inventories and results against the same candidate. +- [x] Resolve every unexplained lost or duplicated assertion. +- [x] Update only CI wiring required for full-SHA sibling harness execution. + +### Stage 3 conformance inventory + +The inventory baseline is the exact template and TestField `v1.12.1` source, +their generated CTest JSON, and the successful Stage 1 and Stage 2 build trees. +The template currently exposes `21` CPU-applicable conformance entries: +`18` CMake-script entries and `3` pytest-file entries. CUDA adds one +template-system verifier plus the two retained CUDA runtime entries. OptiX adds +one conditional template-system verifier. TestField's transitional external +block contributes up to `24` entries, four of which invoke the candidate's +existing verifier rather than a TestField-owned implementation. + +Template-system assertions to migrate: + +| Owner file | Profile | Existing CTest contract | Behavioral assertions | +|---|---|---|---| +| `VerifyTemplateProjectNoOptimization.cmake` | `cpu` | `flags;noopt`, 180 s | `RelWithDebInfo` emits all required no-optimization/debug flags, emits none of the optimized/native/`NDEBUG` flags, and builds. | +| `VerifyTemplateProjectOptimizedFlags.cmake` | `cpu` | `flags;optimized`, 180 s | `Release` and `RelWithDebInfo` emit their required optimization/debug definitions, reject profiling/no-opt/sanitizer flags, and build. | +| `VerifyTemplateProjectDocsWorkflow.cmake` | `docs` | `docs;doxygen`, 240 s | strict docs configure/build succeeds; HTML/XML and Doxyfile exist; public inputs, exclusions, main page, and documented topics are present; internal development/report content is absent. | +| `VerifyTemplateProjectNestedDocsIsolation.cmake` | `docs` | `docs;nested`, 180 s | a nested consumer configures/builds while the nested template contributes neither a parent-visible `doc` target nor a Doxyfile. | +| `VerifyTemplateProjectNestedInstallHeaders.cmake` | `cpu` | `install;nested;template`, 240 s | nested install destinations cannot escape the advertised include root; the exact public and logger headers install without root leaks; an installed-only consumer resolves only the scratch package and builds. | +| `VerifyTemplateProjectVersionSideEffects.cmake` | `cpu` | `configure;version`, 180 s | configure writes build-tree `VERSION` while `WRITE_SOURCE_VERSION_FILE=OFF` neither creates nor changes source `VERSION`. | +| `VerifyTemplateProjectPythonTestOptions.cmake` | `cpu` | `tests;python`, 180 s | disabled tests ignore invalid Python runner/conda settings; disabled Python tests do the same; enabled Python tests reject conflicting conda name/prefix settings. | +| `VerifyTemplateProjectBuildLibCleanSafety.cmake` | `cpu` | `build;clean;safety`, 60 s | `--clean` rejects external, missing-cache, and foreign-cache targets with the intended diagnostic; an owned conventional build is removed and recreated; `--rebuild-only` ignores clean without deleting an external build. | +| `VerifyTemplateProjectPythonPackaging.cmake` | `cpu` | `install;package;python;wrapper`, 180 s | absolute Python install roots are rejected; resolved runtime/runtime, runtime/extension, and target/SONAME collisions fail before partial staging; generator-expression names work; declared unlinked runtimes refresh; wheel and CMake installs contain exactly the declared native artifacts, exclude checkout metadata/cache files, import without checkout loader paths, and use loader-relative paths without scratch paths. | +| `VerifyTemplateProjectBuildTreePackage.cmake` | `cpu` | `package;template`, 180 s | build-tree package/config exports are colocated, source config is not polluted, the namespaced target and `cxx_std_20` propagate, and a build-tree consumer configures/builds. | +| `VerifyTemplateProjectTailoringScript.cmake` | `cpu` | `tailoring;template`, 60 s | list mode is non-mutating; template-only/profiling removal and retention are exact; scripts/workflows preserve modes; logger files survive with namespace tailoring; production wrapper modules survive; default and ROS-removed outputs are correct; malformed fences, namespaces, and missing workflow templates fail without any tree mutation. | +| `VerifyTemplateProjectAddTestsProperties.cmake` | `cpu` | `tests;cmake_utils`, 60 s | Catch2 property resolution accepts a literal list, an indirect variable, empty input, and a single-token literal without changing values. | +| `VerifyTemplateProjectRos2Overlay.cmake` | `ros2` | `ros2;template`, 60 s | required overlay inputs/fences and metadata-only configure are valid; rollout list/collision/no-CI paths are non-destructive; generated paths, placeholder replacement, one active workflow, identifier-boundary renaming, split names, explicit ROS prefix, and cache/unrelated-path exclusions are exact. | +| `VerifyTemplateProjectReleaseTagSync.cmake` | `cpu` | `release;ros2;version`, 180 s | a disposable Git release moves from preparation to one exact final tag; metadata sync changes only four ROS manifests; tag/manifests/package names agree; the canonical archive excludes owned active/generated builds while retaining foreign caches and legitimate `install`/source paths; extracted validation succeeds; the source checkout and tags remain unchanged. | +| `VerifyTemplateProjectCudaSources.cmake` | `cuda` | `cuda;sources;template`, 240 s; `ENABLE_CUDA` | isolated CUDA target builds, the CUDA placeholder belongs to its compile graph, and the OptiX PTX input does not compile as an ordinary source when OptiX is off. | +| `VerifyTemplateProjectOptixInstallExport.cmake` | `cuda` | `optix;install;package;template`, 360 s; `ENABLE_OPTIX` and SDK | the OptiX target builds/installs; exactly one target export exists; it leaks neither the build-machine SDK root nor a nonexistent package-local SDK; an installed consumer finds OptiX explicitly and builds. | +| `VerifyTemplateProjectCudaWithoutCatch2.cmake` | `cuda` | `cuda;catch2;configure`, 180 s; `nvcc` | CUDA configures and builds successfully with tests/Catch2 disabled. | +| `VerifyTemplateProjectCrossCompile.cmake` | `cpu` | three `cross;aarch64` entries, 180 s each; GNU aarch64 toolchain | cross compile commands reject host-native flags and require cross/aarch64/Linux definitions; the core target, installed consumer, and nested consumer each configure/build through the selected toolchain. | +| `testDevcontainerJson.py` | `cpu` | `pytest;python`, 120 s | JSONC comments and unmanaged values survive; Docker, Podman CDI, and disabled-CUDA GPU arguments normalize exactly; the shell configurator forwards the selected runtime. | +| `testWorkflowTemplates.py` | `cpu` | `pytest;python`, 120 s; PyYAML | active/dormant workflows parse; release tags, explicit CUDA opt-in, path ownership, job topology, full-history checkout, shell syntax, Pages actions, ROS ordering, drift rejection, marker-free metadata helper execution, and structured repository configuration satisfy their parser-backed contracts. | +| `testRos2OverlayStatic.py` | `ros2` | `pytest;python`, 120 s | metadata-only configure exports standard fields without enabling C++; manifests match root metadata; ignore markers exist; missing ROS environment fails before mutation; copied metadata synchronization preserves names, dependencies, URLs, modes, XML model instructions, and idempotence; `--no-sync-ros2` is non-mutating. | + +Additional TestField-owned candidate-template assertions already present in the +transitional block: + +| Owner file or case | Profile | Existing properties and prerequisites | Behavioral assertions | +|---|---|---|---| +| `VerifyTemplateProjectBuildMode.cmake`, shared/static | `cpu` | no labels/timeout | each selected library kind installs the exact artifact; an installed downstream consumer configures, builds, exists, and runs. | +| Candidate docs, nested-docs, version, and tailoring entries | mixed | shared `template_project;docs;version`, 240 s | exact duplicates of four template-owned entries above; migrate once, never duplicate them in the standalone union. | +| `VerifyTemplateProjectPythonPackage.cmake`, Python 3.12/3.11 | `cpu` | `template_project;python;wrapper`, 240 s, `template_project_python_wrapper` lock; interpreter and gtwrap | the wrapper builds and passes its checkout import test; generated interpreter metadata is exact; isolated pip install/import succeeds on 3.12 and is rejected on 3.11. | +| `VerifyMatlabWrapperSmoke.cmake`, candidate case | `cpu` | `matlab;wrapper;elf`, 600 s, `matlab` lock, skip regex `MATLAB executable not found`; MATLAB and gtwrap | the candidate MATLAB wrapper configures/builds, toolbox/MEX/core artifacts exist, tcmalloc linkage matches policy, and the MATLAB smoke executes. The two TestField-source cases remain TestField acceptance, not candidate conformance. | +| `VerifyTemplateProjectCudaArchDetection.cmake`, seven cases | `cuda` | 30 s each | valid x86 `nvidia-smi` and Xavier/Orin/Thor fixtures select exact architectures; missing/malformed x86 discovery and ambiguous aarch64 discovery fail. | +| `VerifyTemplateProjectOptixPreflight.cmake`, three cases | `cuda` | negative cases use the wrapper resource lock and 120 s; positive case requires `nvcc`, `nvidia-smi`, and SDK | header-only and missing-PTX sources fail before OptiX handling; an explicit positive SDK configure retains OptiX and architecture 87. | +| `VerifyTemplateProjectBuildLibWrapper.cmake`, three cases | `cpu` | wrapper resource lock, 120 s; gtwrap for success | `build_lib.sh --python-wrap` builds the module; a missing interface disables the wrapper cleanly; rebuild-only preserves the configured wrapper-off cache and produces no module. | + +Retained starter/runtime entries are outside the migration union: five logger +Catch2 cases, the C++ placeholder, Python import smoke, CUDA initialization +fixture and buffer round-trip, target-owned MATLAB regression/ELF checks, +reusable fixtures, and ROS 2 package runtime tests. TestField's own two Catch2, +three pytest, and project-identity acceptance contracts also remain distinct +from candidate-template conformance. + +Inventory findings: + +- no existing entry uses `SKIP_RETURN_CODE`, `DEPENDS`, or CTest dependency + fixtures for template-system conformance; +- the only conformance skip is the MATLAB + `SKIP_REGULAR_EXPRESSION`; CUDA, OptiX, cross, and interpreter prerequisites + currently omit tests at configure time; +- four transitional entries execute verifier implementations from the + candidate checkout, so TestField does not yet own them; +- build-mode, CUDA-architecture, OptiX-preflight, build-helper-wrapper, and + positive OptiX entries have incomplete labels; the standalone registry must + assign one consistent `template_harness` label plus profile/contract labels; +- the wrapper resource lock currently serializes Python package, negative + OptiX preflight, and build-helper wrapper checks; retain it through parity, + then remove it only if source/build isolation proves concurrent safety; +- normal TestField CTest currently registers recursive TestField + configure/build/install checks. They are not candidate-template conformance; + their ownership must be reconciled with the derived-project acceptance + policy before the Stage 5 handoff. + +### Stage 3 execution evidence + +- Baseline: + - isolated branch `major/refactor-tests-ownership` starts at exact TestField + `v1.12.1`, `f632290ce1bfb1f80baeeb3da2ea6db28a998037`; + - the unchanged baseline build passed `40/40`. +- Harness TDD: + - RED: the first public configure test failed because + `template_harness/CMakeLists.txt` did not exist; + - RED: exact profile assertions reported incomplete CPU and empty docs, + CUDA, and ROS 2 inventories; + - GREEN: `tests/harness/testTemplateHarness.py` passed `11/11`, covering + required inputs, semantic diagnostics, exact disjoint inventories, the + duplicate-free `41`-test union, explicit candidate propagation, and + TestField-owned nested release verifiers. +- Candidate parity against template v1.12.1: + - CPU passed `24/24` in `75.82` seconds; + - docs passed `2/2`; + - CUDA/OptiX passed `13/13` with CUDA architecture `120`; + - ROS 2 static conformance passed `2/2`; + - the release verifier passed again after its nested ROS/source-archive + paths were redirected to TestField-owned implementations; + - candidate MATLAB and release checks passed after shared verifier + deduplication. +- TestField independence: + - RED: an isolated TestField source copy failed configure because the + transitional facade required `../cpp_cuda_template_project`; + - GREEN: the same isolated configure passed with no candidate checkout, + candidate CTest entries, or facade cache variables; + - the local Python option verifier passed after its external-only case and + input were removed. +- TestField acceptance ownership: + - RED: ordinary CTest still exposed `testfield_*` recursive acceptance and no + standalone acceptance source existed; + - GREEN: exact standalone inventories contain `11` CPU, `2` docs, and `1` + CUDA entry; ordinary CTest contains no `testfield_*` acceptance entries; + - full TestField acceptance passed `14/14` in `23.49` seconds. +- Documentation discrepancy: + - RED: strict Doxygen rejected the new harness README link because that file + was not an input; + - GREEN: the guide is an explicit single-file input; + - RED: the docs verifier then proved Doxygen enumerated non-input `lib/` and + binary trees through unnecessary `EXCLUDE` directories; + - GREEN: removing those directories retained the narrow allow-list and + stopped both traversals. +- CI and dependency wiring: + - parser-backed workflow contracts passed `16/16`; + - native, docs, CUDA, and ROS workflows pin template commit + `480d10a692836040bcae2023e763c553acfcc64d` and run their owning candidate + profile; + - native, docs, and CUDA workflows explicitly run TestField acceptance; + - the stale non-gitlink template submodule declaration is removed. +- Deduplication: + - transitional candidate registrations and their old TestField fixtures are + deleted; + - MATLAB/tcmalloc and extracted-source validation reuse one shared + TestField-owned implementation; + - exact inventory and behavioral parity show no unexplained lost or + duplicated assertion. ## Stage 4 - Template reduction and tailoring simplification -- [ ] Remove migrated template-conformance implementations and registrations +- [x] Remove migrated template-conformance implementations and registrations only after Stage 3 parity is green. -- [ ] Retain all starter-project runtime tests and fixtures. -- [ ] Make `tests/CMakeLists.txt` stable for derived projects. -- [ ] Remove root- and test-CMake rewriting from tailoring. -- [ ] Preserve production wrapper modules and downstream custom tests. -- [ ] Run default and `--remove-ros2` tailoring twice for idempotence. -- [ ] Build and test both tailored results. -- [ ] Prove tailored projects contain no TestField or template-conformance +- [x] Retain all starter-project runtime tests and fixtures. +- [x] Make `tests/CMakeLists.txt` stable for derived projects. +- [x] Remove root- and test-CMake rewriting from tailoring. +- [x] Preserve production wrapper modules and downstream custom tests. +- [x] Run default and `--remove-ros2` tailoring twice for idempotence. +- [x] Build and test both tailored results. +- [x] Prove tailored projects contain no TestField or template-conformance dependency. +### Stage 4 execution evidence + +- Ownership TDD: + - RED: the expanded TestField tailoring verifier exited `1` before cleanup + and enumerated all `18` candidate-owned `VerifyTemplateProject*` files; + - GREEN: the same verifier passed after the migrated CMake verifiers, + source-release verifier, devcontainer contract, workflow contract, and + static ROS contract were removed from the template. +- Stable starter suite: + - `tests/CMakeLists.txt` now contains only inherited runtime registration; + - fresh untailored CPU configure/build passed `7/7`: five logger cases, one + C++ starter case, and one Python import smoke; + - C++/Python/CUDA starter files, reusable fixtures, MATLAB wrapper smoke, and + target-owned dependency checks remain in the template. +- Tailoring simplification: + - root- and test-CMake patch functions and calls were deleted; + - the TestField fake project proves both CMake files remain byte-for-byte + stable, production wrapper modules survive, and even a downstream custom + `VerifyTemplateProjectCustomBehavior.cmake` file is preserved; + - unreachable non-apply mutation branches were removed from the cleanup + implementation. +- Workflow single source: + - RED: the v2 tailoring contract rejected four dormant `.yml.tpl` workflow + copies; + - GREEN: generic CPU, CUDA, ROS, and Pages workflows are the only runnable + definitions and parse successfully; + - TestField's parser-backed workflow suite passed `11/11`; + - ROS rollout RED identified its obsolete `.tpl` dependency, then GREEN + passed after it copied the canonical active workflow directly. +- Wrapper follow-ups: + - RED: a configure fixture resolved a valid source-relative gtwrap checkout + to an empty path; + - GREEN: the resolver canonicalizes the spelling against + `PROJECT_SOURCE_DIR`, and the existing shell-wrapper verifier passed; + - fallback Python package metadata now requires `>=3.12`. +- Real tailored copies: + - default and `--remove-ros2` copies each produced identical complete + path/mode inventories and file hashes between first and second cleanup; + - root and test CMake SHA-256 values were unchanged across cleanup; + - both copies configured, built, and passed `7/7`; + - the default copy retained `ros2/` and its workflow, while the ROS-removed + copy retained neither; + - root CMake, production modules, tests, workflows, and shell helpers contain + no TestField, standalone-harness, or `VerifyTemplateProject` dependency. + ## Stage 5 - Integrated v2 validation and release preparation -- [ ] Run TestField local tests without a template checkout. -- [ ] Run all applicable standalone harness profiles. -- [ ] Run clean CPU, docs, CUDA/OptiX, ROS 2, wrapper, install/consumer, +- [x] Run TestField local tests without a template checkout. +- [x] Run all applicable standalone harness profiles. +- [x] Run clean CPU, docs, CUDA/OptiX, ROS 2, wrapper, install/consumer, release, shell, Python, YAML, XML, and Git-hygiene gates. -- [ ] Verify source releases inside and outside an unrelated parent Git +- [x] Verify source releases inside and outside an unrelated parent Git repository. - [ ] Inspect both complete indexes under the staged-code quality gate. -- [ ] Re-run the highest-risk wrapper, harness, tailoring, and release gates. +- [x] Re-run the highest-risk wrapper, harness, tailoring, and release gates. - [ ] Prepare one functional ownership batch per repository and only an unavoidable later compatibility-pin batch. - [ ] Stop before pushes or final v2.0.0 tags unless explicitly authorized. +### Stage 5 local execution evidence + +- Candidate-independent TestField: + - normal configure/build/CTest passed `5/5` and its cache contains neither + `TEMPLATE_PROJECT_SOURCE_DIR` nor + `ENABLE_TEMPLATE_PROJECT_BUILD_TESTS`; + - candidate-independent harness/workflow pytest passed `18/18`; + - the standalone TestField acceptance union passed `14/14`, containing + `11` CPU, `2` docs, and `1` CUDA entry. +- Explicit candidate harness: + - CPU passed `24/24`, including shared/static installed consumers, Python + 3.12 install/import, Python 3.11 rejection, MATLAB, packaging, wrapper, + tailoring, release, workflow, devcontainer, and cross-compilation + contracts; + - docs passed `2/2`; + - CUDA/OptiX passed `13/13` with CUDA architecture `120` and the explicit + OptiX SDK; + - ROS 2 static/metadata passed `2/2`; + - exact profile inventories remain disjoint and their `all` union contains + `41` tests. +- Template runtime: + - the refreshed CPU build passed `7/7`; + - the refreshed CUDA/OptiX build passed `9/9`, including the initialization + gate and device-memory round trip; + - a clean ROS 2 Jazzy overlay built all four packages and reported `10` + tests, `0` errors, `0` failures, and `0` skips. +- High-risk reruns: + - the complete CPU harness reran wrapper packaging, runtime staging, + build-helper wrapper modes, tailoring idempotence, release metadata/source + archives, and installed consumers successfully; + - the release verifier passed once with its binary root inside TestField's + unrelated parent Git checkout and again from + `/tmp/cpp_cuda_template_v2_release_outside_git_final`. +- Static and repository hygiene: + - every current shell file passed `bash -n`; + - every changed or new shell file passed ShellCheck; + - all current Python files byte-compiled; + - `8` YAML and `4` XML files in each repository parsed successfully; + - whitespace, exact conflict-marker, generated-manifest, and CMake 3.15 + forbidden-API checks passed in both repositories; + - all six changed/new TestField Python modules have module, class, and + callable documentation. + ## Discrepancy and issue ledger Entries remain in this ledger after resolution. @@ -405,7 +1219,8 @@ Entries remain in this ledger after resolution. - Observed: its staged wrapper port and combined document still target v1.12.0 and the superseded configure-time analyzer. - Action: reconcile, replace, and retest only after the Stage 1 release gate. -- Status: open. +- Status: resolved. The TestField candidate now targets exact template + `v1.12.1` and uses the released build-time resolved-artifact staging design. ### ISSUE-006 - Remote evidence does not cover the index @@ -415,7 +1230,9 @@ Entries remain in this ledger after resolution. - Observed: current successful GitHub checks cover only committed v1.12.0. - Action: run fresh local acceptance against the complete staged candidate and report remote CI as pending until a push is authorized. -- Status: open. +- Status: resolved for the template release. The pushed v1.12.1 branch/tag + checks completed successfully; the unpushed TestField candidate is covered by + its fresh local Stage 2 matrix. ### ISSUE-007 - Archive cache scan races parallel configure tests @@ -454,6 +1271,210 @@ Entries remain in this ledger after resolution. - Status: open as a conditional environment limitation; no staged construct was found outside the documented 3.15 API surface. +### ISSUE-009 - TestField library ignored static selection + +- Stage: 2 +- Severity: blocking +- Expected: `BUILD_SHARED_LIBS=OFF` produces an installed static library usable + by a downstream consumer. +- Observed: TestField hardcoded `add_library(... SHARED ...)`, even though its + documentation advertised both modes. +- Action: declare the standard option with the existing shared default, select + the target kind explicitly, and run both installed-consumer modes. +- Status: resolved. Shared and static disposable consumers both passed. + +### ISSUE-010 - Explicit relative gtwrap root in released template + +- Stage: 2, deferred template follow-up +- Severity: worthwhile +- Expected: a valid explicit `--gtwrap-root lib/wrap` is resolved relative to + the source checkout before generated commands use it. +- Observed: the released common resolver preserves the relative cache spelling, + so generated commands may reinterpret it relative to the binary tree. +- Action: TestField canonicalizes the explicit root at its common resolver + boundary. Apply and verify the same small correction when template + development resumes after the v1.12.1 release baseline. +- Status: resolved in both repositories. The Stage 4 configure fixture proves + the template resolver returns the canonical source-absolute path. + +### ISSUE-011 - TestField wrapper target-help pipeline was generator-sensitive + +- Stage: 2 +- Severity: worthwhile +- Expected: the shell helper confirms the actual wrapper target without false + warnings. +- Observed: under `set -o pipefail`, `rg -q` could close the target-help pipe + after its first match and make the upstream CMake process fail with a broken + pipe. +- Action: read the wrapper target name published in `CMakeCache.txt` instead of + parsing generator-specific help output. +- Status: resolved. The real relative-root wrapper build completed without the + false warning. + +### ISSUE-012 - Fallback Python metadata retains the obsolete interpreter floor + +- Stage: 2, deferred template follow-up +- Severity: worthwhile +- Expected: the missing-`pyproject.toml.in` fallback preserves the project-wide + Python 3.12 minimum. +- Observed: released `HandlePythonWrapper.cmake` writes + `requires-python = ">=3.8"` while normal template and TestField metadata + require `>=3.12`. +- Action: TestField aligns the fallback with 3.12 now. Apply the same literal + correction to the template in its next maintenance or v2 reduction batch. +- Status: resolved in both repositories. The template fallback now requires + Python 3.12 and the TestField wrapper preflight enforces that contract. + +### ISSUE-013 - TestField root target-option setup is duplicated + +- Stage: 2, deferred v2 cleanup +- Severity: cosmetic +- Expected: one assignment block owns each target and namespaced option name. +- Observed: TestField's pre-existing root CMake file repeats + `LIB_TARGET_NAME`, `BUILD_PROGRAMS_OPTION_NAME`, and + `BUILD_EXAMPLES_OPTION_NAME` setup verbatim. +- Action: remove the duplicate with the Stage 3 root-CMake harness migration, + where the adjacent transitional external-test facade is already changing. +- Status: resolved in Stage 3; the repeated assignment block was removed with + the adjacent facade. + +### ISSUE-014 - TestField Doxygen discovery traverses generated builds + +- Stage: 2, deferred v2 cleanup +- Severity: worthwhile +- Expected: Doxygen discovery remains limited to owned source and documentation + inputs. +- Observed: the successful documentation target traversed ignored CPU, release, + wrapper, and MATLAB fixture build products, creating excessive output and + unnecessary work. +- Action: narrow documentation discovery during Stage 5 integrated v2 + validation and prove generated products remain excluded. +- Status: resolved in Stage 3; the strict verifier now rejects traversal of + both `lib/` and the active binary tree. + +### ISSUE-015 - Transitional conformance is not fully TestField-owned + +- Stage: 3 +- Severity: blocking +- Expected: every standalone candidate-template assertion executes an + implementation versioned by TestField. +- Observed: docs, nested-docs, version-side-effect, and tailoring entries invoke + scripts from `TEMPLATE_PROJECT_SOURCE_DIR`; build-mode, CUDA-architecture, + OptiX-preflight, build-helper-wrapper, and positive OptiX entries also have + incomplete labels. +- Action: migrate one TestField-owned copy of every verifier, deduplicate the + four reused entries in the standalone registry, and assign consistent + harness/profile/contract labels while preserving timeouts and resource locks + through parity. +- Status: resolved in Stage 3; all `41` candidate entries execute + TestField-owned implementations with normalized profile and contract + properties. + +### ISSUE-016 - TestField advertises an untracked template submodule + +- Stage: 3 +- Severity: worthwhile +- Expected: dependency declarations describe paths represented by Git links or + by explicit workflow checkouts. +- Observed: `.gitmodules` contains `lib/cpp_cuda_template_project` and an old + branch hint, but the index has no gitlink at that path. CI independently + checks out the candidate into a workspace sibling without an exact ref. +- Action: remove the stale submodule stanza and make the standalone workflow + checkout an explicit full commit SHA. +- Status: resolved in Stage 3; the stale stanza is removed and each CI + candidate checkout uses the full v1.12.1 commit SHA. + +### ISSUE-017 - A local TestField option test depends on the external facade + +- Stage: 3 +- Severity: blocking +- Expected: TestField-local CTest configures and runs without any candidate + template path. +- Observed: `VerifyTestfieldPythonTestOptions.cmake` receives + `TEST_TEMPLATE_SOURCE_DIR` and asserts that + `ENABLE_TEMPLATE_PROJECT_BUILD_TESTS=ON` registers tests even when + `ENABLE_TESTS=OFF`. +- Action: remove the external-only case and candidate input; retain only the + local Python-runner/conda option contracts after the facade is deleted. +- Status: resolved in Stage 3; the local verifier has no candidate input or + external-only case. + +### ISSUE-018 - Recursive TestField acceptance remains in ordinary CTest + +- Stage: 3 and 5 +- Severity: worthwhile +- Expected: normal derived-project CTest contains runtime Catch2/pytest behavior, + while fresh configure/build/install/consumer acceptance is invoked explicitly + by local CI. +- Observed: TestField currently registers flags, docs, nested install, + release, cross, CUDA-source, and related self-reconfigure scripts in its + ordinary CTest graph. +- Action: do not mix this cleanup with candidate-harness parity. Before the + Stage 5 handoff, relocate the still-unique TestField acceptance invocations + to an explicit out-of-tree CI-owned entry point and leave normal CTest with + runtime behavior only. +- Status: resolved after candidate parity; the explicit TestField acceptance + project owns `14` entries and normal CTest is project-runtime-only. + +### ISSUE-019 - Dormant workflow copies preserve tailoring-only complexity + +- Stage: 4 +- Severity: worthwhile +- Expected: moving template-system conformance to TestField reduces the + template and the effort required to tailor it. +- Observed: retaining active template-validation workflows beside dormant + generic `.yml.tpl` copies would require pair validation, marker checks, + materialization, and additive-ROS special handling after their conformance + jobs had moved. +- Action: promote the generic workflows as the template's only active + definitions, remove all dormant copies and materialization code, and keep + their semantic contracts in TestField. +- Status: resolved. The template and derived projects share four direct + workflows; `--remove-ros2` deletes only the ROS workflow. + +### ISSUE-020 - Repository-wide ShellCheck includes unrelated legacy warnings + +- Stage: 5 +- Severity: cosmetic +- Expected: distinguish migration regressions from pre-existing shell-helper + debt during the final hygiene gate. +- Observed: an all-file ShellCheck invocation reports existing warnings in + unchanged devcontainer, VS Code, and profiling helpers. +- Action: require `bash -n` for every current shell file and ShellCheck every + changed or new shell file. Do not expand the v2 ownership migration into + unrelated legacy cleanup. +- Status: open as non-blocking pre-existing maintenance debt; no changed or new + shell file has a ShellCheck finding. + +### ISSUE-021 - Migrated workflow verifier retained dormant-template naming + +- Stage: 5 +- Severity: cosmetic +- Expected: TestField terminology describes the direct reusable workflows + introduced by the v2 architecture. +- Observed: the migrated live parser module and test class still used + `testWorkflowTemplates` even though dormant `.yml.tpl` files no longer exist. +- Action: rename the live module and class to + `testTemplateProjectWorkflows` while retaining the old path only in the + tailoring verifier's forbidden-legacy inventory. +- Status: resolved; direct pytest passed `11/11` and the stable harness CTest + entry passed. + +### ISSUE-022 - Integrated-test residue remained in TestField review surfaces + +- Stage: 5 +- Severity: worthwhile +- Expected: ordinary TestField CTest and native CI express only their current + runtime responsibilities. +- Observed: the reduced `tests/CMakeLists.txt` retained obsolete scaffolding, + the native workflow repeated a path already covered by `tests/**`, and native + test jobs still required Doxygen/Graphviz after docs acceptance moved out. +- Action: collapse the runtime test registry, deduplicate the trigger, keep + PyYAML as the real parser prerequisite, and leave documentation tools with + the docs profile. +- Status: resolved; workflow pytest passed `16/16` and normal TestField CTest + passed `5/5`. + ## Final Status Review and Report ### Stage 1 interim release gate - 2026-07-28 @@ -509,5 +1530,97 @@ Entries remain in this ledger after resolution. proposed title and bullets, create the annotated `v1.12.1` tag, and then authorize Stage 2 TestField alignment. -The final integrated v2.0.0 report remains pending until Stages 2 through 5 -complete. +### Stage 2 published TestField release gate - 2026-07-28 + +- State: + - TestField branch `main`; + - signed commit `f632290ce1bfb1f80baeeb3da2ea6db28a998037`; + - signed annotated tag `v1.12.1` dereferences to that exact commit; + - the final commit contains `19` related files with `1,878` insertions and + `536` deletions; + - TestField has no tracked or staged change and matches `origin/main`; + - remote branch `main` and remote tag `v1.12.1^{}` both resolve to the signed + release commit. +- Implemented behavior: + - the 1,332-line candidate facade is replaced by a 579-line common + coordinator plus focused Python, MATLAB, and runtime-staging modules; + - Python runtime and wrapper filenames are resolved for the active + configuration and validated as one namespace before copying; + - clean paths, source archives, wheels, and CMake installs enforce their + ownership boundaries; + - relative explicit gtwrap roots are canonicalized; + - TestField now honors shared and static library modes; + - all four ROS 2 manifests contain `1.12.1`. +- Validation: + - fresh CPU passed `40/40`; + - CUDA 12.9.41 with `sm_120` passed `41/41`; + - the explicit OptiX build passed `17/17`; + - ROS 2 Jazzy built four packages and reported `10` tests, `0` errors, + `0` failures, and `0` skips; + - real wrapper, wheel, isolated pip import, CMake-prefix import, shared/static + consumer, cleanup, source-release, Doxygen, shell, Python, YAML, XML, + whitespace, conflict-marker, and CMake-floor API checks passed; + - exact-tag metadata regeneration derived `1.12.1` and left tracked content + clean; + - branch/tag native CI passed `35/35` applicable hosted-runner tests per run; + - branch/tag ROS 2 CI each passed `10` tests with no errors, failures, or + skips; + - documentation CI passed; CUDA remained the expected policy skip. +- Staged-code review: + - all substantially modified modules have file/callable documentation and + purpose-oriented block comments; + - the fallback Python floor was corrected from 3.8 to 3.12; + - the package entrypoint gained its module contract and runnable example; + - inaccurate “atomic” wording was replaced with “serialized staging.” +- Open or deferred: + - ISSUE-008 records the unavailable exact local CMake 3.15 runtime; + - ISSUE-010 and ISSUE-012 are small template follow-ups after v1.12.1; + - ISSUE-013 and ISSUE-014 remain non-blocking v2 TestField cleanup; + - the remote CUDA skip remains expected while self-hosted CI is disabled. +- Readiness: Stage 2 is complete and published. The signed TestField commit and + tag are remotely available and all enabled CI paths are green. Stop here + until review or authorization to begin Stage 3. + +### Integrated v2 local release-preparation gate - 2026-07-28 + +- Ownership outcome: + - the template keeps only runtime tests and fixtures inherited by tailored + projects; + - TestField owns all generic candidate conformance through the explicit + standalone harness; + - TestField's ordinary build is candidate-independent and its own expensive + acceptance is a separate CI-owned project; + - tailoring no longer reconstructs CMake files or materializes workflows. +- Reduction: + - the template batch removes approximately `6,700` lines while retaining + production wrapper, CUDA, ROS, packaging, and tailoring behavior; + - dormant workflow copies, pair validation, workflow materialization, and + template self-conformance registrations are gone; + - TestField contains one copy of every external verifier and stable + CPU/docs/CUDA/ROS profile registries. +- Validation: + - TestField runtime passed `5/5`, TestField acceptance passed `14/14`, and + candidate conformance passed `24/24` CPU, `2/2` docs, `13/13` CUDA/OptiX, + and `2/2` ROS 2; + - template runtime passed `7/7` CPU and `9/9` CUDA/OptiX; + - the clean Jazzy overlay passed all `10` reported tests; + - source release, wrapper packaging, install/consumer, tailoring + idempotence, shell, Python, YAML, XML, whitespace, Git, and CMake-floor + gates passed. +- Maintainability: + - root/test CMake and project workflows are stable across tailoring; + - new harness modules have explicit file ownership, callable documentation, + purpose-oriented comments, and stable named inventories; + - live workflow terminology reflects direct project workflows rather than + removed dormant templates; + - design, execution evidence, discrepancies, and this report remain in this + one tracker. +- Remaining gates: + - inspect the complete staged index in both repositories; + - create one related functional commit in the template; + - pin TestField workflows to that exact template commit, then create one + related TestField commit; + - push the authorized branches and inspect remote CI; + - do not create final `v2.0.0` tags without explicit user authorization. +- Known limitation: no CMake 3.15 executable is installed locally. Static API + checks passed; the exact minimum-version runtime remains a remote CI gate. diff --git a/doc/documentation_workflow.md b/doc/documentation_workflow.md index 3935db0..b2db096 100644 --- a/doc/documentation_workflow.md +++ b/doc/documentation_workflow.md @@ -65,17 +65,10 @@ The generated Doxyfile excludes `lib/`, `doc/developments/`, build directories, ## GitHub Pages -`.github/workflows/docs_pages.yml` builds the Doxygen HTML site and uploads `build_docs/doc/html` as a Pages artifact. - -Before configuring Doxygen, the active template workflow runs the owned -parser-backed workflow contract directly: - -```bash -python3 -m pytest -q tests/template_test/testWorkflowTemplates.py -``` - -Changes to that test are included in the workflow path filters. The dormant -generic docs workflow intentionally does not inherit this template-only check. +`.github/workflows/docs_pages.yml` builds the Doxygen HTML site and uploads +`build_docs/doc/html` as a Pages artifact. The same workflow is inherited by a +tailored project; generic workflow structure and template documentation +conformance are tested externally by `cpp_cuda_template_testfield`. Pull requests build and upload the artifact for inspection but do not deploy. Manual `workflow_dispatch` runs are build-only by default; set `deploy_pages=true` to publish intentionally. Default-branch pushes deploy to the `github-pages` environment. @@ -91,10 +84,13 @@ Settings > Pages > Build and deployment > Source: GitHub Actions ## Verification -Run the docs CTest gates before publishing: +Build the documentation from a fresh preset before publishing: ```bash -ctest --test-dir build --output-on-failure -R "docs|pages|issue_templates|version" +cmake --preset docs +cmake --build --preset docs +test -f build_docs/doc/html/index.html +test -d build_docs/doc/xml ``` After a Pages deployment, check: diff --git a/doc/ros2_overlay.md b/doc/ros2_overlay.md index 568b902..b9dbea0 100644 --- a/doc/ros2_overlay.md +++ b/doc/ros2_overlay.md @@ -10,8 +10,7 @@ ROS integration lives in `ros2/` plus the root overlay helpers: - `add_ros2_support.sh` - the four root `COLCON_IGNORE` markers - `.github/workflows/build_ros2_overlay.yml` -- `.github/workflows/build_ros2_overlay.yml.tpl` -- this documentation and the template-development checks +- this documentation and the ROS package runtime tests There is no root `package.xml` and no `ENABLE_ROS2` CMake option. The shim package at `ros2/template_project/` is the only package that includes the core library. Its `CMakeLists.txt` preloads the real root `cmake/` directory, then calls `add_subdirectory()` on the repository root so the usual install/export rules publish `template_project::template_project` into the colcon install prefix. @@ -174,13 +173,11 @@ Use `add_ros2_support.sh` from this template checkout when a derived repository The rollout script is purely additive. It refuses targets that already have `ros2/` or `build_ros2.sh`, copies the overlay files, renames copied ROS package paths and copied file contents from `template_project` to a ROS package prefix, and leaves existing target files untouched. -For CI, rollout reads the dormant generic -`.github/workflows/build_ros2_overlay.yml.tpl` and writes it to the target as -the runnable `.github/workflows/build_ros2_overlay.yml`. It does not copy the -active template-validation workflow, which contains checks for this template's -rollout machinery and placeholder implementation. The rollout helper requires -the generic ownership marker before copying, so a misplaced active workflow -cannot be delivered under the `.tpl` filename. +For CI, rollout copies the reusable +`.github/workflows/build_ros2_overlay.yml` directly into the target. The source +repository and derived projects therefore execute the same workflow definition. +Broader rollout and static-overlay conformance remains in the external +`cpp_cuda_template_testfield` harness. By default, the ROS package prefix is derived from the target CMake package name in `set(project_name "...")`. If the CMake package name is already ROS-valid, the two names match. If the CMake package name is not ROS-valid, the script keeps core CMake references pointed at the original CMake package name while using a ROS-valid package prefix for ROS package names. For example, a target CMake package named `space-nav-frontend` keeps this core CMake shape: @@ -237,32 +234,23 @@ when the supported overlay helper is absent. ## CI -The active `.github/workflows/build_ros2_overlay.yml` is owned by this template -repository and runs the overlay in the `ros:jazzy` container. It has two jobs: - -- `overlay-build`: installs dependencies, synchronizes project metadata, rejects tracked manifest drift, runs `rosdep install --from-paths ros2 -i -r -y --rosdistro jazzy`, builds/tests the overlay, then runs the static pytest. -- `rollout-rehearsal`: makes a full-history clone of the exact CI revision, performs the same pre-`rosdep` metadata sync and drift check, strips the overlay from the clone, re-adds it with `add_ros2_support.sh --verify`, builds the overlay, and checks a plain standalone CMake build. - -Both active jobs require an executable helper and exercise -`./generate_version.sh --sync-ros2` directly; an unsupported invocation is a CI -error. Each successful synchronization is followed by: +The reusable `.github/workflows/build_ros2_overlay.yml` runs one +`overlay-build` job in the `ros:jazzy` container. It installs dependencies, +uses the metadata helper when the checkout advertises full synchronization, +runs `rosdep install --from-paths ros2 -i -r -y --rosdistro jazzy`, then builds +and tests the overlay. Each supported synchronization is followed by: ```bash git diff --exit-code -- ros2/*/package.xml ``` -The static overlay verifier derives its strict `EXPECTED_VERSION` from the -generated `VERSION` file, independently of the manifests being checked. Native -and ROS workflows run for `v*.*.*` tag pushes as well as their branch events. - -The dormant `.github/workflows/build_ros2_overlay.yml.tpl` is the generic -single-project workflow delivered by tailoring or additive rollout. It watches -the derived project's source and overlay paths, synchronizes metadata, installs -ROS dependencies, and runs `./build_ros2.sh --clean --no-version-sync`; it does -not contain template-only static or rollout checks. For compatibility, it warns -and continues with existing manifests when an older derived project lacks the -full metadata marker; when synchronization is supported, manifest drift is a -hard failure. +The workflow watches the project source and overlay paths and runs for +`v*.*.*` tag pushes as well as its branch events. It warns and continues with +existing manifests when an older derived project lacks the full metadata +marker; when synchronization is supported, manifest drift is a hard failure. + +TestField separately owns static manifest checks, additive rollout fixtures, +identifier-boundary renaming, and default/ROS-removed tailoring conformance. CUDA+ROS is local-only in this repository. The available self-hosted GPU runner does not provide the ROS environment, so CI intentionally avoids `build_ros2.sh --cuda`. diff --git a/doc/template_usage.md b/doc/template_usage.md index 678075c..311d436 100644 --- a/doc/template_usage.md +++ b/doc/template_usage.md @@ -18,9 +18,19 @@ Use this order for a new library checkout: Replace `my_project` with the chosen C++ project namespace. Add `--keep-profiling` only when the new project should keep the optional Valgrind/perf helper scripts. 3. Rename the template identifiers in tracked source files only. Exclude build trees, install trees, virtual environments, generated Python build metadata, and other generated artifacts. After cleanup succeeds, either delete `tailor_template_cleanup.sh` or exclude it from the rename pass; it is a one-shot template helper. -4. Remove optional skeletons that the project will not use. For example, if the CUDA module directory is deleted, also remove the matching `add_subdirectory()` entry from `src/CMakeLists.txt`. -5. Configure, build, and run CTest from a clean build directory. -6. Inspect remaining template names with `rg "template_project|template_src|template_src_kernels|cpp_playground"` and keep only intentional references in examples or documentation. +4. Decide which optional features the project will continue to support, + including CUDA, OptiX, TensorRT, TBB, and OpenGL. Retained features remain + dependency-neutral while their options are `OFF`. Keep `FindTensorRT.cmake` + and `HandleTensorRT.cmake` when TensorRT should remain available; removing + TensorRT entirely requires a deliberate review of root, source, package, + test, and documentation references rather than a cleanup-script flag. +5. Remove optional skeletons that the project will not use. For example, if the + CUDA module directory is deleted, also remove the matching + `add_subdirectory()` entry from `src/CMakeLists.txt`. +6. Configure, build, and run CTest from a clean build directory. +7. Inspect remaining template names with + `rg "template_project|template_src|template_src_kernels|cpp_playground"` and + keep only intentional references in examples or documentation. The cleanup script contains template-specific filenames and test names, so running it before a global `template_project` replacement avoids stale cleanup paths. @@ -129,17 +139,19 @@ set(LIB_TARGET_NAME_OVERRIDE nested_my_project_library CACHE STRING "" FORCE) set(my_project_METADATA_ONLY OFF CACHE BOOL "" FORCE) set(my_project_ENABLE_CUDA OFF CACHE BOOL "" FORCE) set(my_project_ENABLE_OPTIX OFF CACHE BOOL "" FORCE) +set(my_project_ENABLE_TENSORRT OFF CACHE BOOL "" FORCE) add_subdirectory(path/to/my_project) target_link_libraries(parent_target PRIVATE nested_my_project::my_project) ``` -The project-qualified metadata, CUDA, and OptiX options are canonical for -`add_subdirectory()` consumers and cannot collide with an application's generic -cache entries. The historical `PROJECT_METADATA_ONLY`, `ENABLE_CUDA`, and -`ENABLE_OPTIX` spellings remain one-config, top-level compatibility aliases -only. When supplied, a legacy alias wins for that configure, migrates its value -to the canonical project-qualified option, and is removed from the cache. -Replace `my_project` with the renamed root `project_name`. +The project-qualified metadata, CUDA, OptiX, and TensorRT options are canonical +for `add_subdirectory()` consumers and cannot collide with an application's +generic cache entries. The historical `PROJECT_METADATA_ONLY`, `ENABLE_CUDA`, +`ENABLE_OPTIX`, and `ENABLE_TENSORRT` spellings remain one-config, top-level +compatibility aliases only. When supplied, a legacy alias wins for that +configure, migrates its value to the canonical project-qualified option, and is +removed from the cache. Replace `my_project` with the renamed root +`project_name`. Only the main project configures documentation, tests, examples, wrappers, and generic `doc` targets. Nested projects keep their library target available without publishing documentation for the parent build. @@ -149,10 +161,12 @@ Use Catch2 for compiled tests, pytest for Python tests, and CTest as the common runner. Put compiled tests in `test*.cpp` or `test*.cu` files and Python tests in `test*.py` files. -The template repository uses `tests/cmake/` for template-owned conformance, -tailoring, and generation checks. Do not copy those verifiers into a tailored -project. A derived project should prove configuration, feature matrices, -installation, packaging, and external consumption with explicit fresh +The template checkout contains only runtime tests and fixtures that a derived +project should inherit. Generic tailoring, workflow, packaging, installation, +and platform conformance is owned by the standalone harness in +`cpp_cuda_template_testfield`; those verifier implementations are not part of +this source tree. A derived project should prove its own configuration, feature +matrices, installation, packaging, and external consumption with explicit fresh out-of-tree commands in its acceptance/CI matrix. Do not make ordinary CTest recursively configure and rebuild the same project when CI already owns that behavioral gate. @@ -224,44 +238,26 @@ By default this also removes `profiling/`. Keep those scripts only when the new ``` The script replaces `template_project::logging` with the required project -namespace, then removes agent/context notes, internal development notes, -workflow snapshot files, template-specific validation CTest scripts, optional -profiling scripts, and the workspace file tied to this template checkout. It -keeps reusable project infrastructure such as `cmake/` (including the -Python/MATLAB wrapper and runtime-staging modules), `build_lib.sh`, docs -workflow files, issue forms, examples, toolchains, starter unit tests, -`.devcontainer/`, and `.vscode/`. - -It also removes the root CMake hook for the template MATLAB regression helper and rewrites `tests/CMakeLists.txt` so only starter project unit tests remain registered. - -This cleanup boundary is intentional: template CMake conformance tests remain -owned by the donor/testfield validation harness, not by the tailored product. - -### Workflow materialization - -The runnable `.github/workflows/*.yml` files in this repository validate the -template itself. Generic workflows for a tailored project are stored beside -them as dormant `.tpl` files so GitHub does not execute both definitions: - -| Dormant project workflow | Materialized tailored workflow | -|---|---| -| `build_linux.yml.tpl` | `build_linux.yml` | -| `build_linux_cuda.yml.tpl` | `build_linux_cuda.yml` | -| `docs_pages.yml.tpl` | `docs_pages.yml` | -| `build_ros2_overlay.yml.tpl` | `build_ros2_overlay.yml` | - -Normal cleanup validates that each active/dormant pair exists, atomically -replaces each active template-validation workflow with its generic project -workflow, and removes every `.tpl` file. The resulting checkout therefore has -only runnable project CI and no dormant workflow templates. - -Each generic workflow carries the `# project-ci-template: generic` ownership -marker. Cleanup preserves that marker and the source file mode, allowing the -same cleanup mode to be reapplied safely while still rejecting an active -template-validation workflow whose matching `.tpl` was lost before -materialization. - -With `--remove-ros2`, cleanup materializes the three non-ROS workflows and -removes both forms of the ROS workflow. Without that flag, all four project -workflows are materialized. Make project-specific runner, dependency, and -deployment changes in the resulting `.yml` files after cleanup. +namespace, then removes agent/context notes, internal development and review +records, optional profiling scripts, and the workspace file tied to this +template checkout. It keeps reusable project infrastructure such as `cmake/` +(including the Python/MATLAB wrapper and runtime-staging modules), +`build_lib.sh`, issue forms, examples, toolchains, starter runtime tests, +MATLAB wrapper checks, `.devcontainer/`, and `.vscode/`. + +Root `CMakeLists.txt`, `tests/CMakeLists.txt`, custom downstream tests, and the +non-ROS workflows are stable inputs: cleanup does not parse or rewrite them. +This boundary is intentional. Template-system conformance remains external in +TestField, while project-owned runtime checks remain with the project. + +### Project workflows + +The four runnable `.github/workflows/*.yml` files are directly reusable by a +derived project. There are no dormant workflow copies and no materialization +step. Normal cleanup preserves all four files byte-for-byte. + +With `--remove-ros2`, cleanup removes only +`.github/workflows/build_ros2_overlay.yml` along with the optional overlay. +Without that flag, the workflow remains available. Make project-specific +runner, dependency, and deployment changes directly in the `.yml` files after +cleanup. diff --git a/doc/testing_and_ci.md b/doc/testing_and_ci.md index a1dab9e..160324d 100644 --- a/doc/testing_and_ci.md +++ b/doc/testing_and_ci.md @@ -2,7 +2,7 @@ ## Local Gates -Use CTest for compiled tests, Python tests, and workflow-level regressions. +Use CTest for compiled and Python runtime tests. `ctest --test-dir ` is the preferred form because it works from the repository root, from scripts, and from CI jobs without changing directories: @@ -18,12 +18,18 @@ normal CTest entries that execute `python -m pytest -q `. ### Template conformance versus derived-project acceptance -The template's default CTest suite includes conformance checks for generic -tailoring, generated files, workflows, and package behavior. Those checks are -template-maintainer infrastructure and are removed by normal tailoring. -Production modules exercised by those checks remain in the tailored project; -for example, wrapper packaging verification is removed while the -`Handle*Wrapper.cmake` and runtime-staging modules it validates are retained. +The template's default CTest suite is already the derived-project suite. It +contains inherited C++ logger/starter behavior, Python import smoke coverage, +optional CUDA initialization/placeholder behavior, reusable fixtures, and +target-owned MATLAB wrapper checks. Tailoring does not rewrite its +registrations. + +Generic template-system conformance is implemented by the standalone harness in +`cpp_cuda_template_testfield`. That harness receives an explicit candidate +source path and owns tailoring, workflows, release metadata, packaging, +installation, consumer, nested-build, cross-compilation, CUDA/OptiX, wrapper, +and static ROS contracts. Its verifier implementations are not copied into this +repository or a derived project. Do not reproduce them as recursive CMake tests in a derived project. In particular, ordinary derived-project CTest must not configure and rebuild the @@ -109,44 +115,30 @@ To disable Python tests while keeping Catch2 tests: cmake -S . -B build -DENABLE_TESTS=ON -DENABLE_PYTHON_TESTS=OFF ``` -Focused documentation checks: +Focused documentation build: ```bash -ctest --test-dir build --output-on-failure -R "docs|pages|issue_templates|version" +cmake --preset docs +cmake --build --preset docs ``` ## CI Workflows -Template-validation workflows are the active `.github/workflows/*.yml` files in -this repository. They verify template-owned contracts such as cleanup, -rollout, static CMake checks, and fixture builds; they are not the workflows -delivered unchanged to a derived project. - -Derived-project workflow templates are stored as dormant matching -`.github/workflows/*.yml.tpl` files. `tailor_template_cleanup.sh` materializes -them as the runnable `.yml` files and removes the `.tpl` sources. The -`testWorkflowTemplates.py` contract parses every active/dormant pair, -validates trigger, job, checkout, and action structure, and executes the ROS -metadata synchronization blocks in temporary Git repositories. This proves -clean synchronization and dirty-manifest rejection without duplicating shell -command spelling in a text scanner. -Executable workflow roles use stable `id` fields, so display names may be -reworded without breaking the contract. - -Dormant workflow templates must not rely on parse-only coverage. The active -Linux `tailored-project-validation` job applies cleanup in a full-history scratch -clone of the exact CI revision, -parses the materialized workflows, builds/tests the tailored C++ fixture, and -builds its docs. The active ROS workflow separately removes and re-adds the -overlay in scratch, materializes the generic ROS workflow, and exercises the -resulting ROS and standalone builds. The active CUDA workflow runs the common -workflow-template contract, materializes the project in both jobs, and then -builds/tests that tailored source tree on the GPU runner. +The active `.github/workflows/*.yml` files are reusable project workflows and +survive normal tailoring unchanged. There are no dormant `.tpl` copies. Native +CPU, CUDA, ROS, and Pages workflows therefore exercise the same definitions +that a derived project receives. + +Template-system workflow structure and behavior is checked from TestField +against an explicitly selected candidate. The parser-backed contract validates +triggers, job topology, checkout depth, shell syntax, current Pages actions, +ROS step ordering, and metadata drift behavior without adding those checks to +ordinary project CTest. The Linux workflows keep CPU tuning portable because build artifacts are tested in a separate job. Do not re-enable `CPU_ENABLE_NATIVE_TUNING=ON` in GitHub Actions unless build and test run on the same pinned CPU family. -The active and generic native CPU, CUDA, and ROS workflows also run for -`v*.*.*` tag pushes. Their existing `paths` filters continue to scope branch +The native CPU, CUDA, and ROS workflows also run for `v*.*.*` tag pushes. +Their existing `paths` filters continue to scope branch pushes and pull requests; GitHub does not evaluate path filters for tag pushes, so a release tag still executes the release-relevant build gates. See [GitHub workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onpushpull_requestpull_request_targetpathspaths-ignore). @@ -157,46 +149,37 @@ while such a runner is available. When the variable is unset or has any other value, both CUDA jobs are skipped before runner allocation, so release-tag and manual workflow runs do not remain queued indefinitely. -The active template ROS workflow executes -`./generate_version.sh --sync-ros2` and rejects any tracked manifest change -with: +The ROS workflow executes `./generate_version.sh --sync-ros2` when the helper +advertises full metadata synchronization, then rejects any tracked manifest +change with: ```bash git diff --exit-code -- ros2/*/package.xml ``` -The generic derived-project ROS workflow applies the same drift guard whenever -the helper supports full metadata synchronization. It emits a compatibility -warning instead of failing when an older derived project has not adopted that -capability yet. After the workflow-owned synchronization, CI passes +It emits a compatibility warning instead of failing when an older derived +project has not adopted that capability yet. After workflow-owned +synchronization, CI passes `--no-version-sync` to the build helper to avoid a second unguarded rewrite. -Template-validation Linux and ROS jobs install `python3-pytest` and -`python3-yaml`; PyYAML parses the active/dormant workflow pairs. Jobs that run -documentation CTests also install Doxygen and Graphviz. Self-hosted and CUDA -template workflows validate the same requirements with -`python3 -m pytest --version`, `python3 -c 'import yaml'`, -`command -v doxygen`, and `command -v dot` before configuring or running tests. -Cleanup removes the workflow-template -pytest, so generic tailored-project CI does not inherit the PyYAML dependency -unless the project adds its own YAML-backed tests. +Project workflows install only their runtime/build prerequisites. PyYAML and +the parser-backed contract belong to TestField rather than the delivered +project. Documentation jobs install Doxygen and Graphviz; CUDA jobs validate +their host tools before configuring. The Pages workflow is separate from the C++ build workflow. It has these stages: -1. Run the repository-owned parser-backed workflow contract. -2. Configure docs with CUDA, OptiX, and tests disabled. -3. Build Doxygen HTML and XML. -4. Verify `index.html` exists before upload. -5. Upload the Pages artifact. -6. Deploy only for default-branch pushes, or manual dispatch when `deploy_pages=true`. -7. Fetch the deployed Pages URL and check that the published index contains the expected documentation links. - -Repository tests prefer executable behavior or the native parser for YAML, -JSON, XML, and generated CMake metadata. They do not use regular-expression -matches against tracked implementation or documentation text. Exact text is -reserved for generated output whose representation is itself contractual, such -as tailoring markers, materialized workflow files, and preserved XML processing -instructions. +1. Configure docs with CUDA, OptiX, and tests disabled. +2. Build Doxygen HTML and XML. +3. Verify `index.html` exists before upload. +4. Upload the Pages artifact. +5. Deploy only for default-branch pushes, or manual dispatch when `deploy_pages=true`. +6. Fetch the deployed Pages URL and verify that it returns non-empty content. + +Tests prefer executable behavior or native parsers for YAML, JSON, XML, and +generated CMake metadata. Exact text is reserved for generated output whose +representation is itself contractual, such as synchronized metadata and +preserved XML processing instructions. ## Issue Templates diff --git a/doc/versioning.md b/doc/versioning.md index 160649f..667ecb9 100644 --- a/doc/versioning.md +++ b/doc/versioning.md @@ -40,10 +40,11 @@ the synchronization explicitly. Keeping source writes opt-in prevents CI and testfield configure runs from dirtying the checkout. -CPack stages the generated build-tree `VERSION` into binary and source -packages. The ignored source-tree file remains a fallback for configuring a -checkout without usable Git metadata, but it is excluded from CPack input so a -stale fallback cannot overwrite the version resolved for the package build. +Binary packages install the generated build-tree `VERSION`. Canonical source +packages instead include the source-tree `VERSION` prepared by +`generate_version.sh`. Do not configure a release package from a stale source +file or mutate release metadata between CMake configuration and CPack; prepare +the checkout again and reconfigure when metadata changes. ## C++ Access @@ -128,20 +129,25 @@ without Git tag context or that metadata is not a valid release input. The TGZ produced from `CPackSourceConfig.cmake` is the canonical source release. It is validated outside Git against the same strict core and full version as the -tagged checkout. CPack injects the generated build-tree `VERSION` and excludes -the ignored source-tree fallback, build trees, plus ROS-generated `build`, -`install`, and `log` outputs. GitHub's automatic source links are non-canonical: -they are repository snapshots and do not include the generated `VERSION` file -required by this release contract. Uploading the CPack TGZ to a GitHub release -remains a deliberate manual step; CI upload automation is not yet part of the -release workflow. - -Build-tree ownership is refreshed when CPack starts, not only when CMake first -configures the release tree. The active binary directory and any nested cache -whose `CMAKE_HOME_DIRECTORY` resolves to this exact checkout are excluded even -when they appeared after configuration. Build-prefixed source directories and -foreign child-project caches remain package input because their names alone do -not prove that this checkout generated them. +tagged checkout. CPack includes the prepared source-tree `VERSION` and excludes +the exact active binary directory plus deterministic generated paths such as +the root install prefix and ROS `build`, `install`, and `log` outputs. Prepare a +release from a checkout without additional generated build trees: CPack does not +scan arbitrary `CMakeCache.txt` files or infer ownership from path names. +Build-prefixed source directories and foreign child-project caches therefore +remain package input. + +The template appends its deterministic exclusions to +`CPACK_SOURCE_IGNORE_FILES` and leaves caller-owned extension hooks, including +`CPACK_PROJECT_CONFIG_FILE` and CPack install scripts, unchanged. Projects may +add local package policy through those public variables without the template +replacing it. + +GitHub's automatic source links are non-canonical: they are repository +snapshots and do not include the generated `VERSION` file required by this +release contract. Uploading the CPack TGZ to a GitHub release remains a +deliberate manual step; CI upload automation is not yet part of the release +workflow. Pushes of `v*.*.*` tags run the native CPU, CUDA, and ROS workflows. The ROS workflow regenerates metadata, derives the expected strict core version from diff --git a/ros2/template_project/package.xml b/ros2/template_project/package.xml index bfcabcc..5c7c901 100644 --- a/ros2/template_project/package.xml +++ b/ros2/template_project/package.xml @@ -2,7 +2,7 @@ template_project - 1.12.2 + 2.0.0 Reusable C++/CUDA library template with optional CUDA, OptiX, Python, MATLAB, and ROS 2 support: ROS 2 colcon shim package. Pietro Califano MIT diff --git a/ros2/template_project_interfaces/package.xml b/ros2/template_project_interfaces/package.xml index 92543cf..35d4a23 100644 --- a/ros2/template_project_interfaces/package.xml +++ b/ros2/template_project_interfaces/package.xml @@ -2,7 +2,7 @@ template_project_interfaces - 1.12.2 + 2.0.0 Reusable C++/CUDA library template with optional CUDA, OptiX, Python, MATLAB, and ROS 2 support: ROS 2 message and service interfaces. Pietro Califano MIT diff --git a/ros2/template_project_ros/package.xml b/ros2/template_project_ros/package.xml index 7aa0aef..978a0de 100644 --- a/ros2/template_project_ros/package.xml +++ b/ros2/template_project_ros/package.xml @@ -2,7 +2,7 @@ template_project_ros - 1.12.2 + 2.0.0 Reusable C++/CUDA library template with optional CUDA, OptiX, Python, MATLAB, and ROS 2 support: ROS 2 bridge package. Pietro Califano MIT diff --git a/ros2/template_project_spinup/package.xml b/ros2/template_project_spinup/package.xml index 2c6c123..de0ff09 100644 --- a/ros2/template_project_spinup/package.xml +++ b/ros2/template_project_spinup/package.xml @@ -2,7 +2,7 @@ template_project_spinup - 1.12.2 + 2.0.0 Reusable C++/CUDA library template with optional CUDA, OptiX, Python, MATLAB, and ROS 2 support: ROS 2 launch and runtime assets. Pietro Califano MIT diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 92f1a7e..234f724 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -122,6 +122,12 @@ if (ENABLE_CUDA) target_link_libraries(${lib_name} ${lib_link_scope} ${CUDA_COMPILE_TARGET}) endif() +# Link TensorRT through its project-owned interface so build-tree and installed +# consumers receive the same SDK targets and feature definition. +if(ENABLE_TENSORRT) + target_link_libraries(${lib_name} ${lib_link_scope} ${TENSORRT_COMPILE_TARGET}) +endif() + # Link OptiX interface target if(ENABLE_OPTIX) target_link_libraries(${lib_name} ${lib_link_scope} ${OPTIX_COMPILE_TARGET}) @@ -164,9 +170,12 @@ endif() if (ENABLE_CUDA) list(APPEND installable_targets ${CUDA_COMPILE_TARGET}) - if(ENABLE_OPTIX) - list(APPEND installable_targets ${OPTIX_COMPILE_TARGET}) - endif() +endif() +if(ENABLE_TENSORRT) + list(APPEND installable_targets ${TENSORRT_COMPILE_TARGET}) +endif() +if(ENABLE_OPTIX) + list(APPEND installable_targets ${OPTIX_COMPILE_TARGET}) endif() if (ENABLE_OPENGL) diff --git a/src/cmake/template_projectConfig.cmake.in b/src/cmake/template_projectConfig.cmake.in index df2cc0b..cb4d1b2 100644 --- a/src/cmake/template_projectConfig.cmake.in +++ b/src/cmake/template_projectConfig.cmake.in @@ -2,20 +2,46 @@ include(CMakeFindDependencyMacro) -# Make optional package-owned find modules available to derived ML libraries -# without enabling any additional dependency in the base template. -set(_template_project_saved_module_path "${CMAKE_MODULE_PATH}") -list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules") - # Automatically call find_dependency for each dependency set(_DEPENDENCIES @EXPORT_TARGET_DEPS@) foreach(dep ${_DEPENDENCIES}) - find_dependency(${dep} REQUIRED) + find_dependency(${dep}) endforeach() -set(CMAKE_MODULE_PATH "${_template_project_saved_module_path}") -unset(_template_project_saved_module_path) +# Resolve the optional installed TensorRT dependency without changing the +# consumer's module search policy. A QUIET outer lookup reports the package as +# unavailable; REQUIRED escalation remains the caller's responsibility. +if(@ENABLE_TENSORRT@) + set(_template_project_tensorrt_quietly_was_defined FALSE) + if(DEFINED TensorRT_FIND_QUIETLY) + set(_template_project_tensorrt_quietly_was_defined TRUE) + set(_template_project_saved_tensorrt_quietly "${TensorRT_FIND_QUIETLY}") + endif() + + set(TensorRT_FIND_QUIETLY TRUE) + set(_template_project_saved_package_name "${CMAKE_FIND_PACKAGE_NAME}") + set(CMAKE_FIND_PACKAGE_NAME TensorRT) + include("${CMAKE_CURRENT_LIST_DIR}/modules/FindTensorRT.cmake") + set(CMAKE_FIND_PACKAGE_NAME "${_template_project_saved_package_name}") + unset(_template_project_saved_package_name) + + if(_template_project_tensorrt_quietly_was_defined) + set(TensorRT_FIND_QUIETLY "${_template_project_saved_tensorrt_quietly}") + else() + unset(TensorRT_FIND_QUIETLY) + endif() + unset(_template_project_saved_tensorrt_quietly) + unset(_template_project_tensorrt_quietly_was_defined) + + if(NOT TARGET TensorRT::nvinfer OR NOT TARGET TensorRT::nvinfer_plugin) + set(${CMAKE_FIND_PACKAGE_NAME}_FOUND FALSE) + set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE + "The installed @PROJECT_NAME@ package was built with TensorRT, but " + "the TensorRT SDK was not found. Set TensorRT_ROOT or TENSORRT_ROOT.") + return() + endif() +endif() if(@ENABLE_OPTIX@) set(_template_project_optix_root_hints "") diff --git a/tailor_template_cleanup.sh b/tailor_template_cleanup.sh index 19a5e94..bb34242 100755 --- a/tailor_template_cleanup.sh +++ b/tailor_template_cleanup.sh @@ -38,11 +38,11 @@ Usage: Purpose: Remove files that are only useful while developing cpp_cuda_template_project - itself, then patch CMake references to removed template-validation tests. + itself while preserving reusable project files. Options: --list Print the cleanup list and exit. - --apply Remove files and patch CMake files. + --apply Remove template-owned files and tailor retained content. --yes Do not prompt before applying. --project-namespace Replace the template_project logger namespace. @@ -62,40 +62,12 @@ template_development_paths=( "cpp_cuda_template_project.code-workspace" "doc/developments" "doc/reports" - "tests/cmake/AddMatlabWrapperRegressionTests.cmake" - "tests/cmake/CheckTcmallocDependency.cmake" - "tests/cmake/VerifyTemplateProjectAddTestsProperties.cmake" - "tests/cmake/VerifyTemplateProjectBuildLibCleanSafety.cmake" - "tests/cmake/VerifyTemplateProjectBuildTreePackage.cmake" - "tests/cmake/VerifyTemplateProjectCrossCompile.cmake" - "tests/cmake/VerifyTemplateProjectCudaSources.cmake" - "tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake" - "tests/cmake/VerifyTemplateProjectNestedDocsIsolation.cmake" - "tests/cmake/VerifyTemplateProjectNoOptimization.cmake" - "tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake" - "tests/cmake/VerifyTemplateProjectOptimizedFlags.cmake" - "tests/cmake/VerifyTemplateProjectPythonPackaging.cmake" - "tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake" - "tests/cmake/VerifyTemplateProjectRos2Overlay.cmake" - "tests/cmake/VerifyTemplateProjectTailoringScript.cmake" - "tests/cmake/VerifyTemplateProjectVersionSideEffects.cmake" - "tests/template_test/testRos2OverlayStatic.py" - "tests/template_test/testWorkflowTemplates.py" - "tests/matlab/RunTemplateWrapperRegression.m" ) optional_paths=( "profiling" ) -project_workflow_names=( - "build_linux.yml" - "build_linux_cuda.yml" - "docs_pages.yml" - "build_ros2_overlay.yml" -) -project_workflow_marker="# project-ci-template: generic" - ros2_overlay_paths=( "ros2" "build_ros2.sh" @@ -105,9 +77,7 @@ ros2_overlay_paths=( "examples/COLCON_IGNORE" "tests/COLCON_IGNORE" ".github/workflows/build_ros2_overlay.yml" - ".github/workflows/build_ros2_overlay.yml.tpl" "doc/ros2_overlay.md" - "tests/template_test/testRos2OverlayStatic.py" ) ros2_overlay_doc_paths=( @@ -153,24 +123,21 @@ EOF fi cat <<'EOF' -CMake edits made by --apply: - - Remove the root CMake include/call for AddMatlabWrapperRegressionTests.cmake. - - Replace tests/CMakeLists.txt template-validation registrations with the project unit-test section. +Content edits made by --apply: - With --remove-ros2, strip fenced doc blocks. Logger namespace edit made by --apply: - --project-namespace replaces template_project::logging in the reusable logger files. -Workflow edits made by --apply: - - Materialize generic project CI workflows from the dormant *.yml.tpl files. - - Remove active template-validation workflow content and all *.yml.tpl files. - - With --remove-ros2, omit the runnable and dormant ROS 2 workflow. - Not removed: - - cmake/ production modules, including Python/MATLAB wrapper staging support. + - cmake/ production modules, including TensorRT discovery/integration and + Python/MATLAB wrapper staging support. - build_lib.sh, generate_version.sh, docs workflow files, issue forms, and docs guides. - src/utils/logging/ and doc/logging.md, because the logger is reusable project infrastructure. - - tests/template_test and tests/template_fixtures, because they are starter project tests. + - Root and test CMake files; tailoring never reconstructs build-system files. + - Starter C++/Python/CUDA tests, fixtures, and MATLAB wrapper runtime checks. + - Downstream custom tests, including CMake-script tests. + - Generic project workflows; --remove-ros2 removes only the ROS workflow. - .devcontainer, .vscode, examples/, and toolchains, because they are reusable project infrastructure. - profiling/ only when --keep-profiling is set. - ROS 2 overlay files unless --remove-ros2 is set. @@ -237,37 +204,6 @@ validate_root() { [[ -f "${ROOT_DIR}/build_lib.sh" ]] || die "Missing build_lib.sh in root: ${ROOT_DIR}" } -validate_workflow_templates() { - local workflow_name_ - local active_workflow_ - local workflow_template_ - - for workflow_name_ in "${project_workflow_names[@]}"; do - active_workflow_="${ROOT_DIR}/.github/workflows/${workflow_name_}" - workflow_template_="${active_workflow_}.tpl" - - if [[ -f "${workflow_template_}" ]]; then - [[ -f "${active_workflow_}" ]] \ - || die "Generic workflow template has no active pair: .github/workflows/${workflow_name_}.tpl" - grep -Fqx -- "${project_workflow_marker}" "${workflow_template_}" \ - || die "Generic workflow template is missing its ownership marker: .github/workflows/${workflow_name_}.tpl" - continue - fi - - if [[ -f "${active_workflow_}" ]]; then - grep -Fqx -- "${project_workflow_marker}" "${active_workflow_}" \ - || die "Active template-validation workflow has no generic template: .github/workflows/${workflow_name_}" - continue - fi - - if ((REMOVE_ROS2)) && [[ "${workflow_name_}" == "build_ros2_overlay.yml" ]]; then - continue - fi - - die "Missing runnable workflow and generic template: .github/workflows/${workflow_name_}" - done -} - tailor_logger_namespace() { local relative_path_ local source_file_ @@ -292,40 +228,6 @@ tailor_logger_namespace() { done } -materialize_project_workflows() { - local workflow_name_ - local active_workflow_ - local workflow_template_ - local tmp_ - - for workflow_name_ in "${project_workflow_names[@]}"; do - if ((REMOVE_ROS2)) && [[ "${workflow_name_}" == "build_ros2_overlay.yml" ]]; then - continue - fi - - active_workflow_="${ROOT_DIR}/.github/workflows/${workflow_name_}" - workflow_template_="${active_workflow_}.tpl" - if [[ ! -f "${workflow_template_}" ]]; then - [[ -f "${active_workflow_}" ]] \ - || die "Cannot materialize missing workflow: .github/workflows/${workflow_name_}" - info "project workflow already materialized .github/workflows/${workflow_name_}" - continue - fi - - if ((APPLY)); then - tmp_="$(mktemp "${active_workflow_}.tmp.XXXXXX")" - TEMPORARY_PATHS+=("${tmp_}") - cp -p -- "${workflow_template_}" "${tmp_}" - chmod --reference="${workflow_template_}" "${tmp_}" - mv -f -- "${tmp_}" "${active_workflow_}" - rm -f -- "${workflow_template_}" - info "materialized project workflow .github/workflows/${workflow_name_}" - else - info "would materialize project workflow .github/workflows/${workflow_name_}" - fi - done -} - remove_path() { local relative_path_="$1" local absolute_path_="${ROOT_DIR}/${relative_path_}" @@ -335,91 +237,8 @@ remove_path() { return fi - if ((APPLY)); then - rm -rf -- "${absolute_path_}" - info "removed ${relative_path_}" - else - info "would remove ${relative_path_}" - fi -} - -patch_root_cmakelists() { - local cmakelists_="${ROOT_DIR}/CMakeLists.txt" - local tmp_ - - if ! grep -q "AddMatlabWrapperRegressionTests.cmake\\|add_template_matlab_wrapper_regression_tests" "${cmakelists_}"; then - info "root CMakeLists.txt has no template MATLAB regression hook" - return - fi - - if ((APPLY)); then - tmp_="$(mktemp "${cmakelists_}.tmp.XXXXXX")" - TEMPORARY_PATHS+=("${tmp_}") - awk ' - /^[[:space:]]*include\("\$\{CMAKE_CURRENT_SOURCE_DIR\}\/tests\/cmake\/AddMatlabWrapperRegressionTests.cmake"\)/ {next} - /^[[:space:]]*add_template_matlab_wrapper_regression_tests\(\)/ {next} - {print} - ' "${cmakelists_}" > "${tmp_}" - chmod --reference="${cmakelists_}" "${tmp_}" - mv -f -- "${tmp_}" "${cmakelists_}" - info "patched CMakeLists.txt" - else - info "would patch CMakeLists.txt" - fi -} - -patch_tests_cmakelists() { - local tests_cmake_="${ROOT_DIR}/tests/CMakeLists.txt" - local tmp_ - - [[ -f "${tests_cmake_}" ]] || { - warn "tests/CMakeLists.txt not found; skipping test CMake patch" - return - } - - if ! grep -q "VerifyTemplateProject\\|template_project_.*flags\\|template_project_docs" "${tests_cmake_}"; then - info "tests/CMakeLists.txt has no template validation registrations" - return - fi - - if ! grep -q "^# Exclude EXCLUDED_LIST" "${tests_cmake_}"; then - warn "tests/CMakeLists.txt marker not found; skipping automatic patch" - return - fi - - if ((APPLY)); then - tmp_="$(mktemp "${tests_cmake_}.tmp.XXXXXX")" - TEMPORARY_PATHS+=("${tmp_}") - { - cat <<'EOF' -# Project unit tests. Template-development validation tests were removed by tailor_template_cleanup.sh. -include(CTest) - -# Exclude EXCLUDED_LIST from the list of tests -set(EXCLUDED_LIST "test_to_exclude") -set(TESTS_LIST "") - -# Include the content of the fixtures directory -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - -# Add subdirectories that may contain compiled and/or Python tests. -add_subdirectory(template_test) -add_subdirectory(template_fixtures) -add_subdirectory(template_cuda) # CUDA-init fixture gate + placeholder (built only when ENABLE_CUDA) - -# Add tests to build and register. -add_tests(${project_name} EXCLUDED_LIST TESTS_LIST ${CUDA_COMPILE_TARGET} CATCH2_TEST_PROPERTIES Catch2::Catch2WithMain) - -# Make catch2 to search for tests -message(STATUS "List of test targets: ${TESTS_LIST}") -EOF - } > "${tmp_}" - chmod --reference="${tests_cmake_}" "${tmp_}" - mv -f -- "${tmp_}" "${tests_cmake_}" - info "patched tests/CMakeLists.txt" - else - info "would patch tests/CMakeLists.txt" - fi + rm -rf -- "${absolute_path_}" + info "removed ${relative_path_}" } filter_ros2_overlay_doc() { @@ -485,18 +304,14 @@ strip_ros2_overlay_doc_fences() { continue fi - if ((APPLY)); then - tmp_="$(mktemp "${doc_file_}.tmp.XXXXXX")" - TEMPORARY_PATHS+=("${tmp_}") - if filter_ros2_overlay_doc "${doc_file_}" > "${tmp_}"; then - chmod --reference="${doc_file_}" "${tmp_}" - mv -f -- "${tmp_}" "${doc_file_}" - info "stripped ROS 2 overlay fence from ${relative_path_}" - else - die "Malformed ROS 2 overlay fence in ${relative_path_}" - fi + tmp_="$(mktemp "${doc_file_}.tmp.XXXXXX")" + TEMPORARY_PATHS+=("${tmp_}") + if filter_ros2_overlay_doc "${doc_file_}" > "${tmp_}"; then + chmod --reference="${doc_file_}" "${tmp_}" + mv -f -- "${tmp_}" "${doc_file_}" + info "stripped ROS 2 overlay fence from ${relative_path_}" else - info "would strip ROS 2 overlay fence from ${relative_path_}" + die "Malformed ROS 2 overlay fence in ${relative_path_}" fi done } @@ -525,7 +340,6 @@ main() { validate_project_namespace validate_root - validate_workflow_templates validate_ros2_overlay_doc_fences print_cleanup_list confirm_apply @@ -552,11 +366,6 @@ main() { info "keeping ROS 2 overlay; pass --remove-ros2 to strip it" fi - materialize_project_workflows - - patch_root_cmakelists - patch_tests_cmakelists - info "template cleanup complete" } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 135d05d..7d78006 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,375 +1,19 @@ -# Basic usage of catch2 for build config: -# These tests can use the Catch2-provided main -#add_executable(tests test.cpp) -#target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) - -# These tests need their own main -#add_executable(custom-main-tests test.cpp test-main.cpp) -#target_link_libraries(custom-main-tests PRIVATE Catch2::Catch2) - +# Starter runtime tests inherited unchanged by tailored projects. include(CTest) -################################################################################ -# CMake template development tests (remove for instantiation) -add_test( - NAME template_project_no_optimization_flags - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/no_optimization - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectNoOptimization.cmake -) -set_tests_properties( - template_project_no_optimization_flags - PROPERTIES - LABELS "flags;noopt" - TIMEOUT 180 -) - -add_test( - NAME template_project_optimized_config_flags - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/optimized_flags - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectOptimizedFlags.cmake -) -set_tests_properties( - template_project_optimized_config_flags - PROPERTIES - LABELS "flags;optimized" - TIMEOUT 180 -) - -add_test( - NAME template_project_docs_build_output - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/docs_workflow - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectDocsWorkflow.cmake -) -set_tests_properties( - template_project_docs_build_output - PROPERTIES - LABELS "docs;doxygen" - TIMEOUT 240 -) - -add_test( - NAME template_project_nested_docs_isolation - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/nested_docs_isolation - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectNestedDocsIsolation.cmake -) -set_tests_properties( - template_project_nested_docs_isolation - PROPERTIES - LABELS "docs;nested" - TIMEOUT 180 -) - -add_test( - NAME template_project_nested_install_headers - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/nested_install_headers - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake -) -set_tests_properties( - template_project_nested_install_headers - PROPERTIES - LABELS "nested;install;template" - TIMEOUT 240 -) - -add_test( - NAME template_project_version_no_source_side_effect - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/version_side_effect - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectVersionSideEffects.cmake -) -set_tests_properties( - template_project_version_no_source_side_effect - PROPERTIES - LABELS "version;configure" - TIMEOUT 180 -) - -add_test( - NAME template_project_python_test_option_gates - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/python_test_options - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectPythonTestOptions.cmake -) -set_tests_properties( - template_project_python_test_option_gates - PROPERTIES - LABELS "tests;python" - TIMEOUT 180 -) - -add_test( - NAME template_project_build_lib_clean_safety - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/build_lib_clean_safety - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectBuildLibCleanSafety.cmake -) -set_tests_properties( - template_project_build_lib_clean_safety - PROPERTIES - LABELS "build;clean;safety" - TIMEOUT 60 -) - -add_test( - NAME template_project_wrapper_maintenance - COMMAND bash - ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_wrapper_maintenance.sh -) -set_tests_properties( - template_project_wrapper_maintenance - PROPERTIES - LABELS "wrapper;maintenance;safety;template" - TIMEOUT 60 -) - -add_test( - NAME template_project_container_launcher - COMMAND bash - ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_run_in_container.sh -) -set_tests_properties( - template_project_container_launcher - PROPERTIES - LABELS "container;safety;template" - TIMEOUT 60 -) - -add_test( - NAME template_project_nested_option_isolation - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/nested_option_isolation - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectNestedOptionIsolation.cmake -) -set_tests_properties( - template_project_nested_option_isolation - PROPERTIES - LABELS "nested;options;template" - TIMEOUT 180 -) - -add_test( - NAME template_project_tensorrt_module - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/tensorrt_module - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectTensorRTModule.cmake -) -set_tests_properties( - template_project_tensorrt_module - PROPERTIES - LABELS "package;tensorrt;template" - TIMEOUT 300 -) - -add_test( - NAME template_project_python_packaging - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/python_packaging - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectPythonPackaging.cmake -) -set_tests_properties( - template_project_python_packaging - PROPERTIES - LABELS "install;package;python;wrapper" - TIMEOUT 180 -) - -add_test( - NAME template_project_build_tree_package_root - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/build_tree_package - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectBuildTreePackage.cmake -) -set_tests_properties( - template_project_build_tree_package_root - PROPERTIES - LABELS "package;template" - TIMEOUT 180 -) - -add_test( - NAME template_project_tailoring_cleanup_script - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/tailoring_cleanup - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectTailoringScript.cmake -) -set_tests_properties( - template_project_tailoring_cleanup_script - PROPERTIES - LABELS "tailoring;template" - TIMEOUT 60 -) - -add_test( - NAME template_project_add_tests_property_resolution - COMMAND ${CMAKE_COMMAND} - -DTEST_CMAKE_UTILS_FILE=${PROJECT_SOURCE_DIR}/cmake/cmake_utils.cmake - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectAddTestsProperties.cmake -) -set_tests_properties( - template_project_add_tests_property_resolution - PROPERTIES - LABELS "tests;cmake_utils" - TIMEOUT 60 -) - -add_test( - NAME template_project_ros2_overlay_static_contract - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/ros2_overlay - -DEXPECTED_VERSION=${PROJECT_VERSION_CORE} - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectRos2Overlay.cmake -) -set_tests_properties( - template_project_ros2_overlay_static_contract - PROPERTIES - LABELS "ros2;template" - TIMEOUT 60 -) - -add_test( - NAME template_project_release_tag_sync - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/release_tag_sync - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectReleaseTagSync.cmake -) -set_tests_properties( - template_project_release_tag_sync - PROPERTIES - LABELS "release;version;ros2" - TIMEOUT 180 -) - -if(ENABLE_CUDA) - add_test( - NAME template_project_cuda_sources - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/cuda_sources - -DTEST_CORE_TARGET=${LIB_TARGET_NAME} - "-DTEST_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES}" - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectCudaSources.cmake - ) - set_tests_properties( - template_project_cuda_sources - PROPERTIES - LABELS "cuda;sources;template" - TIMEOUT 240 - ) -endif() - -if(ENABLE_OPTIX) - add_test( - NAME template_project_optix_install_export - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/optix_install_export - -DTEST_PROJECT_NAME=${project_name} - -DTEST_CORE_TARGET=${LIB_TARGET_NAME} - "-DTEST_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES}" - "-DTEST_OPTIX_ROOT=${OPTIX_ROOT}" - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectOptixInstallExport.cmake - ) - set_tests_properties( - template_project_optix_install_export - PROPERTIES - LABELS "optix;install;package;template" - TIMEOUT 360 - ) -endif() - -find_program( - TEMPLATE_NVCC_EXECUTABLE - NAMES nvcc - PATHS - "$ENV{CUDA_HOME}/bin" - "$ENV{CUDA_PATH}/bin" - "/usr/local/cuda/bin" - "/usr/local/cuda-12.9/bin" -) - -if(TEMPLATE_NVCC_EXECUTABLE) - add_test( - NAME template_project_cuda_without_catch2_configure - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/cuda_without_catch2 - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectCudaWithoutCatch2.cmake - ) - set_tests_properties( - template_project_cuda_without_catch2_configure - PROPERTIES - LABELS "cuda;catch2;configure" - TIMEOUT 180 - ) -else() - message(STATUS "nvcc not found. Skipping CUDA-without-Catch2 configure regression test.") -endif() - -find_program(TEMPLATE_AARCH64_GCC_EXECUTABLE NAMES aarch64-linux-gnu-gcc) -find_program(TEMPLATE_AARCH64_GXX_EXECUTABLE NAMES aarch64-linux-gnu-g++) -set(TEMPLATE_AARCH64_TOOLCHAIN_FILE "${PROJECT_SOURCE_DIR}/cmake/toolchains/defaults/aarch64-linux-gnu.cmake") - -if(TEMPLATE_AARCH64_GCC_EXECUTABLE AND TEMPLATE_AARCH64_GXX_EXECUTABLE AND EXISTS "${TEMPLATE_AARCH64_TOOLCHAIN_FILE}") - foreach(_cross_case IN ITEMS configure_flags install_consumer nested_consumer) - add_test( - NAME template_project_aarch64_cross_${_cross_case} - COMMAND ${CMAKE_COMMAND} - -DTEST_TEMPLATE_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DTEST_BINARY_ROOT=${PROJECT_BINARY_DIR}/cross_compile/${_cross_case} - -DTEST_TOOLCHAIN_FILE=${TEMPLATE_AARCH64_TOOLCHAIN_FILE} - -DTEST_CASE=${_cross_case} - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectCrossCompile.cmake - ) - endforeach() - - set_tests_properties( - template_project_aarch64_cross_configure_flags - template_project_aarch64_cross_install_consumer - template_project_aarch64_cross_nested_consumer - PROPERTIES - LABELS "cross;aarch64" - TIMEOUT 180 - ) -else() - message(STATUS "aarch64-linux-gnu compilers not found. Skipping aarch64 cross-compilation regression tests.") -endif() - -################################################################################ - -# Exclude EXCLUDED_LIST from the list of tests -set(EXCLUDED_LIST "test_to_exclude") # Specify files to exclude WE +# Exclude project-specific files by filename or stem. +set(EXCLUDED_LIST "test_to_exclude") set(TESTS_LIST "") -# Include the content of the fixtures directory +# Make reusable fixture headers available to starter tests. include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -# Add subdirectories that may contain compiled and/or Python tests. +# Register C++, Python, fixture, and optional CUDA runtime tests. add_subdirectory(template_test) add_subdirectory(template_fixtures) -add_subdirectory(template_cuda) # CUDA-init fixture gate + placeholder (built only when ENABLE_CUDA) +add_subdirectory(template_cuda) -# Add tests to build and register. +# Register any test files placed directly under tests/. add_tests(${project_name} EXCLUDED_LIST TESTS_LIST ${CUDA_COMPILE_TARGET} CATCH2_TEST_PROPERTIES Catch2::Catch2WithMain) -# Make catch2 to search for tests message(STATUS "List of test targets: ${TESTS_LIST}") diff --git a/tests/cmake/VerifySourceReleaseArchive.cmake b/tests/cmake/VerifySourceReleaseArchive.cmake deleted file mode 100644 index 4fcaba4..0000000 --- a/tests/cmake/VerifySourceReleaseArchive.cmake +++ /dev/null @@ -1,131 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Validate one extracted canonical source archive independently of Git metadata. -foreach(required_var TEST_SOURCE_ROOT TEST_BINARY_ROOT EXPECTED_VERSION EXPECTED_FULL_VERSION) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT IS_DIRECTORY "${TEST_SOURCE_ROOT}") - message(FATAL_ERROR "Extracted source root does not exist: ${TEST_SOURCE_ROOT}") -endif() -if(EXISTS "${TEST_SOURCE_ROOT}/.git") - message(FATAL_ERROR "Canonical source archive must not contain .git") -endif() - -foreach(required_path - "VERSION" - "LICENSE" - "CMakeLists.txt" - "generate_version.sh" - "src/template_src/placeholder.cpp" - "ros2/tools/sync_package_metadata.py" - "ros2/template_project/package.xml" - "ros2/template_project_interfaces/package.xml" - "ros2/template_project_ros/package.xml" - "ros2/template_project_spinup/package.xml") - if(NOT EXISTS "${TEST_SOURCE_ROOT}/${required_path}") - message(FATAL_ERROR "Canonical source archive is missing required ${required_path}") - endif() -endforeach() - -# Reject fixed generated overlay locations here; the candidate release test -# verifies CMake build-tree ownership with controlled cache fixtures. -foreach(generated_path "ros2/build" "ros2/install" "ros2/log") - if(EXISTS "${TEST_SOURCE_ROOT}/${generated_path}") - message(FATAL_ERROR "Canonical source archive contains generated ROS output: ${generated_path}") - endif() -endforeach() - -find_program(_python_executable NAMES python3 REQUIRED) - -file(READ "${TEST_SOURCE_ROOT}/VERSION" _version_contents) -if(NOT _version_contents MATCHES "Project version core: ([0-9]+\\.[0-9]+\\.[0-9]+)") - message(FATAL_ERROR "Canonical source VERSION has no strict core version") -endif() -set(_archive_core_version "${CMAKE_MATCH_1}") -if(NOT _archive_core_version STREQUAL EXPECTED_VERSION) - message(FATAL_ERROR - "Canonical source core version mismatch: expected ${EXPECTED_VERSION}, got ${_archive_core_version}") -endif() -if(NOT _version_contents MATCHES "Full version: ([^\n]+)") - message(FATAL_ERROR "Canonical source VERSION has no full version") -endif() -set(_archive_full_version "${CMAKE_MATCH_1}") -if(NOT _archive_full_version STREQUAL EXPECTED_FULL_VERSION) - message(FATAL_ERROR - "Canonical source full version mismatch: expected ${EXPECTED_FULL_VERSION}, got ${_archive_full_version}") -endif() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -get_filename_component(_source_discovery_ceiling "${TEST_SOURCE_ROOT}" DIRECTORY) -execute_process( - # Build directories commonly live below the producing Git checkout. Stop - # Git discovery at the extraction parent so this configure must use VERSION. - COMMAND "${CMAKE_COMMAND}" -E env - "GIT_CEILING_DIRECTORIES=${_source_discovery_ceiling}" - "${CMAKE_COMMAND}" - -S "${TEST_SOURCE_ROOT}" - -B "${TEST_BINARY_ROOT}/metadata" - -DPROJECT_METADATA_ONLY=ON - RESULT_VARIABLE _metadata_result - OUTPUT_VARIABLE _metadata_stdout - ERROR_VARIABLE _metadata_stderr) -if(NOT _metadata_result EQUAL 0) - message(FATAL_ERROR - "No-Git metadata-only configure failed with exit code ${_metadata_result}.\n" - "stdout:\n${_metadata_stdout}\n" - "stderr:\n${_metadata_stderr}") -endif() -file(READ "${TEST_BINARY_ROOT}/metadata/CMakeCache.txt" _metadata_cache) -if(NOT _metadata_cache MATCHES "CMAKE_PROJECT_VERSION:STATIC=${EXPECTED_VERSION}([\n\r]|$)") - message(FATAL_ERROR "No-Git configure did not resolve expected version ${EXPECTED_VERSION}") -endif() -if(_metadata_cache MATCHES "CMAKE_CXX_COMPILER:") - message(FATAL_ERROR "Metadata-only archive verification unexpectedly configured a C++ compiler") -endif() - -file(GLOB _manifest_paths "${TEST_SOURCE_ROOT}/ros2/*/package.xml") -list(LENGTH _manifest_paths _manifest_count) -if(NOT _manifest_count EQUAL 4) - message(FATAL_ERROR "Canonical source archive must contain four ROS manifests; found ${_manifest_count}") -endif() -foreach(_manifest_path IN LISTS _manifest_paths) - execute_process( - COMMAND "${_python_executable}" -c - "import sys, xml.etree.ElementTree as ET; version=ET.parse(sys.argv[1]).getroot().findtext('version'); assert version == sys.argv[2], (sys.argv[1], version, sys.argv[2])" - "${_manifest_path}" "${EXPECTED_VERSION}" - RESULT_VARIABLE _manifest_parse_result - ERROR_VARIABLE _manifest_parse_stderr) - if(NOT _manifest_parse_result EQUAL 0) - message(FATAL_ERROR - "Archive manifest version validation failed: ${_manifest_path}\n" - "${_manifest_parse_stderr}") - endif() - file(READ "${_manifest_path}" _manifest_contents) - # The processing instruction is generated representation intentionally - # preserved byte-for-byte by the metadata synchronizer. - string(FIND "${_manifest_contents}" " - -static_assert(__cplusplus >= 202002L); - -int main() -{ - placeholder::placeholder_fcn(); - return 0; -} -") -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -set(_build_dir "${TEST_BINARY_ROOT}/template_build") -set(_install_dir "${TEST_BINARY_ROOT}/template_install") -set(_consumer_source_dir "${TEST_BINARY_ROOT}/consumer_source") -set(_consumer_build_dir "${TEST_BINARY_ROOT}/consumer_build") - -_run_step( - "Configure template build-tree package" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_INSTALL_PREFIX=${_install_dir} - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -DENABLE_CUDA=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) - -foreach(_package_file - "template_projectConfig.cmake" - "template_projectConfigVersion.cmake" - "template_projectTarget.cmake") - _assert_exists("${_build_dir}/${_package_file}") - _assert_not_exists("${_build_dir}/src/${_package_file}") -endforeach() - -file(READ "${_build_dir}/template_projectConfig.cmake" _config_contents) -if(NOT _config_contents MATCHES "include\\(\"\\$\\{CMAKE_CURRENT_LIST_DIR\\}/template_projectTarget\\.cmake\"\\)") - message(FATAL_ERROR "Build-tree config does not include the target export beside itself.") -endif() - -file(READ "${_build_dir}/template_projectTarget.cmake" _target_contents) -if(NOT _target_contents MATCHES "INTERFACE_COMPILE_FEATURES \"cxx_std_20\"") - message(FATAL_ERROR "Build-tree target export does not propagate cxx_std_20.") -endif() -if(NOT _target_contents MATCHES "template_project::template_project") - message(FATAL_ERROR "Build-tree target export does not define the namespaced package target.") -endif() - -_run_step("Build template library" ${CMAKE_COMMAND} --build "${_build_dir}") - -_write_build_tree_consumer("${_consumer_source_dir}") -_run_step( - "Configure build-tree package consumer" - ${CMAKE_COMMAND} - -S "${_consumer_source_dir}" - -B "${_consumer_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -Dtemplate_project_DIR=${_build_dir}) -_run_step("Build build-tree package consumer" ${CMAKE_COMMAND} --build "${_consumer_build_dir}") diff --git a/tests/cmake/VerifyTemplateProjectCrossCompile.cmake b/tests/cmake/VerifyTemplateProjectCrossCompile.cmake deleted file mode 100644 index dd100b4..0000000 --- a/tests/cmake/VerifyTemplateProjectCrossCompile.cmake +++ /dev/null @@ -1,184 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT TEST_TOOLCHAIN_FILE TEST_CASE) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -if(NOT EXISTS "${TEST_TOOLCHAIN_FILE}") - message(FATAL_ERROR "Invalid toolchain file: ${TEST_TOOLCHAIN_FILE}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_configure_template build_dir install_dir) - _run_step( - "Configure template cross build" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${build_dir}" - -DCMAKE_TOOLCHAIN_FILE=${TEST_TOOLCHAIN_FILE} - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -DCMAKE_INSTALL_PREFIX=${install_dir} - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -DENABLE_CUDA=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) -endfunction() - -function(_assert_compile_commands build_dir) - set(_compile_commands "${build_dir}/compile_commands.json") - if(NOT EXISTS "${_compile_commands}") - message(FATAL_ERROR "compile_commands.json not found: ${_compile_commands}") - endif() - - file(READ "${_compile_commands}" _commands) - foreach(_forbidden "-march=native" "-mtune=native") - if(_commands MATCHES "${_forbidden}") - message(FATAL_ERROR "Cross compile commands contain forbidden host-native flag: ${_forbidden}") - endif() - endforeach() - - foreach(_required "CROSS_COMPILED=1" "ARCH_AARCH64=1" "TARGET_OS_LINUX=1") - if(NOT _commands MATCHES "${_required}") - message(FATAL_ERROR "Cross compile commands do not contain required define: ${_required}") - endif() - endforeach() -endfunction() - -function(_write_installed_consumer source_dir) - file(REMOVE_RECURSE "${source_dir}") - file(MAKE_DIRECTORY "${source_dir}") - - file(WRITE "${source_dir}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_cross_consumer LANGUAGES CXX) -find_package(template_project REQUIRED) -add_library(consumer_library STATIC consumer.cpp) -target_link_libraries(consumer_library PUBLIC template_project::template_project) -add_executable(consumer_main main.cpp) -target_link_libraries(consumer_main PRIVATE consumer_library) -") - - file(WRITE "${source_dir}/consumer.cpp" -"#include -void ConsumerCall() -{ - placeholder::placeholder_fcn(); -} -") - - file(WRITE "${source_dir}/main.cpp" -"void ConsumerCall(); -int main() -{ - ConsumerCall(); - return 0; -} -") -endfunction() - -function(_write_nested_consumer source_dir) - file(REMOVE_RECURSE "${source_dir}") - file(MAKE_DIRECTORY "${source_dir}") - - file(WRITE "${source_dir}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_nested_cross_consumer LANGUAGES CXX) -set(LIB_NAMESPACE_OVERRIDE nested_template CACHE STRING \"\" FORCE) -set(LIB_TARGET_NAME_OVERRIDE nested_template_project CACHE STRING \"\" FORCE) -set(ENABLE_TESTS OFF CACHE BOOL \"\" FORCE) -set(ENABLE_FETCH_CATCH2 OFF CACHE BOOL \"\" FORCE) -set(ENABLE_CUDA OFF CACHE BOOL \"\" FORCE) -set(nested_template_BUILD_PROGRAMS OFF CACHE BOOL \"\" FORCE) -set(nested_template_BUILD_EXAMPLES OFF CACHE BOOL \"\" FORCE) -add_subdirectory(\"${TEST_TEMPLATE_SOURCE_DIR}\" \"${CMAKE_CURRENT_BINARY_DIR}/template_project_subbuild\" EXCLUDE_FROM_ALL) -add_library(parent_library STATIC parent.cpp) -target_link_libraries(parent_library PUBLIC nested_template::template_project) -add_executable(nested_main main.cpp) -target_link_libraries(nested_main PRIVATE parent_library) -") - - file(WRITE "${source_dir}/parent.cpp" -"#include -void ParentCall() -{ - placeholder::placeholder_fcn(); -} -") - - file(WRITE "${source_dir}/main.cpp" -"void ParentCall(); -int main() -{ - ParentCall(); - return 0; -} -") -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -if(TEST_CASE STREQUAL "configure_flags") - set(_build_dir "${TEST_BINARY_ROOT}/template_build") - set(_install_dir "${TEST_BINARY_ROOT}/template_install") - _configure_template("${_build_dir}" "${_install_dir}") - _assert_compile_commands("${_build_dir}") - _run_step("Build template cross library" ${CMAKE_COMMAND} --build "${_build_dir}") -elseif(TEST_CASE STREQUAL "install_consumer") - set(_build_dir "${TEST_BINARY_ROOT}/template_build") - set(_install_dir "${TEST_BINARY_ROOT}/template_install") - set(_consumer_source_dir "${TEST_BINARY_ROOT}/consumer_source") - set(_consumer_build_dir "${TEST_BINARY_ROOT}/consumer_build") - - _configure_template("${_build_dir}" "${_install_dir}") - _run_step("Install template cross library" ${CMAKE_COMMAND} --build "${_build_dir}" --target install) - _write_installed_consumer("${_consumer_source_dir}") - _run_step( - "Configure installed cross consumer" - ${CMAKE_COMMAND} - -S "${_consumer_source_dir}" - -B "${_consumer_build_dir}" - -DCMAKE_TOOLCHAIN_FILE=${TEST_TOOLCHAIN_FILE} - -DCMAKE_PREFIX_PATH=${_install_dir} - -DCMAKE_BUILD_TYPE=RelWithDebInfo) - _run_step("Build installed cross consumer" ${CMAKE_COMMAND} --build "${_consumer_build_dir}") -elseif(TEST_CASE STREQUAL "nested_consumer") - set(_nested_source_dir "${TEST_BINARY_ROOT}/nested_source") - set(_nested_build_dir "${TEST_BINARY_ROOT}/nested_build") - - _write_nested_consumer("${_nested_source_dir}") - _run_step( - "Configure nested cross consumer" - ${CMAKE_COMMAND} - -S "${_nested_source_dir}" - -B "${_nested_build_dir}" - -DCMAKE_TOOLCHAIN_FILE=${TEST_TOOLCHAIN_FILE} - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) - _assert_compile_commands("${_nested_build_dir}") - _run_step("Build nested cross consumer" ${CMAKE_COMMAND} --build "${_nested_build_dir}") -else() - message(FATAL_ERROR "Unsupported TEST_CASE='${TEST_CASE}'.") -endif() diff --git a/tests/cmake/VerifyTemplateProjectCudaSources.cmake b/tests/cmake/VerifyTemplateProjectCudaSources.cmake deleted file mode 100644 index 3f94792..0000000 --- a/tests/cmake/VerifyTemplateProjectCudaSources.cmake +++ /dev/null @@ -1,83 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var - TEST_TEMPLATE_SOURCE_DIR - TEST_BINARY_ROOT - TEST_CORE_TARGET - TEST_CUDA_ARCHITECTURES) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -if(TEST_CUDA_ARCHITECTURES STREQUAL "") - message(FATAL_ERROR "TEST_CUDA_ARCHITECTURES must contain at least one CUDA architecture") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_build_dir "${TEST_BINARY_ROOT}/build") - -_run_step( - "Configure isolated CUDA source build" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - "-DCMAKE_CUDA_ARCHITECTURES=${TEST_CUDA_ARCHITECTURES}" - -DCPU_ENABLE_NATIVE_TUNING=OFF - -DENABLE_CUDA=ON - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DENABLE_TBB=OFF - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - -Dtemplate_project_BUILD_PYTHON_WRAPPER=OFF - -Dtemplate_project_BUILD_MATLAB_WRAPPER=OFF) - -_run_step( - "Build isolated CUDA core target" - ${CMAKE_COMMAND} - --build "${_build_dir}" - --target "${TEST_CORE_TARGET}") - -set(_compile_commands_path "${_build_dir}/compile_commands.json") -if(NOT EXISTS "${_compile_commands_path}") - message(FATAL_ERROR "compile_commands.json not found: ${_compile_commands_path}") -endif() - -file(READ "${_compile_commands_path}" _compile_commands) -string(REPLACE "\\" "/" _compile_commands "${_compile_commands}") - -if(NOT _compile_commands MATCHES "src/template_src_kernels/placeholder\\.cu") - message(FATAL_ERROR - "CUDA was enabled, but src/template_src_kernels/placeholder.cu is absent " - "from the isolated core target compile graph: ${_compile_commands_path}") -endif() - -if(_compile_commands MATCHES "src/template_src_kernels/placeholder_to_ptx\\.ptx\\.cu") - message(FATAL_ERROR - "OptiX PTX input was compiled as an ordinary library translation unit while " - "ENABLE_OPTIX=OFF: ${_compile_commands_path}") -endif() diff --git a/tests/cmake/VerifyTemplateProjectCudaWithoutCatch2.cmake b/tests/cmake/VerifyTemplateProjectCudaWithoutCatch2.cmake deleted file mode 100644 index a089c83..0000000 --- a/tests/cmake/VerifyTemplateProjectCudaWithoutCatch2.cmake +++ /dev/null @@ -1,50 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() - -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -set(_build_dir "${TEST_BINARY_ROOT}/configure") -_run_step( - "Configure CUDA build with Catch2 unavailable" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DENABLE_CUDA=ON - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DENABLE_TBB=OFF - -DENABLE_TESTS=ON - -DENABLE_PYTHON_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -DCMAKE_DISABLE_FIND_PACKAGE_Catch2=TRUE - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - -Dtemplate_project_BUILD_PYTHON_WRAPPER=OFF - -Dtemplate_project_BUILD_MATLAB_WRAPPER=OFF) diff --git a/tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake b/tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake deleted file mode 100644 index 9a9c942..0000000 --- a/tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake +++ /dev/null @@ -1,142 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() -get_filename_component(_template_source_dir "${TEST_TEMPLATE_SOURCE_DIR}" REALPATH) - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_assert_file_contains file_path pattern) - if(NOT EXISTS "${file_path}") - message(FATAL_ERROR "Expected file does not exist: ${file_path}") - endif() - file(READ "${file_path}" _contents) - if(NOT _contents MATCHES "${pattern}") - message(FATAL_ERROR "Expected '${file_path}' to match pattern '${pattern}'") - endif() -endfunction() - -function(_assert_file_not_contains file_path pattern) - if(NOT EXISTS "${file_path}") - message(FATAL_ERROR "Expected file does not exist: ${file_path}") - endif() - file(READ "${file_path}" _contents) - if(_contents MATCHES "${pattern}") - message(FATAL_ERROR "Expected '${file_path}' not to match pattern '${pattern}'") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -set(_build_dir "${TEST_BINARY_ROOT}/build") -_run_step( - "Configure documentation build" - ${CMAKE_COMMAND} - -S "${_template_source_dir}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DENABLE_TESTS=OFF - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DBUILD_DOC_HTML=ON - -DBUILD_DOC_XML=ON - -DBUILD_DOC_LATEX=OFF - -DDOC_WARN_AS_ERROR=ON - -DWRITE_SOURCE_VERSION_FILE=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) - -execute_process( - COMMAND ${CMAKE_COMMAND} --build "${_build_dir}" --target doc - RESULT_VARIABLE _doc_build_result - OUTPUT_VARIABLE _doc_build_stdout - ERROR_VARIABLE _doc_build_stderr) -if(NOT _doc_build_result EQUAL 0) - message(FATAL_ERROR - "Build documentation failed with exit code ${_doc_build_result}.\n" - "stdout:\n${_doc_build_stdout}\n" - "stderr:\n${_doc_build_stderr}") -endif() - -set(_doxyfile "${_build_dir}/doc/Doxyfile") -set(_html_index "${_build_dir}/doc/html/index.html") -set(_xml_index "${_build_dir}/doc/xml/index.xml") - -_assert_file_contains("${_doxyfile}" "GENERATE_HTML[ ]*=[ ]*YES") -_assert_file_contains("${_doxyfile}" "GENERATE_XML[ ]*=[ ]*YES") -_assert_file_contains("${_doxyfile}" "INPUT[ ]*=.*${_template_source_dir}/README.md.*${_template_source_dir}/src.*${_template_source_dir}/doc") -_assert_file_contains("${_doxyfile}" "EXCLUDE[ ]*=.*${_template_source_dir}/lib.*${_template_source_dir}/doc/developments.*${_template_source_dir}/doc/reports") -_assert_file_contains("${_doxyfile}" "USE_MDFILE_AS_MAINPAGE[ ]*=[ ]*${_template_source_dir}/doc/main_page.md") -file(READ "${_doxyfile}" _doxyfile_contents) -string(REGEX MATCH "INPUT[^\n]*" _doxyfile_input_line "${_doxyfile_contents}") -if(_doxyfile_input_line MATCHES "${_template_source_dir}/lib") - message(FATAL_ERROR "Doxygen INPUT includes lib directory: ${_doxyfile_input_line}") -endif() -string(REGEX MATCH "(^|\n)EXCLUDE[ ]*=[^\n]*" _doxyfile_exclude_line "${_doxyfile_contents}") -if(NOT _doxyfile_exclude_line MATCHES "${_template_source_dir}/doc/developments") - message(FATAL_ERROR "Doxygen EXCLUDE does not include internal development notes: ${_doxyfile_exclude_line}") -endif() -if(NOT _doxyfile_exclude_line MATCHES "${_template_source_dir}/doc/reports") - message(FATAL_ERROR "Doxygen EXCLUDE does not include internal implementation reports: ${_doxyfile_exclude_line}") -endif() - -if(NOT EXISTS "${_html_index}") - message(FATAL_ERROR "Doxygen HTML index was not generated: ${_html_index}") -endif() -if(NOT EXISTS "${_xml_index}") - message(FATAL_ERROR "Doxygen XML index was not generated: ${_xml_index}") -endif() - -file(GLOB_RECURSE _html_files "${_build_dir}/doc/html/*.html") -set(_combined_html "") -foreach(_html_file IN LISTS _html_files) - file(READ "${_html_file}" _html_text) - string(APPEND _combined_html "\n${_html_text}") -endforeach() - -foreach(_required_text - "Agent Tailoring Prompt" - "build_lib.sh Reference" - "Template Usage Guide" - "C\\+\\+ and CUDA Build Guide" - "Python and MATLAB Wrapper Guide" - "Versioning Guide" - "Documentation Workflow" - "Testing, CI, and Issue Workflow") - if(NOT _combined_html MATCHES "${_required_text}") - message(FATAL_ERROR "Generated HTML documentation does not contain '${_required_text}'") - endif() -endforeach() - -foreach(_internal_text - "MATLAB wrapper crash investigation" - "Documentation workflow rollout" - "docs_workflow_rollout" - "ROS 2 Overlay Implementation Review" - "ros2_overlay_implementation_review") - if(_combined_html MATCHES "${_internal_text}") - message(FATAL_ERROR "Generated HTML documentation contains internal note '${_internal_text}'") - endif() -endforeach() diff --git a/tests/cmake/VerifyTemplateProjectNestedDocsIsolation.cmake b/tests/cmake/VerifyTemplateProjectNestedDocsIsolation.cmake deleted file mode 100644 index c7d34f8..0000000 --- a/tests/cmake/VerifyTemplateProjectNestedDocsIsolation.cmake +++ /dev/null @@ -1,74 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}/parent") - -file(WRITE "${TEST_BINARY_ROOT}/parent/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(parent_docs_isolation LANGUAGES CXX) -set(LIB_NAMESPACE_OVERRIDE nested_template CACHE STRING \"\" FORCE) -set(LIB_TARGET_NAME_OVERRIDE nested_template_project CACHE STRING \"\" FORCE) -set(ENABLE_TESTS OFF CACHE BOOL \"\" FORCE) -set(ENABLE_FETCH_CATCH2 OFF CACHE BOOL \"\" FORCE) -set(ENABLE_CUDA OFF CACHE BOOL \"\" FORCE) -set(nested_template_BUILD_PROGRAMS OFF CACHE BOOL \"\" FORCE) -set(nested_template_BUILD_EXAMPLES OFF CACHE BOOL \"\" FORCE) -add_subdirectory(\"${TEST_TEMPLATE_SOURCE_DIR}\" \"${CMAKE_CURRENT_BINARY_DIR}/template_subbuild\" EXCLUDE_FROM_ALL) -add_library(parent_library STATIC parent.cpp) -target_link_libraries(parent_library PRIVATE nested_template::template_project) -") - -file(WRITE "${TEST_BINARY_ROOT}/parent/parent.cpp" -"#include -void ParentDocsIsolationCall() -{ - placeholder::placeholder_fcn(); -} -") - -set(_parent_build "${TEST_BINARY_ROOT}/parent_build") -_run_step( - "Configure parent nested build" - ${CMAKE_COMMAND} - -S "${TEST_BINARY_ROOT}/parent" - -B "${_parent_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo) - -_run_step("Build parent nested library" ${CMAKE_COMMAND} --build "${_parent_build}" --target parent_library) - -execute_process( - COMMAND ${CMAKE_COMMAND} --build "${_parent_build}" --target doc - RESULT_VARIABLE _doc_result - OUTPUT_VARIABLE _doc_stdout - ERROR_VARIABLE _doc_stderr) -if(_doc_result EQUAL 0) - message(FATAL_ERROR - "Nested template project unexpectedly created a parent-visible 'doc' target.\n" - "stdout:\n${_doc_stdout}\n" - "stderr:\n${_doc_stderr}") -endif() - -if(EXISTS "${_parent_build}/template_subbuild/doc/Doxyfile") - message(FATAL_ERROR "Nested template project configured Doxygen unexpectedly: ${_parent_build}/template_subbuild/doc/Doxyfile") -endif() diff --git a/tests/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake b/tests/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake deleted file mode 100644 index ea9e46d..0000000 --- a/tests/cmake/VerifyTemplateProjectNestedInstallHeaders.cmake +++ /dev/null @@ -1,208 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_assert_exists file_path) - if(NOT EXISTS "${file_path}") - message(FATAL_ERROR "Expected installed path does not exist: ${file_path}") - endif() -endfunction() - -function(_assert_not_exists file_path) - if(EXISTS "${file_path}") - message(FATAL_ERROR "Unexpected install-prefix path exists: ${file_path}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}/parent") - -file(WRITE "${TEST_BINARY_ROOT}/parent/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_nested_install_parent LANGUAGES CXX) -set(ENABLE_TESTS OFF CACHE BOOL \"\" FORCE) -set(ENABLE_FETCH_CATCH2 OFF CACHE BOOL \"\" FORCE) -set(ENABLE_CUDA OFF CACHE BOOL \"\" FORCE) -set(template_project_BUILD_PROGRAMS OFF CACHE BOOL \"\" FORCE) -set(template_project_BUILD_EXAMPLES OFF CACHE BOOL \"\" FORCE) -add_subdirectory(\"${TEST_TEMPLATE_SOURCE_DIR}\" \"\${CMAKE_CURRENT_BINARY_DIR}/template_subbuild\") -add_executable(parent_consumer parent_consumer.cpp) -target_link_libraries(parent_consumer PRIVATE template_project::template_project) -") - -file(WRITE "${TEST_BINARY_ROOT}/parent/parent_consumer.cpp" -"#include - -int main() -{ - return cpp_playground::CWrapperPlaceholder::multiplyBy2(2.0) == 4.0 ? 0 : 1; -} -") - -set(_parent_build "${TEST_BINARY_ROOT}/parent_build") -set(_install_prefix "${TEST_BINARY_ROOT}/install") -_run_step( - "Configure parent nested install build" - ${CMAKE_COMMAND} - -S "${TEST_BINARY_ROOT}/parent" - -B "${_parent_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_INSTALL_PREFIX=${_install_prefix}) - -# Refuse to execute install rules that traverse above the advertised include -# root. The current regression is expected to fail here before it can write -# outside the scratch install prefix. -file(GLOB_RECURSE _nested_install_scripts - "${_parent_build}/template_subbuild/src/*/cmake_install.cmake") -if(NOT _nested_install_scripts) - message(FATAL_ERROR "No nested module install scripts were generated") -endif() - -foreach(_install_script IN LISTS _nested_install_scripts) - file(READ "${_install_script}" _install_script_contents) - string(FIND - "${_install_script_contents}" - "/include/template_project/.." - _unsafe_destination_index) - if(NOT _unsafe_destination_index EQUAL -1) - message(FATAL_ERROR - "Nested header install destination escapes include/template_project: " - "${_install_script}") - endif() -endforeach() - -_run_step( - "Build parent and nested template targets" - ${CMAKE_COMMAND} --build "${_parent_build}" --target parent_consumer) -_run_step( - "Install nested template package" - ${CMAKE_COMMAND} --install "${_parent_build}") - -set(_installed_headers - "wrapped_impl/CWrapperPlaceholder.h" - "template_src/placeholder.h" - "template_src_kernels/placeholder.cuh" - "utils/wrap_adapters/GtsamAliases.h") - -foreach(_installed_header IN LISTS _installed_headers) - _assert_exists( - "${_install_prefix}/include/template_project/${_installed_header}") -endforeach() - -foreach(_leaked_directory wrapped_impl template_src template_src_kernels utils) - _assert_not_exists("${_install_prefix}/${_leaked_directory}") -endforeach() - -# Exercise the logging module's real install rule in isolation. -set(_logging_probe_source "${TEST_BINARY_ROOT}/logging_install_probe") -set(_logging_probe_build "${TEST_BINARY_ROOT}/logging_install_probe_build") -set(_logging_install_prefix "${TEST_BINARY_ROOT}/logging_install") -file(MAKE_DIRECTORY "${_logging_probe_source}") -file(WRITE "${_logging_probe_source}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_logging_install_probe LANGUAGES NONE) -set(project_name template_project) -set(PROJECT_SOURCE_DIR \"${TEST_TEMPLATE_SOURCE_DIR}\") -add_subdirectory( - \"${TEST_TEMPLATE_SOURCE_DIR}/src/utils/logging\" - \"\${CMAKE_CURRENT_BINARY_DIR}/logging_subbuild\") -") - -_run_step( - "Configure logging header install probe" - ${CMAKE_COMMAND} - -S "${_logging_probe_source}" - -B "${_logging_probe_build}" - -DCMAKE_INSTALL_PREFIX=${_logging_install_prefix}) - -set(_logging_install_script - "${_logging_probe_build}/logging_subbuild/cmake_install.cmake") -_assert_exists("${_logging_install_script}") -file(READ "${_logging_install_script}" _logging_install_script_contents) -string(FIND - "${_logging_install_script_contents}" - "/include/template_project/.." - _unsafe_logging_destination_index) -if(NOT _unsafe_logging_destination_index EQUAL -1) - message(FATAL_ERROR - "Nested logging header install destination escapes " - "include/template_project: ${_logging_install_script}") -endif() - -_run_step( - "Install logging module headers" - ${CMAKE_COMMAND} --install "${_logging_probe_build}") -_assert_exists( - "${_logging_install_prefix}/include/template_project/utils/logging/CLogger.h") -_assert_not_exists("${_logging_install_prefix}/utils") - -set(_consumer_source "${TEST_BINARY_ROOT}/installed_consumer") -set(_consumer_build "${TEST_BINARY_ROOT}/installed_consumer_build") -set(_installed_package_dir - "${_install_prefix}/lib/cmake/template_project") -file(MAKE_DIRECTORY "${_consumer_source}") - -file(WRITE "${_consumer_source}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_installed_header_consumer LANGUAGES CXX) -find_package(template_project CONFIG REQUIRED) -add_executable(installed_consumer main.cpp) -target_link_libraries(installed_consumer PRIVATE template_project::template_project) -") - -file(WRITE "${_consumer_source}/main.cpp" -"#include - -int main() -{ - return cpp_playground::CWrapperPlaceholder::multiplyBy2(3.0) == 6.0 ? 0 : 1; -} -") - -_run_step( - "Configure installed-only template consumer" - ${CMAKE_COMMAND} - -S "${_consumer_source}" - -B "${_consumer_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -Dtemplate_project_DIR:PATH=${_installed_package_dir} - -DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF - -DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF) - -file(STRINGS - "${_consumer_build}/CMakeCache.txt" - _resolved_package_dir_entry - REGEX "^template_project_DIR:PATH=") -if(NOT _resolved_package_dir_entry STREQUAL - "template_project_DIR:PATH=${_installed_package_dir}") - message(FATAL_ERROR - "Installed consumer resolved an unexpected template_project package: " - "${_resolved_package_dir_entry}") -endif() - -_run_step( - "Build installed-only template consumer" - ${CMAKE_COMMAND} --build "${_consumer_build}") diff --git a/tests/cmake/VerifyTemplateProjectNestedOptionIsolation.cmake b/tests/cmake/VerifyTemplateProjectNestedOptionIsolation.cmake deleted file mode 100644 index e9d8d39..0000000 --- a/tests/cmake/VerifyTemplateProjectNestedOptionIsolation.cmake +++ /dev/null @@ -1,187 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Verify nested project-option isolation and top-level legacy-option migration. -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR - "Invalid template source directory: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -function(_run_success step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_read_cache_value cache_path cache_key out_var) - file(STRINGS "${cache_path}" _cache_lines REGEX "^${cache_key}:") - list(LENGTH _cache_lines _cache_line_count) - if(NOT _cache_line_count EQUAL 1) - message(FATAL_ERROR "Missing generated CMake cache field: ${cache_key}") - endif() - list(GET _cache_lines 0 _cache_line) - string(REGEX REPLACE "^[^=]*=" "" _cache_value "${_cache_line}") - set(${out_var} "${_cache_value}" PARENT_SCOPE) -endfunction() - -function(_require_cache_value cache_path cache_key expected_value) - _read_cache_value("${cache_path}" "${cache_key}" _actual_value) - if(NOT _actual_value STREQUAL expected_value) - message(FATAL_ERROR - "Expected ${cache_key}=${expected_value}, got ${_actual_value}.") - endif() -endfunction() - -function(_require_cache_entry_absent cache_path cache_key) - file(STRINGS "${cache_path}" _cache_lines REGEX "^${cache_key}:") - if(_cache_lines) - message(FATAL_ERROR - "Legacy cache field ${cache_key} was not removed after migration.") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_parent_source "${TEST_BINARY_ROOT}/parent") -set(_parent_build "${TEST_BINARY_ROOT}/build") -file(MAKE_DIRECTORY "${_parent_source}") - -# The parent deliberately owns conflicting generic values. Canonical template -# selectors keep the nested library active while disabling its CUDA/OptiX path. -file(WRITE "${_parent_source}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(template_project_option_isolation_parent LANGUAGES CXX) - -set(PROJECT_METADATA_ONLY ON CACHE BOOL \"Parent metadata selector\" FORCE) -set(ENABLE_CUDA ON CACHE BOOL \"Parent CUDA selector\" FORCE) -set(ENABLE_OPTIX ON CACHE BOOL \"Parent OptiX selector\" FORCE) - -set(template_project_METADATA_ONLY OFF CACHE BOOL \"\" FORCE) -set(template_project_ENABLE_CUDA OFF CACHE BOOL \"\" FORCE) -set(template_project_ENABLE_OPTIX OFF CACHE BOOL \"\" FORCE) -set(ENABLE_TESTS OFF CACHE BOOL \"\" FORCE) -set(ENABLE_FETCH_CATCH2 OFF CACHE BOOL \"\" FORCE) -set(template_project_BUILD_PROGRAMS OFF CACHE BOOL \"\" FORCE) -set(template_project_BUILD_EXAMPLES OFF CACHE BOOL \"\" FORCE) - -add_subdirectory( - \"${TEST_TEMPLATE_SOURCE_DIR}\" - \"\${CMAKE_CURRENT_BINARY_DIR}/template_project_subbuild\" - EXCLUDE_FROM_ALL) - -if(NOT TARGET template_project::template_project) - message(FATAL_ERROR \"Nested template target was not created.\") -endif() -if(DEFINED CMAKE_CUDA_COMPILER) - message(FATAL_ERROR - \"Nested template consumed the parent's generic ENABLE_CUDA option.\") -endif() -if(NOT PROJECT_METADATA_ONLY OR NOT ENABLE_CUDA OR NOT ENABLE_OPTIX) - message(FATAL_ERROR \"Nested template changed parent-owned generic options.\") -endif() -") - -execute_process( - COMMAND - "${CMAKE_COMMAND}" - -S "${_parent_source}" - -B "${_parent_build}" - -DCMAKE_BUILD_TYPE=Release - RESULT_VARIABLE _configure_result - OUTPUT_VARIABLE _configure_stdout - ERROR_VARIABLE _configure_stderr) -if(NOT _configure_result EQUAL 0) - message(FATAL_ERROR - "Nested option-isolation configure failed with exit code " - "${_configure_result}.\n" - "stdout:\n${_configure_stdout}\n" - "stderr:\n${_configure_stderr}") -endif() - -# Compatibility aliases are one-config inputs. Reconfigure the same build to -# prove they update, rather than merely initialize, their canonical options. -set(_metadata_alias_build "${TEST_BINARY_ROOT}/metadata_alias_build") -_run_success( - "Enable metadata-only mode through the legacy alias" - "${CMAKE_COMMAND}" - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_metadata_alias_build}" - -Dtemplate_project_METADATA_ONLY=OFF - -DPROJECT_METADATA_ONLY=ON) -set(_metadata_alias_cache "${_metadata_alias_build}/CMakeCache.txt") -_require_cache_value( - "${_metadata_alias_cache}" "template_project_METADATA_ONLY" "ON") -_require_cache_entry_absent( - "${_metadata_alias_cache}" "PROJECT_METADATA_ONLY") -file(STRINGS "${_metadata_alias_cache}" _metadata_cxx_compiler - REGEX "^CMAKE_CXX_COMPILER:") -if(_metadata_cxx_compiler) - message(FATAL_ERROR "Metadata-only alias unexpectedly enabled C++.") -endif() - -_run_success( - "Disable metadata-only mode through the legacy alias" - "${CMAKE_COMMAND}" - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_metadata_alias_build}" - -Dtemplate_project_METADATA_ONLY=ON - -DPROJECT_METADATA_ONLY=OFF - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) -_require_cache_value( - "${_metadata_alias_cache}" "template_project_METADATA_ONLY" "OFF") -_require_cache_entry_absent( - "${_metadata_alias_cache}" "PROJECT_METADATA_ONLY") -_read_cache_value( - "${_metadata_alias_cache}" "CMAKE_CXX_COMPILER" _metadata_cxx_compiler) - -# Keep language selection disabled while exercising the CUDA and OptiX aliases; -# this validates cache migration without requiring either SDK on a CPU runner. -set(_feature_alias_build "${TEST_BINARY_ROOT}/feature_alias_build") -_run_success( - "Enable CUDA and OptiX through legacy aliases" - "${CMAKE_COMMAND}" - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_feature_alias_build}" - -Dtemplate_project_METADATA_ONLY=ON - -Dtemplate_project_ENABLE_CUDA=OFF - -Dtemplate_project_ENABLE_OPTIX=OFF - -DENABLE_CUDA=ON - -DENABLE_OPTIX=ON) -set(_feature_alias_cache "${_feature_alias_build}/CMakeCache.txt") -foreach(_feature IN ITEMS CUDA OPTIX) - _require_cache_value( - "${_feature_alias_cache}" "template_project_ENABLE_${_feature}" "ON") - _require_cache_entry_absent( - "${_feature_alias_cache}" "ENABLE_${_feature}") -endforeach() - -_run_success( - "Disable CUDA and OptiX through legacy aliases" - "${CMAKE_COMMAND}" - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_feature_alias_build}" - -Dtemplate_project_ENABLE_CUDA=ON - -Dtemplate_project_ENABLE_OPTIX=ON - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF) -foreach(_feature IN ITEMS CUDA OPTIX) - _require_cache_value( - "${_feature_alias_cache}" "template_project_ENABLE_${_feature}" "OFF") - _require_cache_entry_absent( - "${_feature_alias_cache}" "ENABLE_${_feature}") -endforeach() diff --git a/tests/cmake/VerifyTemplateProjectNoOptimization.cmake b/tests/cmake/VerifyTemplateProjectNoOptimization.cmake deleted file mode 100644 index 41288be..0000000 --- a/tests/cmake/VerifyTemplateProjectNoOptimization.cmake +++ /dev/null @@ -1,62 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") - -_run_step( - "Configure template no-optimization build" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${TEST_BINARY_ROOT}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -DNO_OPTIMIZATION=ON - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -DENABLE_CUDA=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) - -set(_compile_commands "${TEST_BINARY_ROOT}/compile_commands.json") -if(NOT EXISTS "${_compile_commands}") - message(FATAL_ERROR "compile_commands.json not found: ${_compile_commands}") -endif() - -file(READ "${_compile_commands}" _commands) -foreach(_required "-O0" "-g3" "-fno-omit-frame-pointer" "-fno-inline" "-fno-optimize-sibling-calls") - if(NOT _commands MATCHES "${_required}") - message(FATAL_ERROR "No-optimization compile commands do not contain required flag: ${_required}") - endif() -endforeach() - -foreach(_forbidden "-O2" "-O3" "-march=native" "-mtune=native" "-DNDEBUG") - if(_commands MATCHES "${_forbidden}") - message(FATAL_ERROR "No-optimization compile commands contain forbidden flag: ${_forbidden}") - endif() -endforeach() - -_run_step("Build template no-optimization library" ${CMAKE_COMMAND} --build "${TEST_BINARY_ROOT}") diff --git a/tests/cmake/VerifyTemplateProjectOptimizedFlags.cmake b/tests/cmake/VerifyTemplateProjectOptimizedFlags.cmake deleted file mode 100644 index 8a398e8..0000000 --- a/tests/cmake/VerifyTemplateProjectOptimizedFlags.cmake +++ /dev/null @@ -1,75 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_TEMPLATE_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "Invalid template source dir: ${TEST_TEMPLATE_SOURCE_DIR}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_assert_flags build_dir build_type) - set(_compile_commands "${build_dir}/compile_commands.json") - if(NOT EXISTS "${_compile_commands}") - message(FATAL_ERROR "compile_commands.json not found: ${_compile_commands}") - endif() - - file(READ "${_compile_commands}" _commands) - if(build_type STREQUAL "Release") - set(_required_flags "-O3" "-DNDEBUG") - elseif(build_type STREQUAL "RelWithDebInfo") - set(_required_flags "-O2" "-g" "-DNDEBUG") - else() - message(FATAL_ERROR "Unsupported optimized build type: ${build_type}") - endif() - - foreach(_required ${_required_flags}) - if(NOT _commands MATCHES "${_required}") - message(FATAL_ERROR "${build_type} compile commands do not contain required flag: ${_required}") - endif() - endforeach() - - foreach(_forbidden "-O0" "-Og" "-g3" "-fno-omit-frame-pointer" "-fno-inline" "-fno-optimize-sibling-calls" "-fsanitize") - if(_commands MATCHES "${_forbidden}") - message(FATAL_ERROR "${build_type} compile commands contain profiling/debug-only flag: ${_forbidden}") - endif() - endforeach() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") - -foreach(_build_type IN ITEMS Release RelWithDebInfo) - set(_build_dir "${TEST_BINARY_ROOT}/${_build_type}") - _run_step( - "Configure template ${_build_type} build" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=${_build_type} - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -DCPU_ENABLE_NATIVE_TUNING=OFF - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -DENABLE_CUDA=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) - _assert_flags("${_build_dir}" "${_build_type}") - _run_step("Build template ${_build_type} library" ${CMAKE_COMMAND} --build "${_build_dir}") -endforeach() diff --git a/tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake b/tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake deleted file mode 100644 index fb97007..0000000 --- a/tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake +++ /dev/null @@ -1,122 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var - TEST_TEMPLATE_SOURCE_DIR - TEST_BINARY_ROOT - TEST_PROJECT_NAME - TEST_CORE_TARGET - TEST_CUDA_ARCHITECTURES - TEST_OPTIX_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -if(NOT EXISTS "${TEST_OPTIX_ROOT}/include/optix.h") - message(FATAL_ERROR "TEST_OPTIX_ROOT does not contain include/optix.h: ${TEST_OPTIX_ROOT}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_build_dir "${TEST_BINARY_ROOT}/build") -set(_install_prefix "${TEST_BINARY_ROOT}/install") - -_run_step( - "Configure isolated OptiX package build" - ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_INSTALL_PREFIX=${_install_prefix} - "-DCMAKE_CUDA_ARCHITECTURES=${TEST_CUDA_ARCHITECTURES}" - -DCPU_ENABLE_NATIVE_TUNING=OFF - -DENABLE_CUDA=ON - -DENABLE_OPTIX=ON - -DOPTIX_AUTO_INSTALL=OFF - "-DOPTIX_ROOT=${TEST_OPTIX_ROOT}" - -DENABLE_OPENGL=OFF - -DENABLE_TBB=OFF - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - -Dtemplate_project_BUILD_PYTHON_WRAPPER=OFF - -Dtemplate_project_BUILD_MATLAB_WRAPPER=OFF) - -_run_step( - "Build isolated OptiX core target" - ${CMAKE_COMMAND} - --build "${_build_dir}" - --target "${TEST_CORE_TARGET}") -_run_step("Install isolated OptiX package" ${CMAKE_COMMAND} --install "${_build_dir}") - -file(GLOB_RECURSE _target_exports "${_install_prefix}/*Target.cmake") -list(FILTER _target_exports INCLUDE REGEX "/${TEST_PROJECT_NAME}Target\\.cmake$") -list(LENGTH _target_exports _target_export_count) -if(NOT _target_export_count EQUAL 1) - message(FATAL_ERROR - "Expected one installed target export for ${TEST_PROJECT_NAME}, got: ${_target_exports}") -endif() -list(GET _target_exports 0 _target_export) -file(READ "${_target_export}" _target_export_contents) - -string(FIND "${_target_export_contents}" "${TEST_OPTIX_ROOT}" _local_optix_path_index) -if(NOT _local_optix_path_index EQUAL -1) - message(FATAL_ERROR - "Installed target export contains the build machine's OptiX SDK path: ${_target_export}") -endif() -if(_target_export_contents MATCHES "include/optix") - message(FATAL_ERROR - "Installed target export requires a package-local include/optix directory that is not installed: " - "${_target_export}") -endif() - -set(_consumer_source_dir "${TEST_BINARY_ROOT}/consumer") -set(_consumer_build_dir "${TEST_BINARY_ROOT}/consumer-build") -file(MAKE_DIRECTORY "${_consumer_source_dir}") -file(WRITE "${_consumer_source_dir}/CMakeLists.txt" [=[ -cmake_minimum_required(VERSION 3.15) -project(template_optix_install_consumer LANGUAGES CXX) -find_package(@TEST_PROJECT_NAME@ CONFIG REQUIRED) -add_executable(template_optix_install_consumer main.cpp) -target_link_libraries( - template_optix_install_consumer - PRIVATE @TEST_PROJECT_NAME@::@TEST_PROJECT_NAME@) -]=]) -file(READ "${_consumer_source_dir}/CMakeLists.txt" _consumer_cmake) -string(REPLACE "@TEST_PROJECT_NAME@" "${TEST_PROJECT_NAME}" _consumer_cmake "${_consumer_cmake}") -file(WRITE "${_consumer_source_dir}/CMakeLists.txt" "${_consumer_cmake}") -file(WRITE "${_consumer_source_dir}/main.cpp" [=[ -#include - -int main() -{ - return OPTIX_VERSION > 0 ? 0 : 1; -} -]=]) - -_run_step( - "Configure installed OptiX consumer" - ${CMAKE_COMMAND} - -S "${_consumer_source_dir}" - -B "${_consumer_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_PREFIX_PATH=${_install_prefix} - "-DOPTIX_ROOT=${TEST_OPTIX_ROOT}") -_run_step( - "Build installed OptiX consumer" - ${CMAKE_COMMAND} --build "${_consumer_build_dir}") diff --git a/tests/cmake/VerifyTemplateProjectPythonPackaging.cmake b/tests/cmake/VerifyTemplateProjectPythonPackaging.cmake deleted file mode 100644 index a2ace8f..0000000 --- a/tests/cmake/VerifyTemplateProjectPythonPackaging.cmake +++ /dev/null @@ -1,799 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Prove exact, relocatable wrapper packaging with a self-contained native -# fixture. No gtwrap checkout or network access is required. -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -find_program(_python_executable NAMES python3 REQUIRED) - -# Run fixture commands with captured diagnostics so a failed acceptance step -# identifies both its intent and the underlying tool output. -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -# Require a fixture command to fail for the intended contract violation rather -# than accepting an unrelated configure error as proof of rejection. -function(_run_failure step_name expected_message) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(_result EQUAL 0) - message(FATAL_ERROR - "${step_name} unexpectedly succeeded.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() - - set(_combined_output "${_stdout}\n${_stderr}") - string(FIND "${_combined_output}" "${expected_message}" _message_index) - if(_message_index LESS 0) - message(FATAL_ERROR - "${step_name} failed without the expected diagnostic " - "'${expected_message}'.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_fixture_source "${TEST_BINARY_ROOT}/fixture_source") -set(_fixture_build "${TEST_BINARY_ROOT}/fixture_build") -set(_absolute_libdir_build "${TEST_BINARY_ROOT}/absolute_libdir_build") -set(_runtime_collision_build "${TEST_BINARY_ROOT}/runtime_collision_build") -set(_wrapper_collision_build "${TEST_BINARY_ROOT}/wrapper_collision_build") -set(_soname_collision_build "${TEST_BINARY_ROOT}/soname_collision_build") -set(_generator_output_build "${TEST_BINARY_ROOT}/generator_output_build") -set(_wheel_output "${TEST_BINARY_ROOT}/wheel_output") -set(_wheel_install "${TEST_BINARY_ROOT}/wheel_install") -set(_cmake_install "${TEST_BINARY_ROOT}/cmake_install") -set(_expected_wheel_version "1.0.0.dev0+feature.x.5.gabc1234") -file(MAKE_DIRECTORY - "${_fixture_source}/python/fixture_package" - "${_wheel_output}") - -# Build a two-library runtime chain plus an unrelated library in the same -# scratch build. The Python module calls through both declared runtime targets. -file(WRITE "${_fixture_source}/dependency.c" -"int FixtureDependencyValue(void) -{ - return 41; -} -") -file(WRITE "${_fixture_source}/runtime.c" -"int FixtureDependencyValue(void); - -int FixtureRuntimeValue(void) -{ - return FixtureDependencyValue() + 1; -} -") -file(WRITE "${_fixture_source}/unrelated.c" -"int UnrelatedRuntimeValue(void) -{ - return -1; -} -") -file(WRITE "${_fixture_source}/packaged.c" -"int PackagedRuntimeValue(void) -{ - return 7; -} -") -file(WRITE "${_fixture_source}/module.c" -"#define PY_SSIZE_T_CLEAN -#include - -int FixtureRuntimeValue(void); - -static PyObject *RuntimeValue(PyObject *self, PyObject *args) -{ - (void)self; - (void)args; - return PyLong_FromLong(FixtureRuntimeValue()); -} - -static PyMethodDef FixtureMethods[] = { - {\"Runtime_value\", RuntimeValue, METH_NOARGS, - \"Return the value produced by the packaged runtime chain.\"}, - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef FixtureModule = { - PyModuleDef_HEAD_INIT, - \"fixture_package\", - \"Self-contained wrapper packaging fixture.\", - -1, - FixtureMethods -}; - -PyMODINIT_FUNC PyInit_fixture_package(void) -{ - return PyModule_Create(&FixtureModule); -} -") -file(WRITE "${_fixture_source}/python/fixture_package/__init__.py" -"\"\"\"Self-contained wrapper packaging fixture.\"\"\" - -from .fixture_package import Runtime_value - -__all__ = [\"Runtime_value\"] -") -file(WRITE - "${_fixture_source}/python/fixture_package/stale_checkout_runtime.so.99" - "stale checkout-native artifact\n") - -set(_fixture_cmake_template [=[ -cmake_minimum_required(VERSION 3.15) -project(python_packaging_fixture VERSION 1.0.0 LANGUAGES C) - -include(GNUInstallDirs) -find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter Development) -include("@TEST_TEMPLATE_SOURCE_DIR@/cmake/HandleWrapper.cmake") - -# Export the small C fixture API on DLL platforms without adding -# platform-specific declarations to every generated source file. -if(WIN32) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) -endif() - -add_library(fixture_dependency SHARED dependency.c) -set_target_properties( - fixture_dependency - PROPERTIES - VERSION 2.3.4 - SOVERSION 2) - -add_library(fixture_runtime SHARED runtime.c) -target_link_libraries(fixture_runtime PRIVATE fixture_dependency) -set_target_properties( - fixture_runtime - PROPERTIES - VERSION 1.2.3 - SOVERSION 1) - -add_library(unrelated_runtime SHARED unrelated.c) - -# This declared runtime is intentionally not linked into the extension. Its -# staged copy must still refresh when only this target changes. -add_library(fixture_packaged SHARED packaged.c) -set_target_properties( - fixture_packaged - PROPERTIES - VERSION 3.4.5 - SOVERSION 3) - -# Exercise the flat package-directory collision contract without requiring a -# build that would already have overwritten one of the native artifacts. -option(FIXTURE_RUNTIME_COLLISION "Give two runtime targets identical filenames" OFF) -if(FIXTURE_RUNTIME_COLLISION) - set_target_properties( - fixture_dependency - fixture_packaged - PROPERTIES - OUTPUT_NAME colliding_runtime - VERSION 1.2.3 - SOVERSION 1) - set_target_properties( - fixture_dependency - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/dependency" - RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/dependency") - set_target_properties( - fixture_packaged - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/packaged" - RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/packaged") -endif() - -add_library(fixture_package MODULE module.c) -target_link_libraries( - fixture_package - PRIVATE - fixture_runtime - Python3::Python) -set_target_properties(fixture_package PROPERTIES PREFIX "") -if(WIN32) - set_target_properties(fixture_package PROPERTIES SUFFIX ".pyd") -endif() - -# Additional collision fixtures build into separate directories so only the -# wrapper's flat staging contract can reject their resolved filenames. -set(_additional_runtime_targets) -option(FIXTURE_WRAPPER_COLLISION - "Give a runtime target the wrapper extension filename" OFF) -if(FIXTURE_WRAPPER_COLLISION) - add_library(fixture_wrapper_collision MODULE packaged.c) - set_target_properties( - fixture_wrapper_collision - PROPERTIES - PREFIX "" - OUTPUT_NAME fixture_package - LIBRARY_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/wrapper" - RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/wrapper") - if(WIN32) - set_target_properties(fixture_wrapper_collision PROPERTIES SUFFIX ".pyd") - endif() - list(APPEND _additional_runtime_targets fixture_wrapper_collision) -endif() - -option(FIXTURE_SONAME_COLLISION - "Give one runtime the resolved SONAME filename of another" OFF) -if(FIXTURE_SONAME_COLLISION AND UNIX AND NOT APPLE) - add_library(fixture_soname_owner SHARED packaged.c) - set_target_properties( - fixture_soname_owner - PROPERTIES - OUTPUT_NAME soname_collision - VERSION 2.0.0 - SOVERSION 2 - LIBRARY_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/soname_owner") - - add_library(fixture_target_owner SHARED packaged.c) - set_target_properties( - fixture_target_owner - PROPERTIES - PREFIX "" - OUTPUT_NAME "libsoname_collision.so.2" - SUFFIX "" - NO_SONAME TRUE - LIBRARY_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/collision_outputs/target_owner") - list(APPEND - _additional_runtime_targets - fixture_soname_owner - fixture_target_owner) -endif() - -option(FIXTURE_GENERATOR_OUTPUT_NAME - "Use a configuration-dependent runtime output name" OFF) -if(FIXTURE_GENERATOR_OUTPUT_NAME) - set_target_properties( - fixture_packaged - PROPERTIES - OUTPUT_NAME - "$,fixture_packaged_debug,fixture_packaged_release>") -endif() - -# Treat the generated package root as a build product and preserve the fixture -# source directory as immutable input. -set(_package_source "${CMAKE_CURRENT_SOURCE_DIR}/python") -set(_package_source_dir "${_package_source}/fixture_package") -set(_package_build_root "${CMAKE_CURRENT_BINARY_DIR}/python") -set(_package_build_dir "${_package_build_root}/fixture_package") -_stage_python_package_sources( - "${_package_source_dir}" - "${_package_build_dir}") -set_python_target_properties( - fixture_package - "fixture_package" - "${_package_build_dir}") - -# A full requested version must not leak its patch component into the -# major.minor site-packages directory selected for the resolved interpreter. -set(WRAP_PYTHON_VERSION "${Python3_VERSION}") -set(Python_VERSION_MAJOR "${Python3_VERSION_MAJOR}") -set(Python_VERSION_MINOR "${Python3_VERSION_MINOR}") -_resolve_python_install_root(_python_install_root) -set(_expected_python_install_root - "${CMAKE_INSTALL_LIBDIR}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages") -if(NOT "${_python_install_root}" STREQUAL "${_expected_python_install_root}") - message(FATAL_ERROR - "Resolved Python install root '${_python_install_root}' does not match " - "'${_expected_python_install_root}' for requested version " - "'${WRAP_PYTHON_VERSION}'.") -endif() -set(_package_install_destination - "${_python_install_root}/fixture_package") -configure_python_runtime_artifacts( - fixture_package - "${_package_build_dir}" - "${_package_install_destination}" - "${_package_build_dir}/_wrapper_build.py" - fixture_runtime - fixture_dependency - fixture_packaged - ${_additional_runtime_targets}) - -set(PROJECT_NAME fixture_package) -set(PROJECT_VERSION 1.0.0) -set(PROJECT_VERSION_CORE "1.0.0") -set(PROJECT_VERSION_PRERELEASE "feature.x") -set(PROJECT_VERSION_METADATA "5.gabc1234") -set(FULL_VERSION "1.0.0-feature.x+5.gabc1234") - -# Keep recognized SemVer labels canonical while mapping arbitrary labels to a -# PEP 440 development release that retains the source label as local metadata. -function(_assert_python_package_version PRERELEASE EXPECTED_VERSION) - _compose_python_package_version( - _actual_version - "${PROJECT_VERSION_CORE}" - "${PRERELEASE}" - "${PROJECT_VERSION_METADATA}") - if(NOT _actual_version STREQUAL EXPECTED_VERSION) - message(FATAL_ERROR - "Unexpected package version for '${PRERELEASE}': " - "${_actual_version}; expected ${EXPECTED_VERSION}") - endif() -endfunction() - -_assert_python_package_version("alpha.1" "1.0.0a1+5.gabc1234") -_assert_python_package_version("beta.2" "1.0.0b2+5.gabc1234") -_assert_python_package_version("rc.3" "1.0.0rc3+5.gabc1234") -_assert_python_package_version("dev.4" "1.0.0.dev4+5.gabc1234") - -_compose_python_package_version( - PYTHON_PACKAGE_VERSION - "${PROJECT_VERSION_CORE}" - "${PROJECT_VERSION_PRERELEASE}" - "${PROJECT_VERSION_METADATA}") -if(NOT PYTHON_PACKAGE_VERSION STREQUAL - "@_expected_wheel_version@") - message(FATAL_ERROR - "Unexpected PEP 440 package version: ${PYTHON_PACKAGE_VERSION}") -endif() -configure_file( - "@TEST_TEMPLATE_SOURCE_DIR@/python/pyproject.toml.in" - "${_package_build_root}/pyproject.toml" - @ONLY) -configure_file( - "@TEST_TEMPLATE_SOURCE_DIR@/python/setup.py.in" - "${_package_build_root}/setup.py" - @ONLY) - -install( - TARGETS fixture_package - LIBRARY DESTINATION "${_package_install_destination}" - RUNTIME DESTINATION "${_package_install_destination}") -install( - DIRECTORY "${_package_build_dir}/" - DESTINATION "${_package_install_destination}" - PATTERN "_wrapper_build.py" EXCLUDE - PATTERN "__pycache__" EXCLUDE - PATTERN "*.pyc" EXCLUDE - PATTERN "*.so*" EXCLUDE - PATTERN "*.dylib" EXCLUDE - PATTERN "*.dll" EXCLUDE - PATTERN "*.pyd" EXCLUDE) - -file(WRITE - "${CMAKE_CURRENT_BINARY_DIR}/python_install_root.txt" - "${_python_install_root}") -file(GENERATE - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected_runtime_names.txt" - CONTENT -"$ -$ -$ -$ -$ -$ -") -file(GENERATE - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected_wrapper_name.txt" - CONTENT "$") -file(GENERATE - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/unrelated_name.txt" - CONTENT "$") -file(GENERATE - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/packaged_target_path.txt" - CONTENT "$") -file(GENERATE - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/packaged_staged_path.txt" - CONTENT "${_package_build_dir}/$") -]=]) -string(CONFIGURE - "${_fixture_cmake_template}" - _fixture_cmake - @ONLY) -file(WRITE "${_fixture_source}/CMakeLists.txt" "${_fixture_cmake}") - -# A CMake install destination must remain below the user-selected prefix. -_run_failure( - "Reject an absolute CMake Python library destination" - "CMAKE_INSTALL_LIBDIR must be relative" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_absolute_libdir_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_INSTALL_LIBDIR=${TEST_BINARY_ROOT}/absolute_lib) - -# Runtime collision validation uses the filenames resolved for the active -# configuration and must complete before the flat staging directory is changed. -_run_step( - "Configure colliding Python runtime destinations" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_runtime_collision_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DFIXTURE_RUNTIME_COLLISION=ON) -_run_failure( - "Reject colliding Python runtime destinations before staging" - "Python runtime destination collision" - "${CMAKE_COMMAND}" - --build "${_runtime_collision_build}" - --target fixture_package - --parallel 4) -file(GLOB - _collision_staged_files - "${_runtime_collision_build}/python/fixture_package/*colliding_runtime*") -if(_collision_staged_files) - message(FATAL_ERROR - "Runtime collision staging left partial artifacts: " - "${_collision_staged_files}") -endif() - -# The wrapper module and declared runtimes also share the same flat namespace. -_run_step( - "Configure a runtime that collides with the wrapper extension" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_wrapper_collision_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DFIXTURE_WRAPPER_COLLISION=ON) -_run_failure( - "Reject a runtime that collides with the wrapper extension" - "Python runtime destination collision" - "${CMAKE_COMMAND}" - --build "${_wrapper_collision_build}" - --target fixture_package - --parallel 4) - -if(UNIX AND NOT APPLE) - _run_step( - "Configure a target-file versus SONAME collision" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_soname_collision_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DFIXTURE_SONAME_COLLISION=ON) - _run_failure( - "Reject a target-file versus SONAME collision" - "Python runtime destination collision" - "${CMAKE_COMMAND}" - --build "${_soname_collision_build}" - --target fixture_package - --parallel 4) -endif() - -# Actual active-configuration filenames make generator-expression output names -# safe to validate without approximating CMake's naming rules. -_run_step( - "Configure generator-expression runtime output names" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_generator_output_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DFIXTURE_GENERATOR_OUTPUT_NAME=ON) -_run_step( - "Build generator-expression runtime output names" - "${CMAKE_COMMAND}" - --build "${_generator_output_build}" - --target fixture_package - --parallel 4) - -# Verify exact wheel contents using target-derived names emitted by the fixture -# configure, not platform-specific names duplicated in this verifier. -file(WRITE "${TEST_BINARY_ROOT}/verify_wheel.py" -"from email.parser import Parser -from pathlib import Path -import sys -from zipfile import ZipFile - -wheel_path_ = Path(sys.argv[1]) -expected_names_path_ = Path(sys.argv[2]) -wrapper_name_path_ = Path(sys.argv[3]) -unrelated_name_path_ = Path(sys.argv[4]) -expected_version_ = sys.argv[5] - -expected_runtime_names_ = { - name_.strip() - for name_ in expected_names_path_.read_text().splitlines() - if name_.strip() -} -wrapper_name_ = wrapper_name_path_.read_text().strip() -unrelated_name_ = unrelated_name_path_.read_text().strip() -expected_native_names_ = expected_runtime_names_ | {wrapper_name_} - -with ZipFile(wheel_path_) as wheel_file_: - archive_names_ = set(wheel_file_.namelist()) - metadata_names_ = [ - name_ - for name_ in archive_names_ - if name_.endswith(\".dist-info/METADATA\") - ] - assert len(metadata_names_) == 1, metadata_names_ - metadata_ = Parser().parsestr( - wheel_file_.read(metadata_names_[0]).decode(\"utf-8\") - ) - -packaged_native_names_ = { - Path(name_).name - for name_ in archive_names_ - if name_.startswith(\"fixture_package/\") - and ( - \".so\" in Path(name_).name - or Path(name_).suffix in {\".dylib\", \".dll\", \".pyd\"} - ) -} - -assert packaged_native_names_ == expected_native_names_, ( - packaged_native_names_, - expected_native_names_, -) -assert unrelated_name_ not in packaged_native_names_ -assert not any(name_.endswith(\"_wrapper_build.py\") for name_ in archive_names_) -assert metadata_[\"Version\"] == expected_version_, metadata_[\"Version\"] -print(\"wheel_contents=ok\") -") -file(WRITE "${TEST_BINARY_ROOT}/verify_install.py" -"from pathlib import Path -import sys - -package_dir_ = Path(sys.argv[1]) -expected_names_path_ = Path(sys.argv[2]) -wrapper_name_path_ = Path(sys.argv[3]) -unrelated_name_path_ = Path(sys.argv[4]) - -expected_runtime_names_ = { - name_.strip() - for name_ in expected_names_path_.read_text().splitlines() - if name_.strip() -} -wrapper_name_ = wrapper_name_path_.read_text().strip() -unrelated_name_ = unrelated_name_path_.read_text().strip() - -for expected_name_ in expected_runtime_names_ | {wrapper_name_}: - assert (package_dir_ / expected_name_).is_file(), expected_name_ - -installed_native_names_ = { - path_.name - for path_ in package_dir_.iterdir() - if path_.is_file() - and ( - \".so\" in path_.name - or path_.suffix in {\".dylib\", \".dll\", \".pyd\"} - ) -} -expected_native_names_ = expected_runtime_names_ | {wrapper_name_} -assert installed_native_names_ == expected_native_names_, ( - installed_native_names_, - expected_native_names_, -) -assert not (package_dir_ / unrelated_name_).exists() -assert not (package_dir_ / \"_wrapper_build.py\").exists() -assert not list(package_dir_.glob(\"*.pyc\")) -assert not (package_dir_ / \"__pycache__\").exists() -print(\"cmake_install_contents=ok\") -") - -_run_step( - "Configure self-contained Python packaging fixture" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_fixture_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo) -if(EXISTS - "${_fixture_build}/python/fixture_package/stale_checkout_runtime.so.99") - message(FATAL_ERROR - "Configure staged a stale checkout-native package artifact.") -endif() -_run_step( - "Build self-contained Python packaging fixture" - "${CMAKE_COMMAND}" - --build "${_fixture_build}" - --parallel 4) - -# A configured package directory is a disposable build product. Reconfigure -# after injecting an undeclared file and require the package to be reconstructed -# exclusively from its source inputs. -set(_stale_package_marker - "${_fixture_build}/python/fixture_package/stale_review_marker.py") -file(WRITE "${_stale_package_marker}" "raise RuntimeError('stale package file')\n") -_run_step( - "Reconfigure fixture with a stale build-package file" - "${CMAKE_COMMAND}" - -S "${_fixture_source}" - -B "${_fixture_build}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo) -if(EXISTS "${_stale_package_marker}") - message(FATAL_ERROR - "Reconfigure retained undeclared Python package file: " - "${_stale_package_marker}") -endif() - -# Configuration and build must not add packaging outputs beside the source -# package used to seed the disposable fixture. -file(GLOB_RECURSE - _source_python_files - LIST_DIRECTORIES FALSE - RELATIVE "${_fixture_source}/python" - "${_fixture_source}/python/*") -set(_expected_source_python_files - "fixture_package/__init__.py;fixture_package/stale_checkout_runtime.so.99") -if(NOT "${_source_python_files}" STREQUAL "${_expected_source_python_files}") - message(FATAL_ERROR - "Python packaging mutated fixture sources: ${_source_python_files}") -endif() - -# Rebuild an unlinked declared runtime through the wrapper target. Its staged -# file must refresh even though the extension itself does not need to relink. -file(WRITE "${_fixture_source}/packaged.c" -"int PackagedRuntimeValue(void) -{ - return 8; -} -") -_run_step( - "Refresh an incrementally rebuilt declared runtime" - "${CMAKE_COMMAND}" - --build "${_fixture_build}" - --target fixture_package - --parallel 4) -file(READ "${_fixture_build}/packaged_target_path.txt" _packaged_target_path) -file(READ "${_fixture_build}/packaged_staged_path.txt" _packaged_staged_path) -string(STRIP "${_packaged_target_path}" _packaged_target_path) -string(STRIP "${_packaged_staged_path}" _packaged_staged_path) -_run_step( - "Compare refreshed target and staged runtime" - "${CMAKE_COMMAND}" -E compare_files - "${_packaged_target_path}" - "${_packaged_staged_path}") - -_run_step( - "Build isolated fixture wheel" - "${_python_executable}" - -m pip wheel - "${_fixture_build}/python" - --no-build-isolation - --no-deps - --wheel-dir "${_wheel_output}") -file(GLOB _wheel_paths "${_wheel_output}/fixture_package-*.whl") -list(LENGTH _wheel_paths _wheel_count) -if(NOT _wheel_count EQUAL 1) - message(FATAL_ERROR - "Expected one fixture wheel, found ${_wheel_count}: ${_wheel_paths}") -endif() -list(GET _wheel_paths 0 _wheel_path) - -_run_step( - "Verify exact fixture wheel contents" - "${_python_executable}" - "${TEST_BINARY_ROOT}/verify_wheel.py" - "${_wheel_path}" - "${_fixture_build}/expected_runtime_names.txt" - "${_fixture_build}/expected_wrapper_name.txt" - "${_fixture_build}/unrelated_name.txt" - "${_expected_wheel_version}") -_run_step( - "Install fixture wheel into isolated target" - "${_python_executable}" - -m pip install - --no-deps - --target "${_wheel_install}" - "${_wheel_path}") -_run_step( - "Import isolated fixture wheel" - "${CMAKE_COMMAND}" -E env - "PYTHONPATH=${_wheel_install}" - "LD_LIBRARY_PATH=" - "${_python_executable}" -c - "import fixture_package; assert fixture_package.Runtime_value() == 42") - -_run_step( - "Install fixture through CMake prefix" - "${CMAKE_COMMAND}" - --install "${_fixture_build}" - --prefix "${_cmake_install}") -file(READ "${_fixture_build}/python_install_root.txt" _python_install_root) -string(STRIP "${_python_install_root}" _python_install_root) -set(_cmake_package_dir - "${_cmake_install}/${_python_install_root}/fixture_package") -_run_step( - "Verify exact CMake install contents" - "${_python_executable}" - "${TEST_BINARY_ROOT}/verify_install.py" - "${_cmake_package_dir}" - "${_fixture_build}/expected_runtime_names.txt" - "${_fixture_build}/expected_wrapper_name.txt" - "${_fixture_build}/unrelated_name.txt") -_run_step( - "Import isolated CMake-installed fixture" - "${CMAKE_COMMAND}" -E env - "PYTHONPATH=${_cmake_install}/${_python_install_root}" - "LD_LIBRARY_PATH=" - "${_python_executable}" -c - "import fixture_package; assert fixture_package.Runtime_value() == 42") - -# Import success proves the loader chain is viable. On supported host tools, -# also reject embedded absolute scratch paths directly. -file(READ "${_fixture_build}/expected_wrapper_name.txt" _wrapper_name) -string(STRIP "${_wrapper_name}" _wrapper_name) -set(_installed_wrapper "${_cmake_package_dir}/${_wrapper_name}") -file( - STRINGS - "${_fixture_build}/expected_runtime_names.txt" - _runtime_names) -set(_installed_native_artifacts "${_installed_wrapper}") -foreach(_runtime_name IN LISTS _runtime_names) - if(NOT "${_runtime_name}" STREQUAL "") - list(APPEND - _installed_native_artifacts - "${_cmake_package_dir}/${_runtime_name}") - endif() -endforeach() -list(REMOVE_DUPLICATES _installed_native_artifacts) - -if(UNIX AND NOT APPLE) - find_program(_readelf_executable NAMES readelf REQUIRED) - foreach(_installed_native_artifact IN LISTS _installed_native_artifacts) - execute_process( - COMMAND "${_readelf_executable}" -d "${_installed_native_artifact}" - RESULT_VARIABLE _readelf_result - OUTPUT_VARIABLE _readelf_output - ERROR_VARIABLE _readelf_stderr) - if(NOT _readelf_result EQUAL 0) - message(FATAL_ERROR - "readelf failed for ${_installed_native_artifact}: " - "${_readelf_stderr}") - endif() - if(NOT _readelf_output MATCHES "\\$ORIGIN") - message(FATAL_ERROR - "Installed native artifact has no loader-relative RUNPATH: " - "${_installed_native_artifact}") - endif() - string(FIND "${_readelf_output}" "${TEST_BINARY_ROOT}" _scratch_rpath_index) - if(NOT _scratch_rpath_index EQUAL -1) - message(FATAL_ERROR - "Installed native artifact retains scratch path: " - "${_installed_native_artifact}\n${_readelf_output}") - endif() - endforeach() -elseif(APPLE) - find_program(_otool_executable NAMES otool REQUIRED) - foreach(_installed_native_artifact IN LISTS _installed_native_artifacts) - execute_process( - COMMAND "${_otool_executable}" -l "${_installed_native_artifact}" - RESULT_VARIABLE _otool_result - OUTPUT_VARIABLE _otool_output - ERROR_VARIABLE _otool_stderr) - if(NOT _otool_result EQUAL 0) - message(FATAL_ERROR - "otool failed for ${_installed_native_artifact}: ${_otool_stderr}") - endif() - if(NOT _otool_output MATCHES "@loader_path") - message(FATAL_ERROR - "Installed native artifact has no loader-relative RPATH: " - "${_installed_native_artifact}") - endif() - string(FIND "${_otool_output}" "${TEST_BINARY_ROOT}" _scratch_rpath_index) - if(NOT _scratch_rpath_index EQUAL -1) - message(FATAL_ERROR - "Installed native artifact retains scratch path: " - "${_installed_native_artifact}\n${_otool_output}") - endif() - endforeach() -endif() diff --git a/tests/cmake/VerifyTemplateProjectPythonTestOptions.cmake b/tests/cmake/VerifyTemplateProjectPythonTestOptions.cmake deleted file mode 100644 index 351fa4a..0000000 --- a/tests/cmake/VerifyTemplateProjectPythonTestOptions.cmake +++ /dev/null @@ -1,84 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -function(_configure_expect_success case_name) - set(_build_dir "${TEST_BINARY_ROOT}/${case_name}") - execute_process( - COMMAND ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DENABLE_TBB=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "Expected configure '${case_name}' to pass, but it failed with ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_configure_expect_failure case_name) - set(_build_dir "${TEST_BINARY_ROOT}/${case_name}") - execute_process( - COMMAND ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_build_dir}" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DENABLE_TBB=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(_result EQUAL 0) - message(FATAL_ERROR "Expected configure '${case_name}' to fail, but it passed.") - endif() - -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -_configure_expect_success( - tests_disabled_ignores_python_options - -DENABLE_TESTS=OFF - -DPYTHON_TEST_RUNNER=unsupported - -DPYTHON_TEST_CONDA_ENV=env_name - -DPYTHON_TEST_CONDA_PREFIX="${TEST_BINARY_ROOT}/fake_prefix") - -_configure_expect_success( - python_tests_disabled_ignores_python_options - -DENABLE_TESTS=ON - -DENABLE_PYTHON_TESTS=OFF - -DPYTHON_TEST_RUNNER=unsupported - -DPYTHON_TEST_CONDA_ENV=env_name - -DPYTHON_TEST_CONDA_PREFIX="${TEST_BINARY_ROOT}/fake_prefix") - -_configure_expect_failure( - python_tests_enabled_rejects_conflicting_conda_options - -DENABLE_TESTS=ON - -DENABLE_PYTHON_TESTS=ON - -DPYTHON_TEST_CONDA_ENV=env_name - -DPYTHON_TEST_CONDA_PREFIX="${TEST_BINARY_ROOT}/fake_prefix") diff --git a/tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake b/tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake deleted file mode 100644 index 92edd33..0000000 --- a/tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake +++ /dev/null @@ -1,435 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Exercise tag-safe metadata synchronization and canonical source packaging in -# a disposable clone without mutating the repository under test. -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -find_program(_git_executable NAMES git REQUIRED) -find_program(_bash_executable NAMES bash REQUIRED) -find_program(_cpack_executable NAMES cpack REQUIRED) -find_program(_python_executable NAMES python3 REQUIRED) - -set(_synthetic_version "99.98.97") -set(_synthetic_tag "v${_synthetic_version}") -set(_scratch_root "${TEST_BINARY_ROOT}/build_parent/release_clone") -set(_scratch_verifier "${_scratch_root}/tests/cmake/VerifyTemplateProjectRos2Overlay.cmake") -set(_source_release_verifier - "${TEST_TEMPLATE_SOURCE_DIR}/tests/cmake/VerifySourceReleaseArchive.cmake") -set(_manifest_paths - "ros2/template_project/package.xml" - "ros2/template_project_interfaces/package.xml" - "ros2/template_project_ros/package.xml" - "ros2/template_project_spinup/package.xml") - -function(_run_success step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() - set(_last_stdout "${_stdout}" PARENT_SCOPE) -endfunction() - -function(_run_failure step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(_result EQUAL 0) - message(FATAL_ERROR - "${step_name} unexpectedly succeeded.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_read_xml_version manifest_path out_var) - execute_process( - COMMAND "${_python_executable}" -c - "import sys, xml.etree.ElementTree as ET; value=ET.parse(sys.argv[1]).getroot().findtext('version'); assert value; print(value)" - "${manifest_path}" - RESULT_VARIABLE _parse_result - OUTPUT_VARIABLE _version - ERROR_VARIABLE _parse_stderr - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT _parse_result EQUAL 0) - message(FATAL_ERROR - "Could not parse manifest version from ${manifest_path}: ${_parse_stderr}") - endif() - set(${out_var} "${_version}" PARENT_SCOPE) -endfunction() - -execute_process( - COMMAND "${_git_executable}" -C "${TEST_TEMPLATE_SOURCE_DIR}" show-ref --tags - RESULT_VARIABLE _source_tags_result - OUTPUT_VARIABLE _source_tags_before - ERROR_VARIABLE _source_tags_stderr) -if(NOT _source_tags_result EQUAL 0 AND NOT _source_tags_result EQUAL 1) - message(FATAL_ERROR "Could not capture source tags: ${_source_tags_stderr}") -endif() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") -_run_success( - "Create local scratch clone" - "${CMAKE_COMMAND}" -E env GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0 - "${_git_executable}" clone --no-local "${TEST_TEMPLATE_SOURCE_DIR}" "${_scratch_root}") -_run_success("Remove scratch remote" "${_git_executable}" -C "${_scratch_root}" remote remove origin) -_run_success("Configure scratch Git name" "${_git_executable}" -C "${_scratch_root}" config user.name "Template Release Test") -_run_success("Configure scratch Git email" "${_git_executable}" -C "${_scratch_root}" config user.email "release-test@example.invalid") -_run_success("Disable scratch commit signing" "${_git_executable}" -C "${_scratch_root}" config commit.gpgSign false) -_run_success("Disable scratch tag signing" "${_git_executable}" -C "${_scratch_root}" config tag.gpgSign false) - -execute_process( - COMMAND "${_git_executable}" -C "${TEST_TEMPLATE_SOURCE_DIR}" diff --binary HEAD - RESULT_VARIABLE _source_diff_result - OUTPUT_VARIABLE _source_diff - ERROR_VARIABLE _source_diff_stderr) -if(NOT _source_diff_result EQUAL 0) - message(FATAL_ERROR "Could not capture source working-tree diff: ${_source_diff_stderr}") -endif() -if(NOT _source_diff STREQUAL "") - set(_source_patch "${TEST_BINARY_ROOT}/source_worktree.patch") - file(WRITE "${_source_patch}" "${_source_diff}") - _run_success( - "Apply source working-tree diff to scratch clone" - "${_git_executable}" -C "${_scratch_root}" apply --whitespace=nowarn "${_source_patch}") -endif() - -_run_success( - "List untracked source files" - "${_git_executable}" -C "${TEST_TEMPLATE_SOURCE_DIR}" ls-files --others --exclude-standard) -string(REPLACE "\r\n" "\n" _untracked_files "${_last_stdout}") -string(REPLACE "\n" ";" _untracked_files "${_untracked_files}") -list(FILTER _untracked_files EXCLUDE REGEX "^$") -foreach(_untracked_file IN LISTS _untracked_files) - set(_untracked_source "${TEST_TEMPLATE_SOURCE_DIR}/${_untracked_file}") - # Git reports an untracked embedded repository, such as a fetched dependency, - # as one directory entry. It is build state, not part of the source snapshot. - if(IS_DIRECTORY "${_untracked_source}") - continue() - endif() - get_filename_component(_untracked_parent "${_scratch_root}/${_untracked_file}" DIRECTORY) - file(MAKE_DIRECTORY "${_untracked_parent}") - configure_file( - "${_untracked_source}" - "${_scratch_root}/${_untracked_file}" - COPYONLY) -endforeach() -_run_success("Stage source snapshot" "${_git_executable}" -C "${_scratch_root}" add -A) -_run_success( - "Commit source snapshot under test" - "${_git_executable}" -C "${_scratch_root}" commit --allow-empty -m "Snapshot source under test") -_run_success( - "Create synthetic release-preparation commit" - "${_git_executable}" -C "${_scratch_root}" commit --allow-empty -m "Start synthetic release preparation") - -_read_xml_version( - "${_scratch_root}/ros2/template_project/package.xml" - _baseline_version) - -_run_success( - "Create temporary local lightweight release tag" - "${_git_executable}" -C "${_scratch_root}" tag --no-sign "${_synthetic_tag}") -_run_success( - "List tags on synthetic release-preparation commit" - "${_git_executable}" -C "${_scratch_root}" tag --points-at HEAD) -string(STRIP "${_last_stdout}" _preparation_tags) -if(NOT _preparation_tags STREQUAL "${_synthetic_tag}") - message(FATAL_ERROR - "Synthetic release-preparation commit must have only ${_synthetic_tag}; got '${_preparation_tags}'") -endif() -_run_failure( - "Reject stale manifests at the temporary release tag" - "${CMAKE_COMMAND}" - -DTEST_TEMPLATE_SOURCE_DIR=${_scratch_root} - -DTEST_BINARY_ROOT=${TEST_BINARY_ROOT}/stale_overlay - -DEXPECTED_VERSION=${_synthetic_version} - -P "${_scratch_verifier}") - -_run_success( - "Synchronize manifests from the temporary local tag" - "${CMAKE_COMMAND}" -E env GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0 - "${_bash_executable}" "${_scratch_root}/generate_version.sh" --sync-ros2) -_run_success( - "Delete temporary local tag" - "${_git_executable}" -C "${_scratch_root}" tag -d "${_synthetic_tag}") - -_run_success( - "List synchronized files" - "${_git_executable}" -C "${_scratch_root}" diff --name-only) -string(REPLACE "\r\n" "\n" _changed_files "${_last_stdout}") -string(REPLACE "\n" ";" _changed_files "${_changed_files}") -list(FILTER _changed_files EXCLUDE REGEX "^$") -list(SORT _changed_files) -set(_expected_changed_files ${_manifest_paths}) -list(SORT _expected_changed_files) -if(NOT _changed_files STREQUAL _expected_changed_files) - message(FATAL_ERROR - "Metadata sync must modify exactly the four ROS manifests.\n" - "Expected: ${_expected_changed_files}\n" - "Actual: ${_changed_files}") -endif() - -_run_success("Stage synchronized manifests" "${_git_executable}" -C "${_scratch_root}" add -- ${_manifest_paths}) -_run_success( - "Commit synchronized manifests" - "${_git_executable}" -C "${_scratch_root}" commit -m "Prepare synthetic ROS release metadata") - -_run_failure( - "Reject unpublished synchronized commit before final tag" - "${CMAKE_COMMAND}" - -DTEST_TEMPLATE_SOURCE_DIR=${_scratch_root} - -DTEST_BINARY_ROOT=${TEST_BINARY_ROOT}/untagged_overlay - -DEXPECTED_VERSION=${_baseline_version} - -P "${_scratch_verifier}") - -_run_success( - "Create final annotated release tag" - "${_git_executable}" -C "${_scratch_root}" tag -a "${_synthetic_tag}" -m "Synthetic release ${_synthetic_version}") -_run_success( - "Synchronize exact-tag release metadata" - "${CMAKE_COMMAND}" -E env GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0 - "${_bash_executable}" "${_scratch_root}/generate_version.sh" --sync-ros2) -_run_success( - "Confirm exact-tag synchronization left manifests clean" - "${_git_executable}" -C "${_scratch_root}" diff --exit-code -- ${_manifest_paths}) - -foreach(_manifest_path IN LISTS _manifest_paths) - _run_success( - "Read ${_manifest_path} from final tag" - "${_git_executable}" -C "${_scratch_root}" show "${_synthetic_tag}:${_manifest_path}") - set(_tag_manifest "${TEST_BINARY_ROOT}/tag_manifest.xml") - file(WRITE "${_tag_manifest}" "${_last_stdout}") - _read_xml_version("${_tag_manifest}" _tag_manifest_version) - if(NOT _tag_manifest_version STREQUAL _synthetic_version) - message(FATAL_ERROR - "Final tag has ${_tag_manifest_version}, not ${_synthetic_version}, in ${_manifest_path}") - endif() -endforeach() - -_run_success( - "Validate final tagged ROS overlay" - "${CMAKE_COMMAND}" - -DTEST_TEMPLATE_SOURCE_DIR=${_scratch_root} - -DTEST_BINARY_ROOT=${TEST_BINARY_ROOT}/final_overlay - -DEXPECTED_VERSION=${_synthetic_version} - -P "${_scratch_verifier}") - -set(_release_build "${_scratch_root}/generated/current_output") -set(_archive_output "${TEST_BINARY_ROOT}/archive_output") -set(_archive_extract "${TEST_BINARY_ROOT}/archive_extract") - -# Contrast legitimate build-prefixed sources with cache-owned generated trees -# so source packaging depends on ownership evidence rather than path names. -file(MAKE_DIRECTORY - "${_scratch_root}/build_assets" - "${_scratch_root}/build_release_sentinel" - "${_scratch_root}/build_transient" - "${_scratch_root}/examples/build_release_sentinel" - "${_scratch_root}/examples/foreign_build" - "${_scratch_root}/examples/install" - "${_scratch_root}/tools/build_helpers" - "${_scratch_root}/ros2/build/generated" - "${_scratch_root}/ros2/install/generated" - "${_scratch_root}/ros2/log/generated" - "${_archive_output}" - "${_archive_extract}") -file(WRITE - "${_scratch_root}/build_assets/must_ship.txt" - "legitimate build-prefixed source\n") -file(WRITE "${_scratch_root}/build_release_sentinel/must_not_ship.txt" "generated build output\n") -file(WRITE - "${_scratch_root}/build_release_sentinel/CMakeCache.txt" - "CMAKE_HOME_DIRECTORY:INTERNAL=${_scratch_root}\n") -file(CREATE_LINK - "${_scratch_root}/missing-transient-cache" - "${_scratch_root}/build_transient/CMakeCache.txt" - SYMBOLIC - RESULT _transient_cache_link_result) -if(NOT _transient_cache_link_result STREQUAL "0") - message(FATAL_ERROR - "Could not create transient-cache regression fixture: " - "${_transient_cache_link_result}") -endif() -file(WRITE - "${_scratch_root}/examples/build_release_sentinel/must_not_ship.txt" - "generated nested build output\n") -file(WRITE - "${_scratch_root}/examples/build_release_sentinel/CMakeCache.txt" - "CMAKE_HOME_DIRECTORY:INTERNAL=${_scratch_root}\n") -file(WRITE - "${_scratch_root}/examples/foreign_build/CMakeCache.txt" - "CMAKE_HOME_DIRECTORY:INTERNAL=${TEST_BINARY_ROOT}/foreign_source\n") -file(WRITE - "${_scratch_root}/examples/foreign_build/must_ship.txt" - "foreign child-project cache\n") -file(WRITE - "${_scratch_root}/examples/install/must_ship.txt" - "legitimate nested install source\n") -file(WRITE - "${_scratch_root}/tools/build_helpers/must_ship.txt" - "legitimate nested source\n") -file(WRITE "${_scratch_root}/ros2/build/generated/must_not_ship.txt" "generated ROS build output\n") -file(WRITE "${_scratch_root}/ros2/install/generated/must_not_ship.txt" "generated ROS install output\n") -file(WRITE "${_scratch_root}/ros2/log/generated/must_not_ship.txt" "generated ROS log output\n") - -_run_success( - "Configure full exact-tag source release" - "${CMAKE_COMMAND}" - -S "${_scratch_root}" - -B "${_release_build}" - -DCMAKE_BUILD_TYPE=Release - -DENABLE_TESTS=OFF - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) - -# Create another checkout-owned build after the release tree was configured. -# CPack must refresh ownership at package time rather than preserving a stale -# configure-time inventory. -set(_late_owned_build "${_scratch_root}/build_late_sentinel") -file(MAKE_DIRECTORY "${_late_owned_build}") -file(WRITE - "${_late_owned_build}/CMakeCache.txt" - "CMAKE_HOME_DIRECTORY:INTERNAL=${_scratch_root}\n") -file(WRITE - "${_late_owned_build}/must_not_ship.txt" - "late generated build output\n") - -# A source-tree VERSION is only a fallback input. Make it stale after configure -# so the archive must retain the exact metadata generated in the build tree. -file(WRITE - "${_scratch_root}/VERSION" - "Project version: 1.2.3\n" - "Project version core: 1.2.3\n" - "Project version prerelease: stale\n" - "Project version metadata: source\n" - "Full version: 1.2.3-stale+source\n") -_run_success( - "Create canonical CPack source TGZ" - "${CMAKE_COMMAND}" -E chdir "${_archive_output}" - "${_cpack_executable}" --config "${_release_build}/CPackSourceConfig.cmake") - -file(GLOB _source_archives "${_archive_output}/template_project-${_synthetic_version}.tar.gz") -list(LENGTH _source_archives _source_archive_count) -if(NOT _source_archive_count EQUAL 1) - message(FATAL_ERROR - "Expected one canonical source archive, found ${_source_archive_count}: ${_source_archives}") -endif() -list(GET _source_archives 0 _source_archive) -_run_success( - "Extract canonical source TGZ outside Git" - "${CMAKE_COMMAND}" -E chdir "${_archive_extract}" - "${CMAKE_COMMAND}" -E tar xzf "${_source_archive}") - -file(GLOB _extracted_entries LIST_DIRECTORIES TRUE "${_archive_extract}/*") -set(_extracted_roots) -foreach(_extracted_entry IN LISTS _extracted_entries) - if(IS_DIRECTORY "${_extracted_entry}") - list(APPEND _extracted_roots "${_extracted_entry}") - endif() -endforeach() -list(LENGTH _extracted_roots _extracted_root_count) -if(NOT _extracted_root_count EQUAL 1) - message(FATAL_ERROR - "Expected one extracted source root, found ${_extracted_root_count}: ${_extracted_roots}") -endif() -list(GET _extracted_roots 0 _extracted_root) - -# Generated trees are identified by ownership evidence, not by broad directory -# names that can also describe legitimate project sources. -if(EXISTS "${_extracted_root}/examples/build_release_sentinel") - message(FATAL_ERROR - "Canonical source archive contains a nested generated build tree") -endif() -if(EXISTS "${_extracted_root}/generated/current_output") - message(FATAL_ERROR - "Canonical source archive contains the active nested binary tree") -endif() -if(EXISTS "${_extracted_root}/build_late_sentinel") - message(FATAL_ERROR - "Canonical source archive contains a build created after configure") -endif() -if(NOT EXISTS "${_extracted_root}/build_assets/must_ship.txt") - message(FATAL_ERROR - "Canonical source archive omitted legitimate build-prefixed source content") -endif() -if(NOT EXISTS "${_extracted_root}/examples/foreign_build/must_ship.txt") - message(FATAL_ERROR - "Canonical source archive omitted a foreign child-project cache") -endif() -if(NOT EXISTS "${_extracted_root}/examples/install/must_ship.txt") - message(FATAL_ERROR - "Canonical source archive omitted legitimate nested install content") -endif() -if(NOT EXISTS "${_extracted_root}/tools/build_helpers/must_ship.txt") - message(FATAL_ERROR - "Canonical source archive omitted legitimate nested source content") -endif() - -_run_success( - "Validate extracted no-Git canonical source" - "${CMAKE_COMMAND}" - -DTEST_SOURCE_ROOT=${_extracted_root} - -DTEST_BINARY_ROOT=${TEST_BINARY_ROOT}/archive_validation - -DEXPECTED_VERSION=${_synthetic_version} - -DEXPECTED_FULL_VERSION=${_synthetic_version} - -DTEST_ROS_STATIC_VERIFIER=${_extracted_root}/tests/cmake/VerifyTemplateProjectRos2Overlay.cmake - -P "${_source_release_verifier}") - -set(_missing_version_root "${TEST_BINARY_ROOT}/missing_version_source") -file(MAKE_DIRECTORY "${_missing_version_root}") -file(COPY "${_extracted_root}/" DESTINATION "${_missing_version_root}") -file(REMOVE "${_missing_version_root}/VERSION") -_run_failure( - "Reject source archive without VERSION" - "${CMAKE_COMMAND}" - -DTEST_SOURCE_ROOT=${_missing_version_root} - -DTEST_BINARY_ROOT=${TEST_BINARY_ROOT}/missing_version_validation - -DEXPECTED_VERSION=${_synthetic_version} - -DEXPECTED_FULL_VERSION=${_synthetic_version} - -P "${_source_release_verifier}") - -file(REMOVE_RECURSE - "${_scratch_root}/build_assets" - "${_scratch_root}/build_late_sentinel" - "${_scratch_root}/build_release_sentinel" - "${_scratch_root}/build_transient" - "${_scratch_root}/examples/build_release_sentinel" - "${_scratch_root}/examples/foreign_build" - "${_scratch_root}/examples/install" - "${_scratch_root}/generated" - "${_scratch_root}/tools" - "${_scratch_root}/ros2/build" - "${_scratch_root}/ros2/install" - "${_scratch_root}/ros2/log") -_run_success("Check final scratch status" "${_git_executable}" -C "${_scratch_root}" status --porcelain) -if(NOT _last_stdout STREQUAL "") - message(FATAL_ERROR "Final tagged scratch clone is dirty:\n${_last_stdout}") -endif() - -execute_process( - COMMAND "${_git_executable}" -C "${TEST_TEMPLATE_SOURCE_DIR}" show-ref --tags - RESULT_VARIABLE _source_tags_after_result - OUTPUT_VARIABLE _source_tags_after - ERROR_VARIABLE _source_tags_after_stderr) -if(NOT _source_tags_after_result EQUAL 0 AND NOT _source_tags_after_result EQUAL 1) - message(FATAL_ERROR "Could not recapture source tags: ${_source_tags_after_stderr}") -endif() -if(NOT _source_tags_before STREQUAL _source_tags_after) - message(FATAL_ERROR "Release regression changed tags in the source repository") -endif() diff --git a/tests/cmake/VerifyTemplateProjectRos2Overlay.cmake b/tests/cmake/VerifyTemplateProjectRos2Overlay.cmake deleted file mode 100644 index f4f7895..0000000 --- a/tests/cmake/VerifyTemplateProjectRos2Overlay.cmake +++ /dev/null @@ -1,493 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT EXPECTED_VERSION) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -# EXPECTED_VERSION is generated release metadata whose strict representation is -# part of the release contract, so a syntax regex is appropriate here. -if(NOT EXPECTED_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") - message(FATAL_ERROR "EXPECTED_VERSION must be strict X.Y.Z, got '${EXPECTED_VERSION}'") -endif() - -set(_root "${TEST_TEMPLATE_SOURCE_DIR}") - -function(_require_path relative_path) - if(NOT EXISTS "${_root}/${relative_path}") - message(FATAL_ERROR "Missing ROS 2 overlay path: ${relative_path}") - endif() -endfunction() - -function(_read_required file_path out_var) - if(NOT EXISTS "${file_path}") - message(FATAL_ERROR "Required generated fixture not found: ${file_path}") - endif() - file(READ "${file_path}" _contents) - set(${out_var} "${_contents}" PARENT_SCOPE) -endfunction() - -function(_read_cache_value cache_path cache_key out_var) - file(STRINGS "${cache_path}" _cache_lines REGEX "^${cache_key}:") - list(LENGTH _cache_lines _cache_line_count) - if(NOT _cache_line_count EQUAL 1) - message(FATAL_ERROR "Missing generated CMake cache field: ${cache_key}") - endif() - list(GET _cache_lines 0 _cache_line) - string(REGEX REPLACE "^[^=]*=" "" _cache_value "${_cache_line}") - if(_cache_value STREQUAL "") - message(FATAL_ERROR "Empty generated CMake cache field: ${cache_key}") - endif() - set(${out_var} "${_cache_value}" PARENT_SCOPE) -endfunction() - -function(_run_success step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() - set(_last_stdout "${_stdout}" PARENT_SCOPE) - set(_last_stderr "${_stderr}" PARENT_SCOPE) -endfunction() - -function(_run_failure step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(_result EQUAL 0) - message(FATAL_ERROR - "${step_name} unexpectedly succeeded.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_assert_ros2_fence relative_path) - set(_doc_path "${_root}/${relative_path}") - _read_required("${_doc_path}" _contents) - # These exact comments are generator input consumed by --remove-ros2. - string(FIND "${_contents}" "" _begin_index) - string(FIND "${_contents}" "" _end_index) - if(_begin_index LESS 0 OR _end_index LESS 0 OR _begin_index GREATER _end_index) - message(FATAL_ERROR "Missing or malformed ROS 2 overlay fence in ${relative_path}") - endif() -endfunction() - -function(_create_fake_target fake_root project_name) - file(REMOVE_RECURSE "${fake_root}") - file(MAKE_DIRECTORY - "${fake_root}/.github/workflows" - "${fake_root}/doc" - "${fake_root}/examples" - "${fake_root}/lib" - "${fake_root}/python" - "${fake_root}/tests") - file(WRITE "${fake_root}/build_lib.sh" "#!/usr/bin/env bash\n") - file(WRITE "${fake_root}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -set(project_name \"${project_name}\") -set(project_description \"Derived ${project_name} project\") -set(project_homepage_url \"https://example.test/${project_name}\") -set(PROJECT_MAINTAINER_NAME \"Derived Maintainer\" CACHE STRING \"\") -set(PROJECT_MAINTAINER_EMAIL \"maintainer@example.test\" CACHE STRING \"\") -set(PROJECT_LICENSE \"Apache-2.0\" CACHE STRING \"\") -project(\${project_name} - VERSION 2.3.4 - DESCRIPTION \"\${project_description}\" - HOMEPAGE_URL \"\${project_homepage_url}\" - LANGUAGES NONE) -") - configure_file( - "${_root}/generate_version.sh" - "${fake_root}/generate_version.sh" - COPYONLY) - file(WRITE "${fake_root}/VERSION" -"Project version: 2.3.4 -Project version core: 2.3.4 -Project version prerelease: -Project version metadata: -Full version: 2.3.4 -") -endfunction() - -foreach(_required_path - "CMakeLists.txt" - ".github/workflows/build_ros2_overlay.yml" - ".github/workflows/build_ros2_overlay.yml.tpl" - "build_ros2.sh" - "add_ros2_support.sh" - "tailor_template_cleanup.sh" - "generate_version.sh" - "doc/ros2_overlay.md" - "ros2/tools/sync_package_metadata.py" - "tests/template_test/testRos2OverlayStatic.py" - "ros2/template_project/package.xml" - "ros2/template_project_interfaces/package.xml" - "ros2/template_project_ros/package.xml" - "ros2/template_project_spinup/package.xml") - _require_path("${_required_path}") -endforeach() - -find_program(_bash_executable NAMES bash REQUIRED) -find_program(_python_executable NAMES python3 REQUIRED) - -foreach(_script - "build_ros2.sh" - "add_ros2_support.sh" - "tailor_template_cleanup.sh" - "generate_version.sh") - _run_success( - "Validate ${_script} syntax" - "${_bash_executable}" -n "${_root}/${_script}") -endforeach() - -foreach(_marker - "python/COLCON_IGNORE" - "lib/COLCON_IGNORE" - "examples/COLCON_IGNORE" - "tests/COLCON_IGNORE") - _require_path("${_marker}") -endforeach() - -foreach(_fenced_doc - "README.md" - "AGENTS.md" - "CLAUDE.md" - "doc/bootstrap_prompts.md" - "doc/template_usage.md" - "doc/versioning.md") - _assert_ros2_fence("${_fenced_doc}") -endforeach() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_metadata_probe "${TEST_BINARY_ROOT}/metadata_probe") -_run_success( - "Configure root project metadata without build languages" - "${CMAKE_COMMAND}" -S "${_root}" -B "${_metadata_probe}" - -DPROJECT_METADATA_ONLY=ON) -set(_metadata_cache_path "${_metadata_probe}/CMakeCache.txt") -foreach(_cache_key - CMAKE_PROJECT_DESCRIPTION - CMAKE_PROJECT_HOMEPAGE_URL - PROJECT_MAINTAINER_NAME - PROJECT_MAINTAINER_EMAIL - PROJECT_LICENSE) - _read_cache_value("${_metadata_cache_path}" "${_cache_key}" _cache_value) -endforeach() -file(STRINGS "${_metadata_cache_path}" _metadata_cxx_compiler - REGEX "^CMAKE_CXX_COMPILER:") -if(_metadata_cxx_compiler) - message(FATAL_ERROR "Metadata-only configure unexpectedly enabled C++.") -endif() -if(EXISTS "${_metadata_probe}/src") - message(FATAL_ERROR "Metadata-only configure unexpectedly entered src/.") -endif() - -# Configure the overlay shim without compilers to verify that its stable facade -# forwards directly to the canonical root options used by nested consumers. -set(_ros2_facade_probe "${TEST_BINARY_ROOT}/ros2_facade_probe") -_run_success( - "Enable ROS 2 shim GPU facades in metadata-only mode" - "${CMAKE_COMMAND}" - -S "${_root}/ros2/template_project" - -B "${_ros2_facade_probe}" - -Dtemplate_project_METADATA_ONLY=ON - -DTEMPLATE_PROJECT_ENABLE_CUDA=ON - -DTEMPLATE_PROJECT_ENABLE_OPTIX=ON) -set(_ros2_facade_cache_path "${_ros2_facade_probe}/CMakeCache.txt") -foreach(_feature IN ITEMS CUDA OPTIX) - _read_cache_value( - "${_ros2_facade_cache_path}" "template_project_ENABLE_${_feature}" - _ros2_core_feature) - if(NOT _ros2_core_feature STREQUAL "ON") - message(FATAL_ERROR - "ROS 2 ${_feature} facade did not enable its canonical core option.") - endif() -endforeach() - -_run_success( - "Disable ROS 2 shim GPU facades in the same build" - "${CMAKE_COMMAND}" - -S "${_root}/ros2/template_project" - -B "${_ros2_facade_probe}" - -DTEMPLATE_PROJECT_ENABLE_CUDA=OFF - -DTEMPLATE_PROJECT_ENABLE_OPTIX=OFF) -foreach(_feature IN ITEMS CUDA OPTIX) - _read_cache_value( - "${_ros2_facade_cache_path}" "template_project_ENABLE_${_feature}" - _ros2_core_feature) - if(NOT _ros2_core_feature STREQUAL "OFF") - message(FATAL_ERROR - "ROS 2 ${_feature} facade did not disable its canonical core option.") - endif() -endforeach() - -_run_success( - "Parse and validate source ROS manifests" - "${_python_executable}" - "${_root}/tests/template_test/testRos2OverlayStatic.py" - --repo-root "${_root}" - --expected-version "${EXPECTED_VERSION}" - --metadata-cache "${_metadata_cache_path}") - -set(_fake_list "${TEST_BINARY_ROOT}/fake_list") -set(_fake_conflict "${TEST_BINARY_ROOT}/fake_conflict") -set(_fake_doc_conflict "${TEST_BINARY_ROOT}/fake_doc_conflict") -set(_fake_workflow_conflict "${TEST_BINARY_ROOT}/fake_workflow_conflict") -set(_fake_workflow_no_ci "${TEST_BINARY_ROOT}/fake_workflow_no_ci") -set(_fake_apply "${TEST_BINARY_ROOT}/fake_apply") -set(_fake_apply_ci "${TEST_BINARY_ROOT}/fake_apply_ci") -set(_fake_boundary "${TEST_BINARY_ROOT}/fake_boundary") - -_create_fake_target("${_fake_list}" "my_template_project_x") -_run_success( - "List ROS 2 rollout plan for fake target" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --list --root "${_fake_list}") -if(EXISTS "${_fake_list}/ros2") - message(FATAL_ERROR "Rollout list mode modified the target.") -endif() - -_run_failure( - "Reject rollout verification without apply mode" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --verify --root "${_fake_list}") - -_create_fake_target("${_fake_conflict}" "space_nav") -file(MAKE_DIRECTORY "${_fake_conflict}/ros2") -_run_failure( - "Refuse target with existing ROS overlay" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --root "${_fake_conflict}") - -_create_fake_target("${_fake_doc_conflict}" "space_nav") -file(WRITE "${_fake_doc_conflict}/doc/ros2_overlay.md" - "target-owned documentation\n") -_run_failure( - "Refuse target with existing ROS documentation" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --root "${_fake_doc_conflict}") -if(EXISTS "${_fake_doc_conflict}/ros2" - OR EXISTS "${_fake_doc_conflict}/build_ros2.sh") - message(FATAL_ERROR "Documentation collision produced a partial overlay.") -endif() -_read_required("${_fake_doc_conflict}/doc/ros2_overlay.md" _target_doc_contents) -if(NOT _target_doc_contents STREQUAL "target-owned documentation\n") - message(FATAL_ERROR "Documentation collision changed the target-owned file.") -endif() - -_create_fake_target("${_fake_workflow_conflict}" "space_nav") -file(WRITE "${_fake_workflow_conflict}/.github/workflows/build_ros2_overlay.yml" - "target-owned workflow\n") -_run_failure( - "Refuse target with existing ROS workflow" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --root "${_fake_workflow_conflict}") -if(EXISTS "${_fake_workflow_conflict}/ros2" - OR EXISTS "${_fake_workflow_conflict}/build_ros2.sh") - message(FATAL_ERROR "Workflow collision produced a partial overlay.") -endif() -_read_required( - "${_fake_workflow_conflict}/.github/workflows/build_ros2_overlay.yml" - _target_workflow_contents) -if(NOT _target_workflow_contents STREQUAL "target-owned workflow\n") - message(FATAL_ERROR "Workflow collision changed the target-owned file.") -endif() - -_create_fake_target("${_fake_workflow_no_ci}" "space_nav") -file(WRITE "${_fake_workflow_no_ci}/.github/workflows/build_ros2_overlay.yml" - "target-owned workflow\n") -_run_success( - "Ignore target workflow under --no-ci" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_workflow_no_ci}") -if(NOT EXISTS "${_fake_workflow_no_ci}/ros2" - OR NOT EXISTS "${_fake_workflow_no_ci}/build_ros2.sh") - message(FATAL_ERROR "--no-ci rollout omitted required overlay paths.") -endif() -_read_required( - "${_fake_workflow_no_ci}/.github/workflows/build_ros2_overlay.yml" - _no_ci_workflow_contents) -if(NOT _no_ci_workflow_contents STREQUAL "target-owned workflow\n") - message(FATAL_ERROR "--no-ci rollout changed the target workflow.") -endif() - -_create_fake_target("${_fake_apply}" "space_nav") -_run_success( - "Apply ROS rollout without CI" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_apply}") -foreach(_expected_path - "build_ros2.sh" - "ros2/tools/sync_package_metadata.py" - "ros2/space_nav/package.xml" - "ros2/space_nav_interfaces/package.xml" - "ros2/space_nav_ros/package.xml" - "ros2/space_nav_spinup/package.xml" - "python/COLCON_IGNORE" - "lib/COLCON_IGNORE" - "examples/COLCON_IGNORE" - "tests/COLCON_IGNORE") - if(NOT EXISTS "${_fake_apply}/${_expected_path}") - message(FATAL_ERROR "Rollout omitted ${_expected_path}") - endif() -endforeach() -foreach(_forbidden_path - "add_ros2_support.sh" - "tests/cmake/VerifyTemplateProjectRos2Overlay.cmake" - "ros2/build" - "ros2/install" - "ros2/log") - if(EXISTS "${_fake_apply}/${_forbidden_path}") - message(FATAL_ERROR "Rollout unexpectedly copied ${_forbidden_path}") - endif() -endforeach() - -_run_success( - "Synchronize copied overlay metadata" - "${CMAKE_COMMAND}" -E env "GIT_CEILING_DIRECTORIES=${TEST_BINARY_ROOT}" - "${_bash_executable}" "${_fake_apply}/generate_version.sh" --sync-ros2) -_run_success( - "Parse copied overlay manifests" - "${_python_executable}" - "${_root}/tests/template_test/testRos2OverlayStatic.py" - --repo-root "${_fake_apply}" - --expected-version 2.3.4) - -# Placeholder removal is generated rollout output, so exact textual absence is -# a valid generator contract here. -file(GLOB_RECURSE _fake_apply_files LIST_DIRECTORIES false - "${_fake_apply}/ros2/*") -foreach(_fake_file IN LISTS _fake_apply_files) - file(READ "${_fake_file}" _fake_contents) - if(_fake_contents MATCHES "template_project") - message(FATAL_ERROR "Placeholder remained in generated file ${_fake_file}") - endif() -endforeach() - -_create_fake_target("${_fake_apply_ci}" "space_nav") -_run_success( - "Apply ROS rollout with generic CI" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --root "${_fake_apply_ci}") -set(_generated_workflow - "${_fake_apply_ci}/.github/workflows/build_ros2_overlay.yml") -if(NOT EXISTS "${_generated_workflow}" - OR EXISTS "${_fake_apply_ci}/.github/workflows/build_ros2_overlay.yml.tpl") - message(FATAL_ERROR "Rollout did not materialize exactly one runnable workflow.") -endif() -# The workflow is generator output copied from the generic template; byte -# equality is the intended generation contract. -_run_success( - "Compare generated workflow with its template" - "${CMAKE_COMMAND}" -E compare_files - "${_generated_workflow}" - "${_root}/.github/workflows/build_ros2_overlay.yml.tpl") - -_create_fake_target("${_fake_boundary}" "my_template_project_x") -_run_success( - "Apply ROS rollout to a word-boundary project name" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_boundary}") -if(NOT EXISTS "${_fake_boundary}/ros2/my_template_project_x/package.xml" - OR EXISTS "${_fake_boundary}/ros2/my_my_template_project_x_x/package.xml") - message(FATAL_ERROR "Rollout violated identifier-boundary renaming.") -endif() - -set(_fake_cmake_name_split "${TEST_BINARY_ROOT}/fake_cmake_name_split") -_create_fake_target("${_fake_cmake_name_split}" "space-nav-frontend") -_run_success( - "Apply ROS rollout with a non-ROS CMake package name" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_cmake_name_split}") -foreach(_expected_path - "ros2/space_nav_frontend/package.xml" - "ros2/space_nav_frontend_interfaces/package.xml" - "ros2/space_nav_frontend_ros/package.xml" - "ros2/space_nav_frontend_spinup/package.xml") - if(NOT EXISTS "${_fake_cmake_name_split}/${_expected_path}") - message(FATAL_ERROR "Split-name rollout omitted ${_expected_path}") - endif() -endforeach() - -# These CMake tokens are generated code from the rollout renamer, making their -# exact representation part of this generator test. -_read_required( - "${_fake_cmake_name_split}/ros2/space_nav_frontend_ros/CMakeLists.txt" - _split_bridge_cmake) -foreach(_generated_token - "find_package(space-nav-frontend REQUIRED)" - "space-nav-frontend::space-nav-frontend") - string(FIND "${_split_bridge_cmake}" "${_generated_token}" _token_index) - if(_token_index LESS 0) - message(FATAL_ERROR "Generated bridge CMake omitted '${_generated_token}'.") - endif() -endforeach() -_run_success( - "Parse generated split-name dependencies" - "${_python_executable}" -c - "import sys, xml.etree.ElementTree as ET; root=ET.parse(sys.argv[1]).getroot(); deps={node.text for node in root.findall('depend')}; assert 'space_nav_frontend' in deps; assert 'space-nav-frontend' not in deps" - "${_fake_cmake_name_split}/ros2/space_nav_frontend_ros/package.xml") - -set(_fake_ros_prefix_override "${TEST_BINARY_ROOT}/fake_ros_prefix_override") -_create_fake_target("${_fake_ros_prefix_override}" "space-nav-frontend") -_run_success( - "Apply rollout with an explicit ROS prefix" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_ros_prefix_override}" - --ros-prefix snf) -if(NOT EXISTS "${_fake_ros_prefix_override}/ros2/snf_ros/package.xml") - message(FATAL_ERROR "--ros-prefix did not control generated package names.") -endif() - -set(_fake_invalid_ros_prefix "${TEST_BINARY_ROOT}/fake_invalid_ros_prefix") -_create_fake_target("${_fake_invalid_ros_prefix}" "space-nav-frontend") -_run_failure( - "Reject an invalid explicit ROS prefix" - "${_bash_executable}" "${_root}/add_ros2_support.sh" - --list --root "${_fake_invalid_ros_prefix}" --ros-prefix bad-name) - -set(_fake_rollout_source "${TEST_BINARY_ROOT}/fake_rollout_source") -set(_fake_filtered_rollout "${TEST_BINARY_ROOT}/fake_filtered_rollout") -file(REMOVE_RECURSE "${_fake_rollout_source}") -file(MAKE_DIRECTORY - "${_fake_rollout_source}/ros2/template_project/__pycache__") -configure_file( - "${_root}/add_ros2_support.sh" - "${_fake_rollout_source}/add_ros2_support.sh" - COPYONLY) -file(WRITE "${_fake_rollout_source}/build_ros2.sh" "#!/usr/bin/env bash\n") -file(WRITE "${_fake_rollout_source}/ros2/template_project/package.xml" - "template_project\n") -file(WRITE - "${_fake_rollout_source}/ros2/template_project/__pycache__/generated.cpython-312.pyc" - "generated bytecode\n") -file(WRITE "${_fake_rollout_source}/ros2/template_project/generated.pyc" - "generated bytecode\n") -file(WRITE - "${_fake_rollout_source}/ros2/template_project/nottemplate_projectile.txt" - "boundary fixture\n") - -_create_fake_target("${_fake_filtered_rollout}" "space_nav") -_run_success( - "Apply filtered rollout from a synthetic source" - "${_bash_executable}" "${_fake_rollout_source}/add_ros2_support.sh" - --apply --yes --no-ci --root "${_fake_filtered_rollout}") -if(EXISTS "${_fake_filtered_rollout}/ros2/space_nav/__pycache__" - OR EXISTS "${_fake_filtered_rollout}/ros2/space_nav/generated.pyc") - message(FATAL_ERROR "Rollout copied generated Python cache artifacts.") -endif() -if(NOT EXISTS - "${_fake_filtered_rollout}/ros2/space_nav/nottemplate_projectile.txt") - message(FATAL_ERROR "Rollout renamed an unrelated path substring.") -endif() diff --git a/tests/cmake/VerifyTemplateProjectTailoringScript.cmake b/tests/cmake/VerifyTemplateProjectTailoringScript.cmake deleted file mode 100644 index 6c9affd..0000000 --- a/tests/cmake/VerifyTemplateProjectTailoringScript.cmake +++ /dev/null @@ -1,617 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -set(_script "${TEST_TEMPLATE_SOURCE_DIR}/tailor_template_cleanup.sh") -if(NOT EXISTS "${_script}") - message(FATAL_ERROR "Tailoring cleanup script not found: ${_script}") -endif() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_assert_mode file_path expected_mode) - execute_process( - COMMAND stat -c %a "${file_path}" - RESULT_VARIABLE _mode_result - OUTPUT_VARIABLE _actual_mode - ERROR_VARIABLE _mode_stderr - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT _mode_result EQUAL 0) - message(FATAL_ERROR - "Failed to read mode for ${file_path}.\n" - "stderr:\n${_mode_stderr}") - endif() - if(NOT _actual_mode STREQUAL "${expected_mode}") - message(FATAL_ERROR - "Expected mode ${expected_mode} for ${file_path}, got ${_actual_mode}") - endif() -endfunction() - -function(_snapshot_tree tree_root inventory_output hashes_output) - execute_process( - COMMAND bash -c - "find . -printf '%y|%m|%p|%l\\n' | LC_ALL=C sort" - WORKING_DIRECTORY "${tree_root}" - RESULT_VARIABLE _inventory_result - OUTPUT_VARIABLE _inventory - ERROR_VARIABLE _inventory_stderr) - if(NOT _inventory_result EQUAL 0) - message(FATAL_ERROR - "Failed to snapshot path inventory for ${tree_root}.\n" - "stderr:\n${_inventory_stderr}") - endif() - - execute_process( - COMMAND bash -c - "find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum" - WORKING_DIRECTORY "${tree_root}" - RESULT_VARIABLE _hashes_result - OUTPUT_VARIABLE _hashes - ERROR_VARIABLE _hashes_stderr) - if(NOT _hashes_result EQUAL 0) - message(FATAL_ERROR - "Failed to snapshot file hashes for ${tree_root}.\n" - "stderr:\n${_hashes_stderr}") - endif() - - set(${inventory_output} "${_inventory}" PARENT_SCOPE) - set(${hashes_output} "${_hashes}" PARENT_SCOPE) -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_fake_default "${TEST_BINARY_ROOT}/fake_default") -set(_fake_keep "${TEST_BINARY_ROOT}/fake_keep") -set(_fake_remove_ros2 "${TEST_BINARY_ROOT}/fake_remove_ros2") -set(_fake_missing_template "${TEST_BINARY_ROOT}/fake_missing_template") -set(_fake_orphan_fence "${TEST_BINARY_ROOT}/fake_orphan_fence") -set(_fake_nested_fence "${TEST_BINARY_ROOT}/fake_nested_fence") -set(_fake_unclosed_fence "${TEST_BINARY_ROOT}/fake_unclosed_fence") -set(_fake_missing_namespace "${TEST_BINARY_ROOT}/fake_missing_namespace") -set(_fake_invalid_namespace "${TEST_BINARY_ROOT}/fake_invalid_namespace") -set(_workflow_names - "build_linux.yml" - "build_linux_cuda.yml" - "docs_pages.yml" - "build_ros2_overlay.yml") - -_run_step("Validate script syntax" bash -n "${_script}") - -execute_process( - COMMAND bash "${_script}" --list - WORKING_DIRECTORY "${TEST_TEMPLATE_SOURCE_DIR}" - RESULT_VARIABLE _list_result - OUTPUT_VARIABLE _list_stdout - ERROR_VARIABLE _list_stderr) -if(NOT _list_result EQUAL 0) - message(FATAL_ERROR - "tailor_template_cleanup.sh --list failed.\n" - "stdout:\n${_list_stdout}\n" - "stderr:\n${_list_stderr}") -endif() - -function(_create_fake_project fake_root) - file(MAKE_DIRECTORY "${fake_root}/.github/workflows") - file(MAKE_DIRECTORY "${fake_root}/cmake") - file(MAKE_DIRECTORY "${fake_root}/doc/developments") - file(MAKE_DIRECTORY "${fake_root}/doc/reports") - file(MAKE_DIRECTORY "${fake_root}/tests/cmake") - file(MAKE_DIRECTORY "${fake_root}/tests/matlab") - file(MAKE_DIRECTORY "${fake_root}/tests/template_test") - file(MAKE_DIRECTORY "${fake_root}/ros2/template_project") - file(MAKE_DIRECTORY "${fake_root}/python") - file(MAKE_DIRECTORY "${fake_root}/lib") - file(MAKE_DIRECTORY "${fake_root}/examples") - file(MAKE_DIRECTORY "${fake_root}/profiling") - file(MAKE_DIRECTORY "${fake_root}/src/bin") - file(MAKE_DIRECTORY "${fake_root}/src/template_src") - file(MAKE_DIRECTORY "${fake_root}/src/utils/logging") - - file(WRITE "${fake_root}/build_lib.sh" "#!/usr/bin/env bash\n") - file(WRITE "${fake_root}/build_ros2.sh" "#!/usr/bin/env bash\n") - file(WRITE "${fake_root}/add_ros2_support.sh" "#!/usr/bin/env bash\n") - file(WRITE "${fake_root}/generate_version.sh" "#!/usr/bin/env bash\n") - foreach(_production_wrapper_module - "HandleWrapper.cmake" - "HandlePythonWrapper.cmake" - "HandleMatlabWrapper.cmake" - "StagePythonRuntimeArtifacts.cmake") - configure_file( - "${TEST_TEMPLATE_SOURCE_DIR}/cmake/${_production_wrapper_module}" - "${fake_root}/cmake/${_production_wrapper_module}" - COPYONLY) - endforeach() - file(WRITE "${fake_root}/src/utils/logging/CLogger.h" - "namespace template_project::logging { class CLogger; }\n") - file(WRITE "${fake_root}/src/utils/logging/CLogger.cpp" - "namespace template_project::logging { }\n") - file(WRITE "${fake_root}/src/bin/example_program.cpp" - "template_project::logging::CLogger objLogger;\n") - file(WRITE "${fake_root}/src/template_src/placeholder.cpp" - "template_project::logging::CLogger objLogger;\n") - file(WRITE "${fake_root}/tests/template_test/testProjectLogger.cpp" - "using namespace template_project::logging;\n") - file(WRITE "${fake_root}/doc/logging.md" - "Use template_project::logging::CLogger.\n") - file(WRITE "${fake_root}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(fake_tailored_project) -if(BUILD_AS_MAIN_PROJECT) - include(\"\${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/AddMatlabWrapperRegressionTests.cmake\") - add_template_matlab_wrapper_regression_tests() -endif() -") - file(WRITE "${fake_root}/tests/CMakeLists.txt" -"include(CTest) -add_test(NAME template_project_docs_build_output COMMAND false) -add_test(NAME template_project_version_no_source_side_effect COMMAND false) -add_test(NAME template_project_ros2_overlay_static_contract COMMAND \${CMAKE_COMMAND} -P \${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyTemplateProjectRos2Overlay.cmake) - -# Exclude EXCLUDED_LIST from the list of tests -set(EXCLUDED_LIST \"test_to_exclude\") -set(TESTS_LIST \"\") -include_directories(\${CMAKE_CURRENT_SOURCE_DIR}) -if(Catch2_FOUND) - add_subdirectory(template_test) -endif() -") - _run_step( - "Set fake root CMake mode" - chmod 0640 "${fake_root}/CMakeLists.txt") - _run_step( - "Set fake tests CMake mode" - chmod 0600 "${fake_root}/tests/CMakeLists.txt") - _run_step( - "Set fake logger header mode" - chmod 0640 "${fake_root}/src/utils/logging/CLogger.h") - _run_step( - "Set fake logger guide mode" - chmod 0444 "${fake_root}/doc/logging.md") - - foreach(_path - "AGENTS.md" - "CLAUDE.md" - "CONTEXT.md" - "TODO" - "cpp_cuda_template_project.code-workspace" - "doc/developments/plan.md" - "doc/reports/implementation_review.md" - "tests/cmake/AddMatlabWrapperRegressionTests.cmake" - "tests/cmake/CheckTcmallocDependency.cmake" - "tests/cmake/VerifyTemplateProjectBuildLibCleanSafety.cmake" - "tests/cmake/VerifyTemplateProjectBuildTreePackage.cmake" - "tests/cmake/VerifyTemplateProjectCudaSources.cmake" - "tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake" - "tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake" - "tests/cmake/VerifyTemplateProjectPythonPackaging.cmake" - "tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake" - "tests/cmake/VerifyTemplateProjectRos2Overlay.cmake" - "tests/cmake/VerifyTemplateProjectTailoringScript.cmake" - "tests/template_test/testRos2OverlayStatic.py" - "tests/template_test/testWorkflowTemplates.py" - "tests/matlab/RunTemplateWrapperRegression.m" - "profiling/run_ops_profiling.sh") - get_filename_component(_path_dir "${fake_root}/${_path}" DIRECTORY) - file(MAKE_DIRECTORY "${_path_dir}") - file(WRITE "${fake_root}/${_path}" "template-only\n") - endforeach() - - foreach(_path - "README.md" - "AGENTS.md" - "CLAUDE.md" - "doc/bootstrap_prompts.md" - "doc/template_usage.md" - "doc/versioning.md") - get_filename_component(_path_dir "${fake_root}/${_path}" DIRECTORY) - file(MAKE_DIRECTORY "${_path_dir}") - file(WRITE "${fake_root}/${_path}" -"before ros2 fence - -remove this ros2 overlay block - -after ros2 fence -") - endforeach() - _run_step( - "Set fake README mode" - chmod 0644 "${fake_root}/README.md") - _run_step( - "Set fake bootstrap guide mode" - chmod 0640 "${fake_root}/doc/bootstrap_prompts.md") - _run_step( - "Set fake template usage mode" - chmod 0604 "${fake_root}/doc/template_usage.md") - _run_step( - "Set fake versioning guide mode" - chmod 0444 "${fake_root}/doc/versioning.md") - - foreach(_marker - "python/COLCON_IGNORE" - "lib/COLCON_IGNORE" - "examples/COLCON_IGNORE" - "tests/COLCON_IGNORE") - file(WRITE "${fake_root}/${_marker}" "") - endforeach() - - file(WRITE "${fake_root}/ros2/template_project/package.xml" "1.2.3\n") - foreach(_workflow_name IN LISTS _workflow_names) - file(WRITE - "${fake_root}/.github/workflows/${_workflow_name}" - "name: template-only-${_workflow_name}\n") - configure_file( - "${TEST_TEMPLATE_SOURCE_DIR}/.github/workflows/${_workflow_name}.tpl" - "${fake_root}/.github/workflows/${_workflow_name}.tpl" - COPYONLY) - endforeach() - execute_process( - COMMAND chmod 0640 "${fake_root}/.github/workflows/build_linux.yml.tpl" - RESULT_VARIABLE _chmod_result) - if(NOT _chmod_result EQUAL 0) - message(FATAL_ERROR "Failed to set fake workflow template mode for preservation test") - endif() - file(WRITE "${fake_root}/doc/ros2_overlay.md" "ROS 2 overlay docs\n") -endfunction() - -function(_assert_fake_project_cleaned fake_root expect_profiling) - foreach(_removed - "AGENTS.md" - "doc/developments" - "doc/reports" - "tests/cmake/VerifyTemplateProjectBuildLibCleanSafety.cmake" - "tests/cmake/VerifyTemplateProjectCudaSources.cmake" - "tests/cmake/VerifyTemplateProjectDocsWorkflow.cmake" - "tests/cmake/VerifyTemplateProjectOptixInstallExport.cmake" - "tests/cmake/VerifyTemplateProjectPythonPackaging.cmake" - "tests/cmake/VerifyTemplateProjectReleaseTagSync.cmake" - "tests/cmake/VerifyTemplateProjectRos2Overlay.cmake" - "tests/template_test/testRos2OverlayStatic.py" - "tests/template_test/testWorkflowTemplates.py") - if(EXISTS "${fake_root}/${_removed}") - message(FATAL_ERROR "Expected cleanup to remove '${_removed}'") - endif() - endforeach() - - if(expect_profiling) - if(NOT EXISTS "${fake_root}/profiling/run_ops_profiling.sh") - message(FATAL_ERROR "Expected --keep-profiling to preserve profiling scripts.") - endif() - else() - if(EXISTS "${fake_root}/profiling") - message(FATAL_ERROR "Expected cleanup to remove profiling by default.") - endif() - endif() - - file(READ "${fake_root}/CMakeLists.txt" _root_cmake) - if(_root_cmake MATCHES "AddMatlabWrapperRegressionTests|add_template_matlab_wrapper_regression_tests") - message(FATAL_ERROR "Root CMakeLists.txt still references template MATLAB regression hook.") - endif() - _assert_mode("${fake_root}/CMakeLists.txt" "640") - - file(READ "${fake_root}/tests/CMakeLists.txt" _tests_cmake) - if(_tests_cmake MATCHES "template_project_docs|VerifyTemplateProject") - message(FATAL_ERROR "tests/CMakeLists.txt still references template validation tests.") - endif() - if(NOT _tests_cmake MATCHES "Project unit tests") - message(FATAL_ERROR "tests/CMakeLists.txt was not rewritten with project unit-test header.") - endif() - if(NOT _tests_cmake MATCHES "add_tests\\(\\$\\{project_name\\} EXCLUDED_LIST TESTS_LIST") - message(FATAL_ERROR "tests/CMakeLists.txt does not keep the reusable add_tests registration.") - endif() - if(_tests_cmake MATCHES "if\\(Catch2_FOUND\\)") - message(FATAL_ERROR "tests/CMakeLists.txt still gates all starter tests on Catch2.") - endif() - if(_tests_cmake MATCHES "--output-on-failure|--reporter=compact") - message(FATAL_ERROR "tests/CMakeLists.txt still passes CTest/Catch2 runner flags as Catch2 test properties.") - endif() - _assert_mode("${fake_root}/tests/CMakeLists.txt" "600") - - foreach(_retained_logger_path - "src/utils/logging/CLogger.h" - "src/utils/logging/CLogger.cpp" - "src/bin/example_program.cpp" - "src/template_src/placeholder.cpp" - "tests/template_test/testProjectLogger.cpp" - "doc/logging.md") - if(NOT EXISTS "${fake_root}/${_retained_logger_path}") - message(FATAL_ERROR - "Expected cleanup to retain reusable logger path '${_retained_logger_path}'") - endif() - file(READ "${fake_root}/${_retained_logger_path}" _retained_logger_contents) - if(NOT _retained_logger_contents MATCHES "tailored_project::logging") - message(FATAL_ERROR - "Expected cleanup to tailor logger namespace in '${_retained_logger_path}'") - endif() - if(_retained_logger_contents MATCHES "template_project::logging") - message(FATAL_ERROR - "Cleanup left the template logger namespace in '${_retained_logger_path}'") - endif() - endforeach() - _assert_mode("${fake_root}/src/utils/logging/CLogger.h" "640") - _assert_mode("${fake_root}/doc/logging.md" "444") - - foreach(_production_wrapper_module - "HandleWrapper.cmake" - "HandlePythonWrapper.cmake" - "HandleMatlabWrapper.cmake" - "StagePythonRuntimeArtifacts.cmake") - set(_retained_wrapper_module - "${fake_root}/cmake/${_production_wrapper_module}") - if(NOT EXISTS "${_retained_wrapper_module}") - message(FATAL_ERROR - "Cleanup removed production wrapper module " - "'cmake/${_production_wrapper_module}'") - endif() - _run_step( - "Compare retained production wrapper module ${_production_wrapper_module}" - "${CMAKE_COMMAND}" -E compare_files - "${TEST_TEMPLATE_SOURCE_DIR}/cmake/${_production_wrapper_module}" - "${_retained_wrapper_module}") - endforeach() - - foreach(_workflow_name build_linux.yml build_linux_cuda.yml docs_pages.yml) - set(_materialized_workflow "${fake_root}/.github/workflows/${_workflow_name}") - set(_workflow_template "${fake_root}/.github/workflows/${_workflow_name}.tpl") - if(NOT EXISTS "${_materialized_workflow}") - message(FATAL_ERROR "Expected tailored workflow '${_workflow_name}'") - endif() - if(EXISTS "${_workflow_template}") - message(FATAL_ERROR "Tailoring left dormant workflow '${_workflow_name}.tpl'") - endif() - file(READ "${_materialized_workflow}" _materialized_contents) - file(READ - "${TEST_TEMPLATE_SOURCE_DIR}/.github/workflows/${_workflow_name}.tpl" - _expected_contents) - if(NOT _materialized_contents STREQUAL _expected_contents) - message(FATAL_ERROR "Tailoring did not materialize ${_workflow_name} byte-for-byte") - endif() - if(_workflow_name STREQUAL "build_linux.yml") - execute_process( - COMMAND stat -c %a "${_materialized_workflow}" - RESULT_VARIABLE _mode_result - OUTPUT_VARIABLE _materialized_mode - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT _mode_result EQUAL 0 OR NOT _materialized_mode STREQUAL "640") - message(FATAL_ERROR - "Tailoring did not preserve build_linux.yml.tpl mode 0640; got '${_materialized_mode}'") - endif() - endif() - endforeach() -endfunction() - -function(_assert_ros2_overlay_kept fake_root) - foreach(_kept - "ros2/template_project/package.xml" - "build_ros2.sh" - "python/COLCON_IGNORE" - "lib/COLCON_IGNORE" - "examples/COLCON_IGNORE" - "tests/COLCON_IGNORE" - ".github/workflows/build_ros2_overlay.yml") - if(NOT EXISTS "${fake_root}/${_kept}") - message(FATAL_ERROR "Expected default cleanup to keep '${_kept}'") - endif() - endforeach() - - set(_materialized_ros_workflow - "${fake_root}/.github/workflows/build_ros2_overlay.yml") - if(EXISTS "${fake_root}/.github/workflows/build_ros2_overlay.yml.tpl") - message(FATAL_ERROR "Default tailoring left the dormant ROS workflow template") - endif() - file(READ "${_materialized_ros_workflow}" _materialized_ros_contents) - file(READ - "${TEST_TEMPLATE_SOURCE_DIR}/.github/workflows/build_ros2_overlay.yml.tpl" - _expected_ros_contents) - if(NOT _materialized_ros_contents STREQUAL _expected_ros_contents) - message(FATAL_ERROR "Default tailoring did not materialize the generic ROS workflow") - endif() - foreach(_doc_path - "README.md" - "doc/bootstrap_prompts.md" - "doc/template_usage.md" - "doc/versioning.md") - file(READ "${fake_root}/${_doc_path}" _doc_contents) - if(NOT _doc_contents MATCHES "ros2-overlay-begin|remove this ros2 overlay block|ros2-overlay-end") - message(FATAL_ERROR "Expected default cleanup to keep ROS 2 fence in ${_doc_path}") - endif() - endforeach() -endfunction() - -function(_assert_ros2_overlay_removed fake_root) - foreach(_removed - "ros2" - "build_ros2.sh" - "add_ros2_support.sh" - "python/COLCON_IGNORE" - "lib/COLCON_IGNORE" - "examples/COLCON_IGNORE" - "tests/COLCON_IGNORE" - ".github/workflows/build_ros2_overlay.yml" - ".github/workflows/build_ros2_overlay.yml.tpl" - "doc/ros2_overlay.md" - "tests/template_test/testRos2OverlayStatic.py") - if(EXISTS "${fake_root}/${_removed}") - message(FATAL_ERROR "Expected --remove-ros2 to remove '${_removed}'") - endif() - endforeach() - - if(NOT EXISTS "${fake_root}/generate_version.sh") - message(FATAL_ERROR "--remove-ros2 must not remove generate_version.sh") - endif() - - foreach(_doc_path - "README.md" - "AGENTS.md" - "CLAUDE.md" - "doc/bootstrap_prompts.md" - "doc/template_usage.md" - "doc/versioning.md") - if(NOT EXISTS "${fake_root}/${_doc_path}") - continue() - endif() - file(READ "${fake_root}/${_doc_path}" _doc_contents) - if(_doc_contents MATCHES "ros2-overlay-begin|remove this ros2 overlay block|ros2-overlay-end") - message(FATAL_ERROR "Expected --remove-ros2 to strip ROS 2 fence from ${_doc_path}") - endif() - if(NOT _doc_contents MATCHES "before ros2 fence" OR NOT _doc_contents MATCHES "after ros2 fence") - message(FATAL_ERROR "Expected --remove-ros2 to preserve surrounding text in ${_doc_path}") - endif() - endforeach() - - _assert_mode("${fake_root}/README.md" "644") - _assert_mode("${fake_root}/doc/bootstrap_prompts.md" "640") - _assert_mode("${fake_root}/doc/template_usage.md" "604") - _assert_mode("${fake_root}/doc/versioning.md" "444") -endfunction() - -function(_assert_malformed_fence_rejected fake_root readme_contents case_name) - _create_fake_project("${fake_root}") - file(WRITE "${fake_root}/README.md" "${readme_contents}") - _run_step( - "Reset malformed-fence README mode" - chmod 0644 "${fake_root}/README.md") - _snapshot_tree("${fake_root}" _inventory_before _hashes_before) - execute_process( - COMMAND bash "${_script}" --apply --yes - --project-namespace tailored_project - --remove-ros2 --root "${fake_root}" - RESULT_VARIABLE _malformed_result - OUTPUT_VARIABLE _malformed_stdout - ERROR_VARIABLE _malformed_stderr) - if(_malformed_result EQUAL 0) - message(FATAL_ERROR "Tailoring accepted ${case_name} ROS 2 overlay fences") - endif() - _snapshot_tree("${fake_root}" _inventory_after _hashes_after) - if(NOT _inventory_after STREQUAL _inventory_before) - message(FATAL_ERROR - "Tailoring changed the path inventory or modes before rejecting ${case_name} fences.\n" - "Before:\n${_inventory_before}\n" - "After:\n${_inventory_after}") - endif() - if(NOT _hashes_after STREQUAL _hashes_before) - message(FATAL_ERROR - "Tailoring changed file contents before rejecting ${case_name} fences.\n" - "Before:\n${_hashes_before}\n" - "After:\n${_hashes_after}") - endif() -endfunction() - -function(_assert_namespace_rejected_unchanged fake_root) - _create_fake_project("${fake_root}") - _snapshot_tree("${fake_root}" _inventory_before _hashes_before) - execute_process( - COMMAND bash "${_script}" --apply --yes ${ARGN} --root "${fake_root}" - RESULT_VARIABLE _namespace_result - OUTPUT_VARIABLE _namespace_stdout - ERROR_VARIABLE _namespace_stderr) - if(_namespace_result EQUAL 0) - message(FATAL_ERROR "Tailoring accepted invalid namespace arguments: ${ARGN}") - endif() - _snapshot_tree("${fake_root}" _inventory_after _hashes_after) - if(NOT _inventory_after STREQUAL _inventory_before OR - NOT _hashes_after STREQUAL _hashes_before) - message(FATAL_ERROR - "Tailoring changed the tree before rejecting namespace arguments: ${ARGN}") - endif() -endfunction() - -_assert_namespace_rejected_unchanged( - "${_fake_missing_namespace}") -_assert_namespace_rejected_unchanged( - "${_fake_invalid_namespace}" - --project-namespace bad-name) - -_assert_malformed_fence_rejected( - "${_fake_nested_fence}" - "before\n\nouter\n\ninner\n\n\nafter\n" - "nested-begin") -_assert_malformed_fence_rejected( - "${_fake_orphan_fence}" - "before\n\nafter\n" - "orphan-end") -_assert_malformed_fence_rejected( - "${_fake_unclosed_fence}" - "before\n\nunclosed\n" - "unclosed-begin") - -_create_fake_project("${_fake_default}") - -_run_step( - "Apply tailoring cleanup to fake project" - bash "${_script}" --apply --yes --project-namespace tailored_project - --root "${_fake_default}") -_assert_fake_project_cleaned("${_fake_default}" FALSE) -_assert_ros2_overlay_kept("${_fake_default}") -_run_step( - "Reapply tailoring cleanup to an already materialized project" - bash "${_script}" --apply --yes --project-namespace tailored_project - --root "${_fake_default}") -_assert_fake_project_cleaned("${_fake_default}" FALSE) -_assert_ros2_overlay_kept("${_fake_default}") - -_create_fake_project("${_fake_keep}") - -_run_step( - "Apply tailoring cleanup to fake project with profiling preserved" - bash "${_script}" --apply --yes --project-namespace tailored_project - --keep-profiling --root "${_fake_keep}") -_assert_fake_project_cleaned("${_fake_keep}" TRUE) -_assert_ros2_overlay_kept("${_fake_keep}") - -_create_fake_project("${_fake_remove_ros2}") - -_run_step( - "Apply tailoring cleanup to fake project with ROS 2 removed" - bash "${_script}" --apply --yes --project-namespace tailored_project - --remove-ros2 --root "${_fake_remove_ros2}") -_assert_fake_project_cleaned("${_fake_remove_ros2}" FALSE) -_assert_ros2_overlay_removed("${_fake_remove_ros2}") -_run_step( - "Reapply tailoring cleanup after ROS 2 removal" - bash "${_script}" --apply --yes --project-namespace tailored_project - --remove-ros2 --root "${_fake_remove_ros2}") -_assert_fake_project_cleaned("${_fake_remove_ros2}" FALSE) -_assert_ros2_overlay_removed("${_fake_remove_ros2}") - -_create_fake_project("${_fake_missing_template}") -file(REMOVE - "${_fake_missing_template}/.github/workflows/build_linux.yml.tpl") -_snapshot_tree( - "${_fake_missing_template}" - _missing_template_inventory_before - _missing_template_hashes_before) -execute_process( - COMMAND bash "${_script}" --apply --yes - --project-namespace tailored_project --root "${_fake_missing_template}" - RESULT_VARIABLE _missing_template_result - OUTPUT_VARIABLE _missing_template_stdout - ERROR_VARIABLE _missing_template_stderr) -if(_missing_template_result EQUAL 0) - message(FATAL_ERROR - "Tailoring accepted an active template-validation workflow whose generic .tpl was missing") -endif() -_snapshot_tree( - "${_fake_missing_template}" - _missing_template_inventory_after - _missing_template_hashes_after) -if(NOT _missing_template_inventory_after STREQUAL _missing_template_inventory_before OR - NOT _missing_template_hashes_after STREQUAL _missing_template_hashes_before) - message(FATAL_ERROR - "Tailoring changed the tree before rejecting a missing generic workflow template.") -endif() diff --git a/tests/cmake/VerifyTemplateProjectTensorRTModule.cmake b/tests/cmake/VerifyTemplateProjectTensorRTModule.cmake deleted file mode 100644 index bb8f89e..0000000 --- a/tests/cmake/VerifyTemplateProjectTensorRTModule.cmake +++ /dev/null @@ -1,132 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Verify portable TensorRT discovery and module delivery without requiring a -# real SDK or linking vendor binaries. -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -function(_run_step step_name) - execute_process( - COMMAND ${ARGN} - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "${step_name} failed with exit code ${_result}.\n" - "stdout:\n${_stdout}\n" - "stderr:\n${_stderr}") - endif() -endfunction() - -function(_configure_consumer step_name module_dir build_dir root_argument) - _run_step( - "${step_name}" - "${CMAKE_COMMAND}" - -S "${_consumer_source}" - -B "${build_dir}" - "-DTEST_TENSORRT_MODULE_DIR=${module_dir}" - "-D${root_argument}=${_tensorrt_root}") -endfunction() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -set(_tensorrt_arch "aarch64-linux-gnu") -set(_tensorrt_root "${TEST_BINARY_ROOT}/TensorRT") -set(_tensorrt_include - "${_tensorrt_root}/targets/${_tensorrt_arch}/include") -set(_tensorrt_lib "${_tensorrt_root}/targets/${_tensorrt_arch}/lib") -set(_consumer_source "${TEST_BINARY_ROOT}/consumer") -file(MAKE_DIRECTORY - "${_tensorrt_include}" - "${_tensorrt_lib}" - "${_consumer_source}") - -file(WRITE "${_tensorrt_include}/NvInfer.h" "#pragma once\n") -file(WRITE "${_tensorrt_include}/NvInferVersion.h" -"#define NV_TENSORRT_MAJOR 10 -#define NV_TENSORRT_MINOR 7 -#define NV_TENSORRT_PATCH 0 -#define NV_TENSORRT_BUILD 1 -") -file(WRITE "${_tensorrt_lib}/libnvinfer.so" "fixture\n") -file(WRITE "${_tensorrt_lib}/libnvinfer_plugin.so" "fixture\n") - -file(WRITE "${_consumer_source}/CMakeLists.txt" -"cmake_minimum_required(VERSION 3.15) -project(tensorrt_module_consumer LANGUAGES NONE) -set(CMAKE_LIBRARY_ARCHITECTURE \"${_tensorrt_arch}\") -set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH FALSE) -set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH FALSE) -list(PREPEND CMAKE_MODULE_PATH \"\${TEST_TENSORRT_MODULE_DIR}\") -find_package(TensorRT 10.7 REQUIRED) -if(NOT TARGET TensorRT::nvinfer OR NOT TARGET TensorRT::nvinfer_plugin) - message(FATAL_ERROR \"TensorRT imported targets are unavailable.\") -endif() -if(NOT TensorRT_VERSION STREQUAL \"10.7.0.1\") - message(FATAL_ERROR \"Unexpected TensorRT version: \${TensorRT_VERSION}\") -endif() -if(NOT TensorRT_INCLUDE_DIRS STREQUAL \"${_tensorrt_include}\") - message(FATAL_ERROR \"Unexpected TensorRT includes: \${TensorRT_INCLUDE_DIRS}\") -endif() -list(LENGTH TensorRT_LIBRARIES _library_count) -if(NOT _library_count EQUAL 2) - message(FATAL_ERROR \"Unexpected TensorRT libraries: \${TensorRT_LIBRARIES}\") -endif() -") - -# Accept the canonical package-name hint and the established all-uppercase -# compatibility spelling against a non-x86 SDK layout. -_configure_consumer( - "Discover source TensorRT module through TensorRT_ROOT" - "${TEST_TEMPLATE_SOURCE_DIR}/cmake" - "${TEST_BINARY_ROOT}/consumer_source_canonical" - TensorRT_ROOT) -_configure_consumer( - "Discover source TensorRT module through TENSORRT_ROOT" - "${TEST_TEMPLATE_SOURCE_DIR}/cmake" - "${TEST_BINARY_ROOT}/consumer_source_compatibility" - TENSORRT_ROOT) - -set(_template_build "${TEST_BINARY_ROOT}/template_build") -set(_template_install "${TEST_BINARY_ROOT}/template_install") -_run_step( - "Configure template for TensorRT module delivery" - "${CMAKE_COMMAND}" - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${_template_build}" - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_LIBDIR=lib - -DENABLE_TESTS=OFF - -DENABLE_FETCH_CATCH2=OFF - -Dtemplate_project_ENABLE_CUDA=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF) -_run_step( - "Build template for TensorRT module delivery" - "${CMAKE_COMMAND}" --build "${_template_build}" --parallel 4) -_run_step( - "Install template TensorRT module" - "${CMAKE_COMMAND}" --install "${_template_build}" --prefix "${_template_install}") - -set(_build_module_dir "${_template_build}/modules") -set(_install_module_dir - "${_template_install}/lib/cmake/template_project/modules") -foreach(_module_dir IN ITEMS "${_build_module_dir}" "${_install_module_dir}") - if(NOT EXISTS "${_module_dir}/FindTensorRT.cmake") - message(FATAL_ERROR "TensorRT module was not delivered to ${_module_dir}.") - endif() -endforeach() - -_configure_consumer( - "Discover build-tree TensorRT module" - "${_build_module_dir}" - "${TEST_BINARY_ROOT}/consumer_build_tree" - TensorRT_ROOT) -_configure_consumer( - "Discover installed TensorRT module" - "${_install_module_dir}" - "${TEST_BINARY_ROOT}/consumer_install_tree" - TensorRT_ROOT) diff --git a/tests/cmake/VerifyTemplateProjectVersionSideEffects.cmake b/tests/cmake/VerifyTemplateProjectVersionSideEffects.cmake deleted file mode 100644 index 856bad9..0000000 --- a/tests/cmake/VerifyTemplateProjectVersionSideEffects.cmake +++ /dev/null @@ -1,53 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -foreach(required_var TEST_TEMPLATE_SOURCE_DIR TEST_BINARY_ROOT) - if(NOT DEFINED ${required_var}) - message(FATAL_ERROR "Missing required variable: ${required_var}") - endif() -endforeach() - -set(_source_version "${TEST_TEMPLATE_SOURCE_DIR}/VERSION") -set(_had_source_version OFF) -set(_source_version_before "") -if(EXISTS "${_source_version}") - set(_had_source_version ON) - file(READ "${_source_version}" _source_version_before) -endif() - -file(REMOVE_RECURSE "${TEST_BINARY_ROOT}") -file(MAKE_DIRECTORY "${TEST_BINARY_ROOT}") - -execute_process( - COMMAND ${CMAKE_COMMAND} - -S "${TEST_TEMPLATE_SOURCE_DIR}" - -B "${TEST_BINARY_ROOT}/build" - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DENABLE_TESTS=OFF - -DENABLE_CUDA=OFF - -DENABLE_OPTIX=OFF - -DENABLE_OPENGL=OFF - -DWRITE_SOURCE_VERSION_FILE=OFF - -Dtemplate_project_BUILD_PROGRAMS=OFF - -Dtemplate_project_BUILD_EXAMPLES=OFF - RESULT_VARIABLE _configure_result - OUTPUT_VARIABLE _configure_stdout - ERROR_VARIABLE _configure_stderr) -if(NOT _configure_result EQUAL 0) - message(FATAL_ERROR - "Configure failed with exit code ${_configure_result}.\n" - "stdout:\n${_configure_stdout}\n" - "stderr:\n${_configure_stderr}") -endif() - -if(NOT EXISTS "${TEST_BINARY_ROOT}/build/VERSION") - message(FATAL_ERROR "Build-tree VERSION was not written.") -endif() - -if(_had_source_version) - file(READ "${_source_version}" _source_version_after) - if(NOT "${_source_version_after}" STREQUAL "${_source_version_before}") - message(FATAL_ERROR "Source VERSION changed even though WRITE_SOURCE_VERSION_FILE=OFF.") - endif() -elseif(EXISTS "${_source_version}") - message(FATAL_ERROR "Source VERSION was created even though WRITE_SOURCE_VERSION_FILE=OFF.") -endif() diff --git a/tests/scripts/test_run_in_container.sh b/tests/scripts/test_run_in_container.sh deleted file mode 100644 index fc92663..0000000 --- a/tests/scripts/test_run_in_container.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# Validate container-launch arguments without requiring a daemon or image. - -set -Eeuo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" -readonly REPO_ROOT -SCRIPT_PATH="${REPO_ROOT}/run_in_container.sh" -readonly SCRIPT_PATH -TEST_ROOT="$(mktemp -d)" -readonly TEST_ROOT -FAKE_BIN="${TEST_ROOT}/fake-bin" -ENGINE_LOG="${TEST_ROOT}/engine.log" -readonly FAKE_BIN ENGINE_LOG -PASS_COUNT=0 - -cleanup() { - if [[ -d "${TEST_ROOT}" ]]; then - rm -rf -- "${TEST_ROOT}" - fi -} -trap cleanup EXIT - -fail() { - printf '[FAIL] %s\n' "$*" >&2 - exit 1 -} - -pass() { - PASS_COUNT=$((PASS_COUNT + 1)) - printf '[PASS] %s\n' "$*" -} - -assert_log_contains() { - local expected_text_="$1" - grep -Fqx -- "ARG ${expected_text_}" "${ENGINE_LOG}" || { - sed -n '1,260p' "${ENGINE_LOG}" >&2 - fail "engine log does not contain argument '${expected_text_}'" - } -} - -assert_log_excludes() { - local unexpected_text_="$1" - if grep -Fqx -- "ARG ${unexpected_text_}" "${ENGINE_LOG}"; then - sed -n '1,260p' "${ENGINE_LOG}" >&2 - fail "engine log unexpectedly contains argument '${unexpected_text_}'" - fi -} - -create_fake_engine() { - mkdir -p "${FAKE_BIN}" - cat >"${FAKE_BIN}/container-engine" <<'EOF' -#!/usr/bin/env bash -set -Eeuo pipefail - -printf 'CALL %s\n' "$(basename "$0")" >>"${CONTAINER_ENGINE_LOG}" -for argument_ in "$@"; do - printf 'ARG %s\n' "${argument_}" >>"${CONTAINER_ENGINE_LOG}" -done - -if [[ "${1:-}" == "info" ]]; then - printf '%s\n' "${FAKE_PODMAN_ROOTLESS:-true}" - exit 0 -fi -if [[ "${1:-}" == "image" && "${2:-}" == "inspect" ]]; then - exit 0 -fi -if [[ "${1:-}" == "container" && "${2:-}" == "inspect" ]]; then - exit 1 -fi -if [[ "${1:-}" == "run" ]]; then - if [[ "$*" == *'id -u vscode'* ]]; then - printf '%s\n' "${FAKE_IMAGE_IDS}" - elif [[ " $* " == *' --detach '* ]]; then - printf 'fixture-container-id\n' - fi - exit 0 -fi - -exit 0 -EOF - chmod +x "${FAKE_BIN}/container-engine" - ln -s container-engine "${FAKE_BIN}/docker" - ln -s container-engine "${FAKE_BIN}/podman" -} - -run_launcher() { - : >"${ENGINE_LOG}" - env -u SSH_AUTH_SOCK \ - PATH="${FAKE_BIN}:${PATH}" \ - CONTAINER_ENGINE_LOG="${ENGINE_LOG}" \ - FAKE_IMAGE_IDS="$(id -u):$(id -g)" \ - bash "${SCRIPT_PATH}" "$@" >/dev/null -} - -test_docker_command_ownership() { - run_launcher --engine docker --no-gpu -- printf fixture - - assert_log_contains run - assert_log_contains "$(id -u):$(id -g)" - assert_log_contains HOME=/tmp - assert_log_contains "type=bind,source=${REPO_ROOT},target=/workspace" - assert_log_excludes --gpus - pass 'Docker command mode preserves host ownership without GPU flags' -} - -test_podman_command_ownership() { - run_launcher --engine podman -- printf fixture - - assert_log_contains --security-opt=label=disable - assert_log_contains --userns=keep-id - assert_log_contains nvidia.com/gpu=all - assert_log_contains keep-groups - pass 'rootless Podman command mode preserves ownership and GPU groups' -} - -test_vscode_attachment_contract() { - run_launcher --engine docker --no-gpu --vscode \ - --container-name fixture-vscode - - assert_log_contains --detach - assert_log_contains fixture-vscode - assert_log_contains vscode - assert_log_contains "type=bind,source=${REPO_ROOT},target=/workspaces/cpp_cuda_template_project" - assert_log_contains /usr/bin/sleep - assert_log_contains infinity - pass 'VS Code mode starts a stable host-owned attachment container' -} - -test_invalid_matlab_root_stops_before_engine() { - local invalid_matlab_root_="${TEST_ROOT}/invalid-matlab" - local output_file_="${TEST_ROOT}/invalid-matlab.out" - - mkdir -p "${invalid_matlab_root_}" - : >"${ENGINE_LOG}" - if env -u SSH_AUTH_SOCK \ - PATH="${FAKE_BIN}:${PATH}" \ - CONTAINER_ENGINE_LOG="${ENGINE_LOG}" \ - FAKE_IMAGE_IDS="$(id -u):$(id -g)" \ - bash "${SCRIPT_PATH}" --engine docker --matlab-root \ - "${invalid_matlab_root_}" -- true >"${output_file_}" 2>&1; then - fail 'invalid MATLAB root unexpectedly succeeded' - fi - [[ ! -s "${ENGINE_LOG}" ]] || fail 'invalid MATLAB root invoked engine' - grep -Fq 'extern/include/mex.h' "${output_file_}" || { - sed -n '1,120p' "${output_file_}" >&2 - fail 'invalid MATLAB root produced no actionable diagnostic' - } - pass 'invalid MATLAB roots fail before container-engine access' -} - -create_fake_engine -test_docker_command_ownership -test_podman_command_ownership -test_vscode_attachment_contract -test_invalid_matlab_root_stops_before_engine - -printf '[SUMMARY] %d tests passed\n' "${PASS_COUNT}" diff --git a/tests/scripts/test_use_system_matlab_libraries.sh b/tests/scripts/test_use_system_matlab_libraries.sh deleted file mode 100755 index 5f4d83b..0000000 --- a/tests/scripts/test_use_system_matlab_libraries.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env bash -# Verify dry-run, privilege, backup, apply, restore, and discovery contracts -# against a disposable MATLAB-library fixture. - -set -Eeuo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" -readonly REPO_ROOT -readonly SCRIPT_PATH="${REPO_ROOT}/scripts/use_system_matlab_libraries.sh" -TEST_ROOT="$(mktemp -d)" -readonly TEST_ROOT - -PASS_COUNT=0 - -cleanup() { - if [[ -n "${TEST_ROOT:-}" && -d "${TEST_ROOT}" ]]; then - rm -rf -- "${TEST_ROOT}" - fi -} -trap cleanup EXIT - -fail() { - printf '[FAIL] %s\n' "$*" >&2 - exit 1 -} - -pass() { - PASS_COUNT=$((PASS_COUNT + 1)) - printf '[PASS] %s\n' "$*" -} - -assert_contains() { - local haystack_="$1" - local needle_="$2" - local context_="$3" - - [[ "${haystack_}" == *"${needle_}"* ]] || - fail "${context_}: expected output to contain '${needle_}'" -} - -assert_link_text() { - local link_path_="$1" - local expected_text_="$2" - local actual_text_ - - [[ -L "${link_path_}" ]] || fail "Expected symlink: ${link_path_}" - actual_text_="$(readlink "${link_path_}")" - [[ "${actual_text_}" == "${expected_text_}" ]] || - fail "${link_path_}: expected '${expected_text_}', got '${actual_text_}'" -} - -run_expect_success() { - local output_file_="$1" - shift - - if ! "$@" >"${output_file_}" 2>&1; then - sed -n '1,240p' "${output_file_}" >&2 - fail "Command unexpectedly failed: $*" - fi -} - -run_expect_failure() { - local output_file_="$1" - shift - - if "$@" >"${output_file_}" 2>&1; then - sed -n '1,240p' "${output_file_}" >&2 - fail "Command unexpectedly succeeded: $*" - fi -} - -create_executable_() { - local path_="$1" - shift - - printf '%s\n' "$@" >"${path_}" - chmod +x "${path_}" -} - -setup_fixture() { - FIXTURE_PREFIX="${TEST_ROOT}/MATLAB" - FIXTURE_MATLAB_ROOT="${FIXTURE_PREFIX}/R2024b" - FIXTURE_BIN="${TEST_ROOT}/fake-bin" - FIXTURE_SYSTEM_LIB="${TEST_ROOT}/system-lib" - - mkdir -p \ - "${FIXTURE_MATLAB_ROOT}/bin/glnxa64" \ - "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/orig" \ - "${FIXTURE_MATLAB_ROOT}/toolbox/compiler_sdk/runtime/glnxa64" \ - "${FIXTURE_BIN}" \ - "${FIXTURE_SYSTEM_LIB}" - - create_executable_ "${FIXTURE_MATLAB_ROOT}/bin/matlab" \ - '#!/usr/bin/env bash' \ - 'exit 0' - ln -s "${FIXTURE_MATLAB_ROOT}/bin/matlab" "${FIXTURE_BIN}/matlab" - - touch \ - "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6.0.30" \ - "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.4.7.0" \ - "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_imgproc.so.4.7.0" \ - "${FIXTURE_SYSTEM_LIB}/libstdc++.so.6.0.35" \ - "${FIXTURE_SYSTEM_LIB}/libopencv_core.so.4.10.0" \ - "${FIXTURE_SYSTEM_LIB}/libopencv_imgproc.so.4.10.0" - - ln -s 'libstdc++.so.6.0.30' \ - "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6" - ln -s 'libstdc++.so.6.0.30' \ - "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/orig/libstdc++.so.6" - ln -s 'libstdc++.so.6.0.30' \ - "${FIXTURE_MATLAB_ROOT}/toolbox/compiler_sdk/runtime/glnxa64/libstdc++.so.6" - ln -s 'libopencv_core.so.4.7.0' \ - "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407" - ln -s 'libopencv_imgproc.so.4.7.0' \ - "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_imgproc.so.407" - - # The single-quoted strings are the literal source of the generated shim. - # shellcheck disable=SC2016 - create_executable_ "${FIXTURE_BIN}/id" \ - '#!/usr/bin/env bash' \ - 'if [[ "${1:-}" == "-u" ]]; then' \ - ' printf "%s\\n" "${FAKE_ID_UID:-1000}"' \ - 'else' \ - ' /usr/bin/id "$@"' \ - 'fi' - - # shellcheck disable=SC2016 - create_executable_ "${FIXTURE_BIN}/ldconfig" \ - '#!/usr/bin/env bash' \ - 'printf "3 libs found in cache\\n"' \ - 'printf "\\tlibstdc++.so.6 (libc6,x86-64) => %s/libstdc++.so.6.0.35\\n" "${FAKE_SYSTEM_LIB}"' \ - 'printf "\\tlibopencv_core.so (libc6,x86-64) => %s/libopencv_core.so.4.10.0\\n" "${FAKE_SYSTEM_LIB}"' \ - 'printf "\\tlibopencv_imgproc.so (libc6,x86-64) => %s/libopencv_imgproc.so.4.10.0\\n" "${FAKE_SYSTEM_LIB}"' - - # shellcheck disable=SC2016 - create_executable_ "${FIXTURE_BIN}/file" \ - '#!/usr/bin/env bash' \ - 'printf "%s: ELF 64-bit LSB shared object, x86-64\\n" "${@: -1}"' - - # shellcheck disable=SC2016 - create_executable_ "${FIXTURE_BIN}/readelf" \ - '#!/usr/bin/env bash' \ - 'case "${@: -1}" in' \ - ' *libstdc++*) soname_="libstdc++.so.6" ;;' \ - ' *libopencv_core*) soname_="libopencv_core.so.410" ;;' \ - ' *libopencv_imgproc*) soname_="libopencv_imgproc.so.410" ;;' \ - ' *) exit 1 ;;' \ - 'esac' \ - 'printf " 0x000000000000000e (SONAME) Library soname: [%s]\\n" "${soname_}"' -} - -main() { - local output_file_="${TEST_ROOT}/command-output.txt" - local output_ - - setup_fixture - - run_expect_failure "${output_file_}" bash "${SCRIPT_PATH}" --matlab-root "${FIXTURE_MATLAB_ROOT}" - output_="$(<"${output_file_}")" - assert_contains "${output_}" 'Select at least one library family' 'selector guard' - pass 'requires an explicit library selector' - - run_expect_success "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - bash "${SCRIPT_PATH}" --matlab-version R2024b \ - --matlab-prefix "${FIXTURE_PREFIX}" --all - output_="$(<"${output_file_}")" - assert_contains "${output_}" '[DRY-RUN]' 'dry-run mode' - assert_contains "${output_}" 'OpenCV SONAME change: 407 -> 410' 'OpenCV mismatch warning' - assert_contains "${output_}" '[CMD] ldconfig -p' 'command logging' - assert_contains "${output_}" '[EXIT] 0' 'command exit logging' - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6" 'libstdc++.so.6.0.30' - assert_link_text "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407" 'libopencv_core.so.4.7.0' - pass 'dry-run plans all selected replacements without mutation' - - run_expect_failure "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - FAKE_ID_UID=1000 \ - bash "${SCRIPT_PATH}" --matlab-root "${FIXTURE_MATLAB_ROOT}" --all --apply - output_="$(<"${output_file_}")" - assert_contains "${output_}" 'sudo' 'root guard' - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6" 'libstdc++.so.6.0.30' - pass 'apply refuses to mutate without root' - - run_expect_success "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - FAKE_ID_UID=0 \ - bash "${SCRIPT_PATH}" --matlab-root "${FIXTURE_MATLAB_ROOT}" --all --apply - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6" \ - "${FIXTURE_SYSTEM_LIB}/libstdc++.so.6.0.35" - assert_link_text "${FIXTURE_MATLAB_ROOT}/toolbox/compiler_sdk/runtime/glnxa64/libstdc++.so.6" \ - "${FIXTURE_SYSTEM_LIB}/libstdc++.so.6.0.35" - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/orig/libstdc++.so.6" \ - 'libstdc++.so.6.0.30' - assert_link_text "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407" \ - "${FIXTURE_SYSTEM_LIB}/libopencv_core.so.4.10.0" - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6.matlab-backup" \ - 'libstdc++.so.6.0.30' - assert_link_text "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407.matlab-backup" \ - 'libopencv_core.so.4.7.0' - pass 'apply replaces selected links and preserves one-time backups' - - run_expect_success "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - FAKE_ID_UID=0 \ - bash "${SCRIPT_PATH}" --matlab-root "${FIXTURE_MATLAB_ROOT}" --all --apply - output_="$(<"${output_file_}")" - assert_contains "${output_}" '[UNCHANGED]' 'idempotent apply' - pass 'repeated apply is idempotent' - - run_expect_success "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - FAKE_ID_UID=0 \ - bash "${SCRIPT_PATH}" --matlab-root "${FIXTURE_MATLAB_ROOT}" --all --restore - assert_link_text "${FIXTURE_MATLAB_ROOT}/sys/os/glnxa64/libstdc++.so.6" 'libstdc++.so.6.0.30' - assert_link_text "${FIXTURE_MATLAB_ROOT}/toolbox/compiler_sdk/runtime/glnxa64/libstdc++.so.6" \ - 'libstdc++.so.6.0.30' - assert_link_text "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407" \ - 'libopencv_core.so.4.7.0' - [[ -L "${FIXTURE_MATLAB_ROOT}/bin/glnxa64/libopencv_core.so.407.matlab-backup" ]] || - fail 'Restore removed the recovery backup' - pass 'restore reinstates exact original link text and retains backups' - - run_expect_success "${output_file_}" env \ - PATH="${FIXTURE_BIN}:/usr/bin:/bin" \ - FAKE_SYSTEM_LIB="${FIXTURE_SYSTEM_LIB}" \ - bash "${SCRIPT_PATH}" --libstdcxx - output_="$(<"${output_file_}")" - assert_contains "${output_}" "MATLAB root: ${FIXTURE_MATLAB_ROOT}" 'PATH autodetection' - pass 'autodetects MATLAB from PATH' - - printf '[SUMMARY] %d tests passed\n' "${PASS_COUNT}" -} - -main "$@" diff --git a/tests/scripts/test_wrapper_maintenance.sh b/tests/scripts/test_wrapper_maintenance.sh deleted file mode 100644 index 3fafb60..0000000 --- a/tests/scripts/test_wrapper_maintenance.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash -# Verify that wrapper checkout maintenance is explicit and CMake-owned. - -set -Eeuo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" -readonly REPO_ROOT -REAL_CMAKE="$(command -v cmake)" -readonly REAL_CMAKE -TEST_ROOT="$(mktemp -d)" -readonly TEST_ROOT -PASS_COUNT=0 - -cleanup() { - if [[ -d "${TEST_ROOT}" ]]; then - rm -rf -- "${TEST_ROOT}" - fi -} -trap cleanup EXIT - -fail() { - printf '[FAIL] %s\n' "$*" >&2 - exit 1 -} - -pass() { - PASS_COUNT=$((PASS_COUNT + 1)) - printf '[PASS] %s\n' "$*" -} - -assert_not_contains() { - local file_path_="$1" - local unexpected_text_="$2" - local context_="$3" - - if grep -Fq -- "${unexpected_text_}" "${file_path_}"; then - sed -n '1,240p' "${file_path_}" >&2 - fail "${context_}: found '${unexpected_text_}'" - fi -} - -assert_contains_once() { - local file_path_="$1" - local expected_text_="$2" - local context_="$3" - local match_count_ - - match_count_="$(grep -Fxc -- "${expected_text_}" "${file_path_}" || true)" - [[ "${match_count_}" == "1" ]] || { - sed -n '1,240p' "${file_path_}" >&2 - fail "${context_}: expected one '${expected_text_}', found ${match_count_}" - } -} - -create_fixture() { - FIXTURE_PROJECT="${TEST_ROOT}/project" - FIXTURE_WRAP="${TEST_ROOT}/wrap" - FAKE_BIN="${TEST_ROOT}/fake-bin" - readonly FIXTURE_PROJECT FIXTURE_WRAP FAKE_BIN - - mkdir -p \ - "${FIXTURE_PROJECT}/src" \ - "${FIXTURE_WRAP}/cmake" \ - "${FIXTURE_WRAP}/.git" \ - "${FAKE_BIN}" - cp "${REPO_ROOT}/build_lib.sh" "${FIXTURE_PROJECT}/build_lib.sh" - touch "${FIXTURE_PROJECT}/src/wrap_interface.i" - touch "${FIXTURE_WRAP}/cmake/PybindWrap.cmake" - - printf '%s\n' \ - 'cmake_minimum_required(VERSION 3.15)' \ - 'set(project_name "maintenance_fixture")' \ - 'project(maintenance_fixture LANGUAGES NONE)' \ - >"${FIXTURE_PROJECT}/CMakeLists.txt" - - cat >"${FAKE_BIN}/cmake" <<'EOF' -#!/usr/bin/env bash -set -Eeuo pipefail - -if [[ "${1:-}" == "--version" ]]; then - printf 'cmake version 3.28.3\n' - exit 0 -fi -if [[ "${1:-}" == "--build" ]]; then - exit 0 -fi - -printf '%s\n' "$@" >>"${WRAPPER_CMAKE_LOG}" -EOF - chmod +x "${FAKE_BIN}/cmake" - - cat >"${FAKE_BIN}/git" <<'EOF' -#!/usr/bin/env bash -set -Eeuo pipefail -printf '%s\n' "$*" >>"${WRAPPER_GIT_LOG}" -exit 0 -EOF - chmod +x "${FAKE_BIN}/git" -} - -run_build_helper() { - local build_name_="$1" - shift - - : >"${WRAPPER_CMAKE_LOG}" - : >"${WRAPPER_GIT_LOG}" - PATH="${FAKE_BIN}:${PATH}" \ - WRAPPER_CMAKE_LOG="${WRAPPER_CMAKE_LOG}" \ - WRAPPER_GIT_LOG="${WRAPPER_GIT_LOG}" \ - bash "${FIXTURE_PROJECT}/build_lib.sh" \ - -B "${build_name_}" \ - -p \ - --gtwrap-root "${FIXTURE_WRAP}" \ - --skip-tests \ - "$@" \ - >/dev/null -} - -test_default_is_non_mutating() { - run_build_helper build_default - - [[ ! -s "${WRAPPER_GIT_LOG}" ]] || fail "default build invoked Git" - assert_not_contains "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_MAINTENANCE_UPDATE=' 'default maintenance grant' - assert_not_contains "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_SYNC_TO_MASTER=' 'default synchronization request' - assert_not_contains "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_INIT_SUBMODULE_IF_MISSING=' 'default submodule request' - assert_not_contains "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_ADD_SUBMODULE_IF_MISSING=' 'removed submodule-add request' - pass 'default wrapper build passes no maintenance policy' -} - -test_update_is_delegated_to_cmake() { - run_build_helper build_update --wrap-update - - [[ ! -s "${WRAPPER_GIT_LOG}" ]] || fail "--wrap-update invoked Git directly" - assert_contains_once "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_MAINTENANCE_UPDATE=ON' 'maintenance grant' - assert_contains_once "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_SYNC_TO_MASTER=ON' 'synchronization request' - assert_contains_once "${WRAPPER_CMAKE_LOG}" \ - '-DGTWRAP_BRANCH=master' 'maintenance branch' - pass '--wrap-update delegates one explicit maintenance request to CMake' -} - -test_declared_submodule_only() { - local cmake_source_="${TEST_ROOT}/submodule-fixture" - local cmake_build_="${TEST_ROOT}/submodule-build" - - mkdir -p "${cmake_source_}/project" - touch "${cmake_source_}/project/.gitmodules" - : >"${WRAPPER_GIT_LOG}" - cat >"${cmake_source_}/CMakeLists.txt" </dev/null - - [[ ! -s "${WRAPPER_GIT_LOG}" ]] || { - sed -n '1,240p' "${WRAPPER_GIT_LOG}" >&2 - fail 'undeclared wrapper submodule invoked Git' - } - pass 'wrapper initialization ignores undeclared submodules' -} - -create_fixture -WRAPPER_CMAKE_LOG="${TEST_ROOT}/cmake.log" -WRAPPER_GIT_LOG="${TEST_ROOT}/git.log" -export WRAPPER_CMAKE_LOG WRAPPER_GIT_LOG - -test_default_is_non_mutating -test_update_is_delegated_to_cmake -test_declared_submodule_only - -printf '[SUMMARY] %d tests passed\n' "${PASS_COUNT}" diff --git a/tests/template_test/testDevcontainerJson.py b/tests/template_test/testDevcontainerJson.py deleted file mode 100644 index 6dbba54..0000000 --- a/tests/template_test/testDevcontainerJson.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Regression tests for the devcontainer JSON updater.""" - -from __future__ import annotations - -import importlib.util -import json -import os -import subprocess -import sys -import types -from pathlib import Path - - -def _LoadUpdateModule() -> types.ModuleType: - """Load the devcontainer updater script as a Python module. - - Example: - module_ = _LoadUpdateModule() - print(hasattr(module_, "load_existing")) - # Output: - # True - """ - repoRoot_ = Path(__file__).resolve().parents[2] - modulePath_ = repoRoot_ / ".devcontainer" / "update_devcontainer_json.py" - spec_ = importlib.util.spec_from_file_location( - "update_devcontainer_json", modulePath_) - assert spec_ is not None - assert spec_.loader is not None - - module_ = importlib.util.module_from_spec(spec_) - spec_.loader.exec_module(module_) - return module_ - - -def _RunWriter( - devcontainerJson_: Path, *, cuda_: str, gpuRuntime_: str -) -> dict[str, object]: - """Run the devcontainer updater and return its JSON output. - - Example: - data_ = _RunWriter( - Path("devcontainer.json"), cuda_="off", gpuRuntime_="docker" - ) - print(isinstance(data_, dict)) - # Output: - # True - """ - repoRoot_ = Path(__file__).resolve().parents[2] - writerPath_ = repoRoot_ / ".devcontainer" / "update_devcontainer_json.py" - env_ = os.environ.copy() - env_.update( - { - "CUDA": cuda_, - "CUDA_VERSION": "12.9", - "ROS_MODE": "none", - "ROS_DISTRO": "", - "ROS_PROFILE": "ros-base", - "DEVCONTAINER_JSON_PATH": str(devcontainerJson_), - "DEVCONTAINER_GPU_RUNTIME": gpuRuntime_, - } - ) - - result_ = subprocess.run( - [sys.executable, str(writerPath_)], - check=True, - capture_output=True, - text=True, - env=env_, - ) - data_ = json.loads(result_.stdout) - assert isinstance(data_, dict) - return data_ - - -def _PrepareConfigureWorkspace(tmpPath_: Path) -> Path: - """Create a minimal configure_devcontainer.sh workspace. - - Example: - workspace_ = _PrepareConfigureWorkspace(Path("/tmp/example")) - print((workspace_ / "configure_devcontainer.sh").name) - # Output: - # configure_devcontainer.sh - """ - repoRoot_ = Path(__file__).resolve().parents[2] - workspace_ = tmpPath_ / "workspace" - devcontainerDir_ = workspace_ / ".devcontainer" - devcontainerDir_.mkdir(parents=True) - - (workspace_ / "configure_devcontainer.sh").write_text( - (repoRoot_ / "configure_devcontainer.sh").read_text(encoding="utf-8"), - encoding="utf-8", - ) - (devcontainerDir_ / "update_devcontainer_json.py").write_text( - (repoRoot_ / ".devcontainer" / "update_devcontainer_json.py").read_text( - encoding="utf-8" - ), - encoding="utf-8", - ) - (devcontainerDir_ / "Dockerfile").write_text( - "FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04\n", - encoding="utf-8", - ) - (devcontainerDir_ / "devcontainer.json").write_text( - json.dumps({"name": "Existing", "runArgs": ["--ipc", "host"]}), - encoding="utf-8", - ) - return workspace_ - - -class TestDevcontainerJson: - def test_load_existing_accepts_inline_jsonc_comments(self, tmp_path: Path) -> None: - devcontainerJson_ = tmp_path / "devcontainer.json" - devcontainerJson_.write_text( - """ -{ - // Existing hand-written comments should be tolerated. - "name": "Existing", // Inline comments after properties should also work. - "remoteEnv": { - "DISPLAY": "unix:0", // Preserve unmanaged environment entries. - "DOCS_URL": "https://example.invalid/docs" - } -} -""", - encoding="utf-8", - ) - updateModule_ = _LoadUpdateModule() - - data_ = updateModule_.load_existing(str(devcontainerJson_)) - - assert data_["name"] == "Existing" - assert data_["remoteEnv"]["DISPLAY"] == "unix:0" - assert data_["remoteEnv"]["DOCS_URL"] == "https://example.invalid/docs" - json.dumps(data_) - - def test_cuda_docker_runtime_emits_docker_gpu_args(self, tmp_path: Path) -> None: - devcontainerJson_ = tmp_path / "devcontainer.json" - devcontainerJson_.write_text( - json.dumps( - { - "name": "Existing", - "runArgs": [ - "--device", - "nvidia.com/gpu=all", - "--security-opt=label=disable", - "--ipc", - "host", - ], - } - ), - encoding="utf-8", - ) - - data_ = _RunWriter(devcontainerJson_, cuda_="on", gpuRuntime_="docker") - - assert data_["runArgs"] == ["--gpus", "all", "--ipc", "host"] - - def test_cuda_podman_runtime_emits_cdi_gpu_args(self, tmp_path: Path) -> None: - devcontainerJson_ = tmp_path / "devcontainer.json" - devcontainerJson_.write_text( - json.dumps( - { - "name": "Existing", - "runArgs": ["--gpus", "all", "--ipc", "host"], - } - ), - encoding="utf-8", - ) - - data_ = _RunWriter(devcontainerJson_, cuda_="on", gpuRuntime_="podman") - - assert data_["runArgs"] == [ - "--device", - "nvidia.com/gpu=all", - "--security-opt=label=disable", - "--ipc", - "host", - ] - - def test_cuda_off_removes_all_managed_gpu_args(self, tmp_path: Path) -> None: - devcontainerJson_ = tmp_path / "devcontainer.json" - devcontainerJson_.write_text( - json.dumps( - { - "name": "Existing", - "runArgs": [ - "--gpus=all", - "--device=nvidia.com/gpu=all", - "--security-opt", - "label=disable", - "--ipc", - "host", - ], - } - ), - encoding="utf-8", - ) - - data_ = _RunWriter(devcontainerJson_, cuda_="off", - gpuRuntime_="docker") - - assert data_["runArgs"] == ["--ipc", "host"] - - def test_configure_devcontainer_passes_gpu_runtime_to_writer( - self, tmp_path: Path - ) -> None: - workspace_ = _PrepareConfigureWorkspace(tmp_path) - scriptPath_ = workspace_ / "configure_devcontainer.sh" - - subprocess.run( - [ - "bash", - str(scriptPath_), - "--cuda", - "--gpu-runtime", - "docker", - "--base", - "ubuntu-24.04", - "--non-interactive", - ], - check=True, - cwd=workspace_, - capture_output=True, - text=True, - ) - - data_ = json.loads( - (workspace_ / ".devcontainer" / "devcontainer.json").read_text( - encoding="utf-8" - ) - ) - assert data_["runArgs"] == ["--gpus", "all", "--ipc", "host"] - - subprocess.run( - [ - "bash", - str(scriptPath_), - "--cuda", - "--gpu-runtime", - "podman", - "--base", - "ubuntu-24.04", - "--non-interactive", - ], - check=True, - cwd=workspace_, - capture_output=True, - text=True, - ) - - data_ = json.loads( - (workspace_ / ".devcontainer" / "devcontainer.json").read_text( - encoding="utf-8" - ) - ) - assert data_["runArgs"] == [ - "--device", - "nvidia.com/gpu=all", - "--security-opt=label=disable", - "--ipc", - "host", - ] diff --git a/tests/template_test/testRos2OverlayStatic.py b/tests/template_test/testRos2OverlayStatic.py deleted file mode 100644 index 6ef5349..0000000 --- a/tests/template_test/testRos2OverlayStatic.py +++ /dev/null @@ -1,577 +0,0 @@ -"""Behavioral and structured-data tests for the optional ROS 2 overlay.""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass -from email.headerregistry import Address -import os -from pathlib import Path -import shutil -import stat -import subprocess -import sys -import xml.etree.ElementTree as ET - -import pytest - - -@dataclass(frozen=True) -class CProjectMetadata: - """Project metadata exported by a metadata-only CMake configure. - - Example: - metadata_ = CProjectMetadata("Description", "https://example.test", "A", "a@example.test", "MIT") - print(metadata_.license) - # Output: MIT - """ - - description: str - homepage: str - maintainerName: str - maintainerEmail: str - license: str - - -def _RepoRoot() -> Path: - """Return the template repository root. - - Example: - print(_RepoRoot().name) - # Output: cpp_cuda_template_project - """ - return Path(__file__).resolve().parents[2] - - -def _SkipIfNoRos2(repoRoot_: Path) -> None: - """Skip pytest checks when tailoring removed the overlay. - - Example: - _SkipIfNoRos2(_RepoRoot()) - print("overlay present") - # Output: overlay present - """ - if not (repoRoot_ / "ros2").is_dir(): - pytest.skip("ROS 2 overlay is not present in this tailored project") - - -def _PackageXmlPaths(repoRoot_: Path) -> list[Path]: - """Return immediate ROS package manifests. - - Example: - print(all(path_.name == "package.xml" for path_ in _PackageXmlPaths(_RepoRoot()))) - # Output: True - """ - return sorted((repoRoot_ / "ros2").glob("*/package.xml")) - - -def _IsStrictVersion(version_: str) -> bool: - """Return whether a value is a strict three-component numeric version. - - Example: - print(_IsStrictVersion("1.2.3")) - # Output: True - """ - components_: list[str] = version_.split(".") - return len(components_) == 3 and all( - component_.isdecimal() - and (component_ == "0" or not component_.startswith("0")) - for component_ in components_ - ) - - -def _PackageRoot(packageXml_: Path) -> ET.Element: - """Parse and return a package manifest root element. - - Example: - print(_PackageRoot(_PackageXmlPaths(_RepoRoot())[0]).tag) - # Output: package - """ - root_ = ET.parse(packageXml_).getroot() - assert root_.tag == "package", packageXml_ - return root_ - - -def _PackageVersion(packageXml_: Path) -> str: - """Return a package manifest's semantic version value. - - Example: - print(_PackageVersion(_PackageXmlPaths(_RepoRoot())[0]).count(".")) - # Output: 2 - """ - version_: str | None = _PackageRoot(packageXml_).findtext("version") - assert version_ is not None, packageXml_ - return version_.strip() - - -def _ReadKeyValueFile(filePath_: Path) -> dict[str, str]: - """Read colon-separated generated metadata fields without regular expressions. - - Example: - # fields_ = _ReadKeyValueFile(Path("VERSION")) - # Output: fields_["Project version core"] == "1.11.0" - """ - fields_: dict[str, str] = {} - for line_ in filePath_.read_text(encoding="utf-8").splitlines(): - key_, separator_, value_ = line_.partition(":") - if separator_: - fields_[key_.strip()] = value_.strip() - return fields_ - - -def _ReadVersionCore(versionFile_: Path) -> str: - """Read the strict core value from new or legacy VERSION output. - - Example: - # version_ = _ReadVersionCore(Path("VERSION")) - # Output: version_ == "1.11.0" - """ - fields_ = _ReadKeyValueFile(versionFile_) - for fieldName_ in ("Project version core", "Project version"): - version_: str | None = fields_.get(fieldName_) - if version_ is not None and _IsStrictVersion(version_): - return version_ - raise AssertionError(f"No strict core version found in {versionFile_}") - - -def _ReadCMakeCache(cachePath_: Path) -> dict[str, str]: - """Read generated CMake cache entries into a key/value mapping. - - Example: - # cache_ = _ReadCMakeCache(Path("build/CMakeCache.txt")) - # Output: cache_["CMAKE_PROJECT_NAME"] == "template_project" - """ - cache_: dict[str, str] = {} - for line_ in cachePath_.read_text(encoding="utf-8").splitlines(): - keyAndType_, separator_, value_ = line_.partition("=") - if not separator_ or keyAndType_.startswith(("//", "#")): - continue - key_, typeSeparator_, _ = keyAndType_.partition(":") - if typeSeparator_: - cache_[key_] = value_ - return cache_ - - -def _MetadataFromCache(cachePath_: Path) -> CProjectMetadata: - """Construct project metadata from a generated CMake cache. - - Example: - # metadata_ = _MetadataFromCache(Path("build/CMakeCache.txt")) - # Output: metadata_.homepage starts with "https://" - """ - cache_ = _ReadCMakeCache(cachePath_) - keys_: tuple[str, ...] = ( - "CMAKE_PROJECT_DESCRIPTION", - "CMAKE_PROJECT_HOMEPAGE_URL", - "PROJECT_MAINTAINER_NAME", - "PROJECT_MAINTAINER_EMAIL", - "PROJECT_LICENSE", - ) - for key_ in keys_: - assert cache_.get(key_), (cachePath_, key_) - return CProjectMetadata( - description=cache_["CMAKE_PROJECT_DESCRIPTION"], - homepage=cache_["CMAKE_PROJECT_HOMEPAGE_URL"], - maintainerName=cache_["PROJECT_MAINTAINER_NAME"], - maintainerEmail=cache_["PROJECT_MAINTAINER_EMAIL"], - license=cache_["PROJECT_LICENSE"], - ) - - -def _DescriptionSuffix(packageName_: str) -> str: - """Return the role-specific description suffix for a ROS package. - - Example: - print(_DescriptionSuffix("demo_interfaces")) - # Output: ROS 2 message and service interfaces. - """ - if packageName_.endswith("_interfaces"): - return "ROS 2 message and service interfaces." - if packageName_.endswith("_ros"): - return "ROS 2 bridge package." - if packageName_.endswith("_spinup"): - return "ROS 2 launch and runtime assets." - return "ROS 2 colcon shim package." - - -def ValidateRos2Manifests( - repoRoot_: Path, - expectedVersion_: str, - metadataCachePath_: Path | None = None, -) -> None: - """Validate ROS manifests through ElementTree and optional CMake metadata. - - Example: - # ValidateRos2Manifests(_RepoRoot(), "1.11.0") - # Output: returns without error when all manifests match - """ - assert _IsStrictVersion(expectedVersion_), expectedVersion_ - packagePaths_ = _PackageXmlPaths(repoRoot_) - assert packagePaths_, repoRoot_ - metadata_: CProjectMetadata | None = ( - _MetadataFromCache(metadataCachePath_) - if metadataCachePath_ is not None - else None - ) - - for packagePath_ in packagePaths_: - root_ = _PackageRoot(packagePath_) - packageName_: str | None = root_.findtext("name") - assert packageName_ == packagePath_.parent.name, packagePath_ - assert _PackageVersion(packagePath_) == expectedVersion_, packagePath_ - - description_: str | None = root_.findtext("description") - license_: str | None = root_.findtext("license") - maintainer_ = root_.find("maintainer") - assert description_ and description_.strip(), packagePath_ - assert license_ and license_.strip(), packagePath_ - assert maintainer_ is not None and maintainer_.text, packagePath_ - assert maintainer_.get("email"), packagePath_ - websiteUrls_: list[str] = [ - (url_.text or "").strip() - for url_ in root_.findall("url") - if url_.get("type") == "website" - ] - assert len(websiteUrls_) == 1 and websiteUrls_[0].startswith("https://"), ( - packagePath_, - websiteUrls_, - ) - - if metadata_ is None: - continue - descriptionBase_ = metadata_.description.removesuffix(".") - assert description_ == ( - f"{descriptionBase_}: {_DescriptionSuffix(packageName_)}" - ), packagePath_ - assert maintainer_.text == metadata_.maintainerName, packagePath_ - assert maintainer_.get("email") == metadata_.maintainerEmail, packagePath_ - assert license_ == metadata_.license, packagePath_ - assert websiteUrls_ == [metadata_.homepage], packagePath_ - - -def _PrepareManifestFixture( - sourcePath_: Path, targetPath_: Path, packagePrefix_: str, addRepositoryUrl_: bool -) -> tuple[str, int]: - """Create a stale copied-manifest fixture using ElementTree. - - Example: - # name_, mode_ = _PrepareManifestFixture(source_, target_, "demo", False) - # Output: target_ contains stale metadata while preserving its XML preamble - """ - sourceText_ = sourcePath_.read_text(encoding="utf-8") - packageIndex_ = sourceText_.index(" None: - repoRoot_ = _RepoRoot() - metadataBuild_ = tmp_path / "metadata_build" - result_ = subprocess.run( - [ - "cmake", - "-S", - str(repoRoot_), - "-B", - str(metadataBuild_), - "-DPROJECT_METADATA_ONLY=ON", - ], - cwd=repoRoot_, - check=False, - capture_output=True, - text=True, - ) - assert result_.returncode == 0, (result_.stdout, result_.stderr) - cachePath_ = metadataBuild_ / "CMakeCache.txt" - metadata_ = _MetadataFromCache(cachePath_) - assert metadata_.homepage.startswith("https://") - maintainerAddress_ = Address(addr_spec=metadata_.maintainerEmail) - assert maintainerAddress_.username - assert maintainerAddress_.domain - assert "CMAKE_CXX_COMPILER" not in _ReadCMakeCache(cachePath_) - assert not (metadataBuild_ / "src").exists() - - def test_packageMetadataMatchesRootProject(self, tmp_path: Path) -> None: - repoRoot_ = _RepoRoot() - _SkipIfNoRos2(repoRoot_) - metadataBuild_ = tmp_path / "metadata_build" - subprocess.run( - [ - "cmake", - "-S", - str(repoRoot_), - "-B", - str(metadataBuild_), - "-DPROJECT_METADATA_ONLY=ON", - ], - check=True, - capture_output=True, - text=True, - ) - versionFile_ = repoRoot_ / "VERSION" - expectedVersion_ = ( - _ReadVersionCore(versionFile_) - if versionFile_.exists() - else next(iter({_PackageVersion(path_) for path_ in _PackageXmlPaths(repoRoot_)})) - ) - ValidateRos2Manifests( - repoRoot_, expectedVersion_, metadataBuild_ / "CMakeCache.txt" - ) - - def test_colconIgnoreMarkersArePresent(self) -> None: - repoRoot_ = _RepoRoot() - _SkipIfNoRos2(repoRoot_) - for marker_ in ( - "python/COLCON_IGNORE", - "lib/COLCON_IGNORE", - "examples/COLCON_IGNORE", - "tests/COLCON_IGNORE", - ): - assert (repoRoot_ / marker_).is_file(), marker_ - - def test_buildScriptFailsBeforeMutationWithoutRosEnvironment(self) -> None: - repoRoot_ = _RepoRoot() - _SkipIfNoRos2(repoRoot_) - generatedPaths_: tuple[Path, ...] = tuple( - repoRoot_ / "ros2" / name_ for name_ in ("build", "install", "log") - ) - existedBefore_: dict[Path, bool] = { - path_: path_.exists() for path_ in generatedPaths_ - } - environment_: dict[str, str] = dict(os.environ) - environment_["ROS_DISTRO"] = "template_contract_missing" - result_ = subprocess.run( - ["bash", str(repoRoot_ / "build_ros2.sh"), "--skip-tests"], - cwd=repoRoot_, - env=environment_, - check=False, - capture_output=True, - text=True, - ) - assert result_.returncode != 0 - assert {path_: path_.exists() for path_ in generatedPaths_} == existedBefore_ - - def test_generateVersionSyncsCopiedRosPackageMetadata(self, tmp_path: Path) -> None: - repoRoot_ = _RepoRoot() - _SkipIfNoRos2(repoRoot_) - - scriptCopy_ = tmp_path / "generate_version.sh" - shutil.copy2(repoRoot_ / "generate_version.sh", scriptCopy_) - scriptCopy_.chmod(0o755) - helperCopy_ = tmp_path / "ros2/tools/sync_package_metadata.py" - helperCopy_.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(repoRoot_ / "ros2/tools/sync_package_metadata.py", helperCopy_) - - scratchDescription_ = "Scratch project metadata used by the ROS overlay test." - scratchHomepage_ = "https://example.test/space-nav-frontend" - scratchMaintainer_ = "Scratch Maintainer" - scratchEmail_ = "maintainer@example.test" - scratchLicense_ = "Apache-2.0" - (tmp_path / "CMakeLists.txt").write_text( - "\n".join( - ( - "cmake_minimum_required(VERSION 3.15)", - f'set(project_description "{scratchDescription_}")', - f'set(project_homepage_url "{scratchHomepage_}")', - f'set(PROJECT_MAINTAINER_NAME "{scratchMaintainer_}" CACHE STRING "")', - f'set(PROJECT_MAINTAINER_EMAIL "{scratchEmail_}" CACHE STRING "")', - f'set(PROJECT_LICENSE "{scratchLicense_}" CACHE STRING "")', - "project(space-nav-frontend", - " VERSION 9.8.7", - ' DESCRIPTION "${project_description}"', - ' HOMEPAGE_URL "${project_homepage_url}"', - " LANGUAGES NONE)", - "", - ) - ), - encoding="utf-8", - ) - (tmp_path / "VERSION").write_text( - "\n".join( - ( - "Project version: 9.8.7", - "Project version core: 9.8.7", - "Project version prerelease: ", - "Project version metadata: ", - "Full version: 9.8.7", - "", - ) - ), - encoding="utf-8", - ) - - expectedModes_: dict[Path, int] = {} - expectedNames_: list[str] = [] - for index_, packagePath_ in enumerate(_PackageXmlPaths(repoRoot_)): - tailoredPackageName_ = packagePath_.parent.name.replace( - "template_project", "snf" - ) - targetPath_ = tmp_path / "ros2" / tailoredPackageName_ / "package.xml" - packageName_, _ = _PrepareManifestFixture( - packagePath_, targetPath_, "snf", index_ == 0 - ) - targetPath_.chmod(0o664 if index_ == 0 else 0o640) - expectedModes_[targetPath_] = stat.S_IMODE(targetPath_.stat().st_mode) - expectedNames_.append(packageName_) - - result_ = subprocess.run( - ["bash", str(scriptCopy_)], - cwd=tmp_path, - check=False, - capture_output=True, - text=True, - ) - assert result_.returncode == 0, (result_.stdout, result_.stderr) - - metadataBuild_ = tmp_path / "metadata_validation" - subprocess.run( - [ - "cmake", - "-S", - str(tmp_path), - "-B", - str(metadataBuild_), - "-DPROJECT_METADATA_ONLY=ON", - ], - check=True, - capture_output=True, - text=True, - ) - ValidateRos2Manifests( - tmp_path, - _ReadVersionCore(tmp_path / "VERSION"), - metadataBuild_ / "CMakeCache.txt", - ) - - syncedPaths_ = sorted((tmp_path / "ros2").glob("*/package.xml")) - assert [_PackageRoot(path_).findtext("name") for path_ in syncedPaths_] == ( - expectedNames_ - ) - repositoryUrls_: list[str | None] = [ - url_.text - for url_ in _PackageRoot(tmp_path / "ros2/snf/package.xml").findall("url") - if url_.get("type") == "repository" - ] - assert repositoryUrls_ == ["https://example.test/source.git"] - bridgeDependencies_: set[str] = { - dependency_.text or "" - for dependency_ in _PackageRoot( - tmp_path / "ros2/snf_ros/package.xml" - ).findall("depend") - } - assert {"snf", "snf_interfaces"} <= bridgeDependencies_ - assert { - path_: stat.S_IMODE(path_.stat().st_mode) for path_ in syncedPaths_ - } == expectedModes_ - - # The XML model instruction is generated representation deliberately - # preserved byte-for-byte by the synchronizer, so an exact marker check - # is appropriate here. - assert all( - " None: - repoRoot_ = _RepoRoot() - _SkipIfNoRos2(repoRoot_) - - scriptCopy_ = tmp_path / "generate_version.sh" - shutil.copy2(repoRoot_ / "generate_version.sh", scriptCopy_) - helperCopy_ = tmp_path / "ros2/tools/sync_package_metadata.py" - helperCopy_.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(repoRoot_ / "ros2/tools/sync_package_metadata.py", helperCopy_) - - packageSource_ = _PackageXmlPaths(repoRoot_)[0] - packageTarget_ = tmp_path / "ros2" / packageSource_.parent.name / "package.xml" - packageTarget_.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(packageSource_, packageTarget_) - bytesBefore_ = packageTarget_.read_bytes() - (tmp_path / "VERSION").write_text( - "Project version core: 9.8.7\n" - "Project version prerelease: \n" - "Project version metadata: \n" - "Full version: 9.8.7\n", - encoding="utf-8", - ) - - result_ = subprocess.run( - ["bash", str(scriptCopy_), "--no-sync-ros2"], - cwd=tmp_path, - check=False, - capture_output=True, - text=True, - ) - - assert result_.returncode == 0, (result_.stdout, result_.stderr) - assert packageTarget_.read_bytes() == bytesBefore_ - - -def _Main(arguments_: list[str]) -> int: - """Run the manifest validator for CMake release fixtures. - - Example: - # _Main(["--repo-root", ".", "--expected-version", "1.11.0"]) - # Output: 0 when manifests satisfy the contract - """ - parser_ = argparse.ArgumentParser() - parser_.add_argument("--repo-root", type=Path, required=True) - parser_.add_argument("--expected-version", required=True) - parser_.add_argument("--metadata-cache", type=Path) - options_ = parser_.parse_args(arguments_) - ValidateRos2Manifests( - options_.repo_root, - options_.expected_version, - options_.metadata_cache, - ) - return 0 - - -if __name__ == "__main__": - sys.exit(_Main(sys.argv[1:])) diff --git a/tests/template_test/testWorkflowTemplates.py b/tests/template_test/testWorkflowTemplates.py deleted file mode 100644 index bb0194d..0000000 --- a/tests/template_test/testWorkflowTemplates.py +++ /dev/null @@ -1,524 +0,0 @@ -"""Parser-backed and behavioral contracts for GitHub workflows.""" - -from __future__ import annotations - -import json -import os -from pathlib import Path -import subprocess -from typing import cast - -import yaml - - -_WORKFLOW_NAMES: tuple[str, ...] = ( - "build_linux.yml", - "build_linux_cuda.yml", - "docs_pages.yml", - "build_ros2_overlay.yml", -) - - -def _RepoRoot() -> Path: - """Return the template repository root. - - Example: - print(_RepoRoot().name) - # Output: cpp_cuda_template_project - """ - return Path(__file__).resolve().parents[2] - - -def _LoadWorkflow(workflowPath_: Path) -> dict[str, object]: - """Load one workflow through PyYAML. - - Example: - workflow_ = _LoadWorkflow(_RepoRoot() / ".github/workflows/build_linux.yml") - print("jobs" in workflow_) - # Output: True - """ - parsed_: object = yaml.safe_load(workflowPath_.read_text(encoding="utf-8")) - assert isinstance(parsed_, dict), workflowPath_ - return cast(dict[str, object], parsed_) - - -def _WorkflowTriggers(workflowPath_: Path) -> dict[str, object]: - """Return the parsed trigger mapping for one workflow. - - Example: - triggers_ = _WorkflowTriggers( - _RepoRoot() / ".github/workflows/build_linux.yml" - ) - print("push" in triggers_) - # Output: True - """ - workflow_ = _LoadWorkflow(workflowPath_) - workflowObjects_ = cast(dict[object, object], workflow_) - triggers_: object = workflowObjects_.get("on", workflowObjects_.get(True)) - assert isinstance(triggers_, dict), workflowPath_ - return cast(dict[str, object], triggers_) - - -def _Jobs(workflowPath_: Path) -> dict[str, dict[str, object]]: - """Return the parsed jobs keyed by job identifier. - - Example: - jobs_ = _Jobs(_RepoRoot() / ".github/workflows/docs_pages.yml") - print("build-docs" in jobs_) - # Output: True - """ - jobsRaw_: object = _LoadWorkflow(workflowPath_).get("jobs") - assert isinstance(jobsRaw_, dict), workflowPath_ - jobs_: dict[str, dict[str, object]] = {} - for jobName_, jobRaw_ in jobsRaw_.items(): - assert isinstance(jobName_, str), workflowPath_ - assert isinstance(jobRaw_, dict), (workflowPath_, jobName_) - jobs_[jobName_] = cast(dict[str, object], jobRaw_) - return jobs_ - - -def _Steps(job_: dict[str, object]) -> list[dict[str, object]]: - """Return a job's parsed step mappings. - - Example: - job_ = _Jobs(_RepoRoot() / ".github/workflows/docs_pages.yml")["build-docs"] - print(len(_Steps(job_)) > 0) - # Output: True - """ - stepsRaw_: object = job_.get("steps") - assert isinstance(stepsRaw_, list), job_ - steps_: list[dict[str, object]] = [] - for stepRaw_ in stepsRaw_: - assert isinstance(stepRaw_, dict), stepRaw_ - steps_.append(cast(dict[str, object], stepRaw_)) - return steps_ - - -def _StepById(job_: dict[str, object], stepId_: str) -> dict[str, object]: - """Return the uniquely identified step from a parsed job. - - Example: - job_ = _Jobs(_RepoRoot() / ".github/workflows/docs_pages.yml")["build-docs"] - print(_StepById(job_, "build_docs")["id"]) - # Output: build_docs - """ - matches_: list[dict[str, object]] = [ - step_ for step_ in _Steps(job_) if step_.get("id") == stepId_ - ] - assert len(matches_) == 1, (stepId_, matches_) - return matches_[0] - - -def _TriggerPaths(workflowPath_: Path, eventName_: str) -> list[str]: - """Return a branch event's parsed path filter. - - Example: - paths_ = _TriggerPaths( - _RepoRoot() / ".github/workflows/build_linux.yml", "push" - ) - print("CMakeLists.txt" in paths_) - # Output: True - """ - event_: object = _WorkflowTriggers(workflowPath_).get(eventName_) - assert isinstance(event_, dict), (workflowPath_, eventName_) - paths_: object = event_.get("paths") - assert isinstance(paths_, list), (workflowPath_, eventName_) - assert all(isinstance(path_, str) for path_ in paths_), paths_ - return cast(list[str], paths_) - - -def _InitializeManifestRepository(repositoryRoot_: Path) -> None: - """Create a committed ROS manifest fixture. - - Example: - # _InitializeManifestRepository(Path("/tmp/workflow-contract")) - # Output: a Git repository containing ros2/demo/package.xml - """ - manifestPath_ = repositoryRoot_ / "ros2/demo/package.xml" - manifestPath_.parent.mkdir(parents=True) - manifestPath_.write_text( - "demo1.2.3\n", - encoding="utf-8", - ) - subprocess.run(["git", "init", "--quiet", str(repositoryRoot_)], check=True) - subprocess.run( - ["git", "-C", str(repositoryRoot_), "add", "ros2/demo/package.xml"], - check=True, - ) - subprocess.run( - [ - "git", - "-C", - str(repositoryRoot_), - "-c", - "user.name=Workflow Contract", - "-c", - "user.email=workflow-contract@example.invalid", - "commit", - "--quiet", - "-m", - "Record manifest", - ], - check=True, - ) - - -def _WriteMetadataHelper( - repositoryRoot_: Path, *, includeCapabilityMarkers_: bool = True -) -> None: - """Write an executable fake metadata helper for workflow execution. - - Example: - # _WriteMetadataHelper(Path("/tmp/workflow-contract")) - # Output: executable generate_version.sh - """ - helperPath_ = repositoryRoot_ / "generate_version.sh" - capabilityMarkers_ = "" - if includeCapabilityMarkers_: - capabilityMarkers_ = ( - "# Contract fixture supports --sync-ros2.\n" - "ROS2_PROJECT_METADATA_SYNC=1\n" - ) - helperPath_.write_text( - "#!/usr/bin/env bash\n" - + capabilityMarkers_ - + """if [[ "${MUTATE_MANIFEST:-0}" == "1" ]]; then - python3 - <<'PY' -from pathlib import Path -import xml.etree.ElementTree as ET -path_ = Path("ros2/demo/package.xml") -tree_ = ET.parse(path_) -version_ = tree_.getroot().find("version") -assert version_ is not None -version_.text = "1.2.4" -tree_.write(path_, encoding="unicode") -PY -fi -""", - encoding="utf-8", - ) - helperPath_.chmod(0o755) - - -def _RunMetadataStep( - step_: dict[str, object], repositoryRoot_: Path, mutateManifest_: bool -) -> subprocess.CompletedProcess[str]: - """Execute a workflow metadata-sync step in a Git fixture. - - Example: - # result_ = _RunMetadataStep(step_, repositoryRoot_, False) - # Output: result_.returncode == 0 - """ - runBlock_: object = step_.get("run") - assert isinstance(runBlock_, str), step_ - environment_: dict[str, str] = dict(os.environ) - environment_["GITHUB_WORKSPACE"] = str(repositoryRoot_) - environment_["MUTATE_MANIFEST"] = "1" if mutateManifest_ else "0" - return subprocess.run( - ["bash", "-Eeuo", "pipefail", "-c", runBlock_], - cwd=repositoryRoot_, - env=environment_, - check=False, - capture_output=True, - text=True, - ) - - -class TestWorkflowTemplates: - def test_activeAndDormantWorkflowPairsParseAsYaml(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - for workflowName_ in _WORKFLOW_NAMES: - for workflowPath_ in ( - workflowRoot_ / workflowName_, - workflowRoot_ / f"{workflowName_}.tpl", - ): - assert workflowPath_.is_file(), workflowPath_ - assert _Jobs(workflowPath_), workflowPath_ - - def test_releaseTagsRunNativeAndRosWorkflows(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - for workflowName_ in ( - "build_linux.yml", - "build_linux_cuda.yml", - "build_ros2_overlay.yml", - ): - for workflowPath_ in ( - workflowRoot_ / workflowName_, - workflowRoot_ / f"{workflowName_}.tpl", - ): - push_: object = _WorkflowTriggers(workflowPath_).get("push") - assert isinstance(push_, dict), workflowPath_ - assert push_.get("tags") == ["v*.*.*"], workflowPath_ - - def test_cudaJobsRequireExplicitSelfHostedRunnerOptIn(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - expectedCondition_ = "${{ vars.CI_USE_SELF_HOSTED == 'true' }}" - for workflowPath_ in ( - workflowRoot_ / "build_linux_cuda.yml", - workflowRoot_ / "build_linux_cuda.yml.tpl", - ): - for jobName_, job_ in _Jobs(workflowPath_).items(): - assert jobName_ in {"build", "test"}, workflowPath_ - assert job_.get("if") == expectedCondition_, ( - workflowPath_, - jobName_, - ) - - def test_branchPathFiltersOwnTheirSemanticTests(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - nativePaths_: tuple[Path, ...] = ( - workflowRoot_ / "build_linux.yml", - workflowRoot_ / "build_linux.yml.tpl", - ) - rosPaths_: tuple[Path, ...] = ( - workflowRoot_ / "build_ros2_overlay.yml", - workflowRoot_ / "build_ros2_overlay.yml.tpl", - ) - for workflowPath_ in nativePaths_: - for eventName_ in ("push", "pull_request"): - assert "generate_version.sh" in _TriggerPaths(workflowPath_, eventName_) - - for workflowPath_ in rosPaths_: - assert "workflow_dispatch" in _WorkflowTriggers(workflowPath_) - for eventName_ in ("push", "pull_request"): - paths_ = _TriggerPaths(workflowPath_, eventName_) - assert {"CMakeLists.txt", "cmake/**", "src/**"} <= set(paths_) - - activeRos_ = workflowRoot_ / "build_ros2_overlay.yml" - for eventName_ in ("push", "pull_request"): - paths_ = _TriggerPaths(activeRos_, eventName_) - assert "tests/template_test/testWorkflowTemplates.py" in paths_ - assert "tests/template_test/testRos2OverlayStatic.py" in paths_ - - docsWorkflow_ = workflowRoot_ / "docs_pages.yml" - for eventName_ in ("push", "pull_request"): - paths_ = _TriggerPaths(docsWorkflow_, eventName_) - assert "README.md" in paths_ - assert "tests/template_test/testWorkflowTemplates.py" in paths_ - - def test_workflowTopologySeparatesTemplateAndGenericJobs(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - expectedJobs_: dict[str, tuple[set[str], set[str]]] = { - "build_linux.yml": ( - {"build", "test", "tailored-project-validation"}, - {"build", "test"}, - ), - "build_linux_cuda.yml": ({"build", "test"}, {"build", "test"}), - "docs_pages.yml": ({"build-docs", "deploy"}, {"build-docs", "deploy"}), - "build_ros2_overlay.yml": ( - {"overlay-build", "rollout-rehearsal"}, - {"overlay-build"}, - ), - } - for workflowName_, (activeJobs_, genericJobs_) in expectedJobs_.items(): - assert set(_Jobs(workflowRoot_ / workflowName_)) == activeJobs_ - assert set(_Jobs(workflowRoot_ / f"{workflowName_}.tpl")) == genericJobs_ - - activeDocs_ = _Jobs(workflowRoot_ / "docs_pages.yml")["build-docs"] - genericDocs_ = _Jobs(workflowRoot_ / "docs_pages.yml.tpl")["build-docs"] - assert _StepById(activeDocs_, "workflow_contracts") - assert all( - step_.get("id") != "workflow_contracts" - for step_ in _Steps(genericDocs_) - ) - - activeCuda_ = _Jobs(workflowRoot_ / "build_linux_cuda.yml")["build"] - genericCuda_ = _Jobs(workflowRoot_ / "build_linux_cuda.yml.tpl")["build"] - assert _StepById(activeCuda_, "workflow_contracts") - assert _StepById(activeCuda_, "materialize_tailored_project") - genericCudaIds_ = {step_.get("id") for step_ in _Steps(genericCuda_)} - assert "workflow_contracts" not in genericCudaIds_ - assert "materialize_tailored_project" not in genericCudaIds_ - - def test_everyCheckoutFetchesFullHistory(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - for workflowName_ in _WORKFLOW_NAMES: - for workflowPath_ in ( - workflowRoot_ / workflowName_, - workflowRoot_ / f"{workflowName_}.tpl", - ): - checkoutCount_ = 0 - for job_ in _Jobs(workflowPath_).values(): - for step_ in _Steps(job_): - uses_: object = step_.get("uses") - if not isinstance(uses_, str) or not uses_.startswith( - "actions/checkout@" - ): - continue - checkoutCount_ += 1 - with_: object = step_.get("with") - assert isinstance(with_, dict), (workflowPath_, step_) - assert with_.get("fetch-depth") == 0, (workflowPath_, step_) - assert checkoutCount_ > 0, workflowPath_ - - def test_shellRunBlocksParse(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - for workflowName_ in _WORKFLOW_NAMES: - for workflowPath_ in ( - workflowRoot_ / workflowName_, - workflowRoot_ / f"{workflowName_}.tpl", - ): - for jobId_, job_ in _Jobs(workflowPath_).items(): - for stepIndex_, step_ in enumerate(_Steps(job_)): - runBlock_: object = step_.get("run") - if not isinstance(runBlock_, str): - continue - result_ = subprocess.run( - ["bash", "-n"], - input=runBlock_, - check=False, - capture_output=True, - text=True, - ) - assert result_.returncode == 0, ( - workflowPath_, - jobId_, - stepIndex_, - result_.stderr, - ) - - def test_docsWorkflowUsesCurrentPagesActions(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - for workflowPath_ in ( - workflowRoot_ / "docs_pages.yml", - workflowRoot_ / "docs_pages.yml.tpl", - ): - jobs_ = _Jobs(workflowPath_) - buildSteps_ = _Steps(jobs_["build-docs"]) - deploySteps_ = _Steps(jobs_["deploy"]) - assert any( - step_.get("uses") == "actions/upload-pages-artifact@v5" - for step_ in buildSteps_ - ) - assert any( - step_.get("uses") == "actions/configure-pages@v6" - for step_ in deploySteps_ - ) - assert any( - step_.get("uses") == "actions/deploy-pages@v5" - for step_ in deploySteps_ - ) - - def test_rosJobOrderIsRepresentedStructurally(self) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - activeJobs_ = _Jobs(workflowRoot_ / "build_ros2_overlay.yml") - genericJobs_ = _Jobs(workflowRoot_ / "build_ros2_overlay.yml.tpl") - - for job_ in (*activeJobs_.values(), *genericJobs_.values()): - container_: object = job_.get("container") - assert isinstance(container_, dict), job_ - assert container_.get("image") == "ros:jazzy" - stepIds_: list[object] = [step_.get("id") for step_ in _Steps(job_)] - orderedIds_: tuple[str, ...] = ( - "checkout_repository", - "trust_worktree", - "install_dependencies", - "sync_metadata", - "resolve_dependencies", - ) - indices_: list[int] = [stepIds_.index(id_) for id_ in orderedIds_] - assert indices_ == sorted(indices_) - - assert _StepById(activeJobs_["overlay-build"], "static_contracts") - assert _StepById(activeJobs_["rollout-rehearsal"], "additive_rollout") - genericIds_ = { - step_.get("id") for step_ in _Steps(genericJobs_["overlay-build"]) - } - assert "static_contracts" not in genericIds_ - assert "additive_rollout" not in genericIds_ - - def test_metadataSyncStepsRejectManifestDrift(self, tmp_path: Path) -> None: - workflowRoot_ = _RepoRoot() / ".github/workflows" - activeJobs_ = _Jobs(workflowRoot_ / "build_ros2_overlay.yml") - genericJob_ = _Jobs(workflowRoot_ / "build_ros2_overlay.yml.tpl")[ - "overlay-build" - ] - - steps_: list[tuple[str, dict[str, object]]] = [ - ( - f"active-{jobName_}", - _StepById(job_, "sync_metadata"), - ) - for jobName_, job_ in activeJobs_.items() - ] - steps_.append( - ( - "generic-overlay-build", - _StepById(genericJob_, "sync_metadata"), - ) - ) - - for fixtureName_, step_ in steps_: - cleanRoot_ = tmp_path / f"{fixtureName_}-clean" - _InitializeManifestRepository(cleanRoot_) - _WriteMetadataHelper(cleanRoot_) - cleanResult_ = _RunMetadataStep(step_, cleanRoot_, False) - assert cleanResult_.returncode == 0, ( - cleanResult_.stdout, - cleanResult_.stderr, - ) - - dirtyRoot_ = tmp_path / f"{fixtureName_}-dirty" - _InitializeManifestRepository(dirtyRoot_) - _WriteMetadataHelper(dirtyRoot_) - dirtyResult_ = _RunMetadataStep(step_, dirtyRoot_, True) - assert dirtyResult_.returncode != 0 - - compatibleRoot_ = tmp_path / "generic-compatible-skip" - _InitializeManifestRepository(compatibleRoot_) - compatibleResult_ = _RunMetadataStep( - _StepById(genericJob_, "sync_metadata"), - compatibleRoot_, - False, - ) - assert compatibleResult_.returncode == 0, compatibleResult_.stderr - - def test_activeMetadataSyncExecutesMarkerFreeHelper( - self, tmp_path: Path - ) -> None: - activeJobs_ = _Jobs( - _RepoRoot() / ".github/workflows/build_ros2_overlay.yml" - ) - for jobName_, job_ in activeJobs_.items(): - repositoryRoot_ = tmp_path / jobName_ - _InitializeManifestRepository(repositoryRoot_) - _WriteMetadataHelper( - repositoryRoot_, includeCapabilityMarkers_=False - ) - result_ = _RunMetadataStep( - _StepById(job_, "sync_metadata"), repositoryRoot_, False - ) - assert result_.returncode == 0, ( - result_.stdout, - result_.stderr, - ) - - def test_structuredRepositoryConfigurationParses(self) -> None: - repoRoot_ = _RepoRoot() - presets_: object = json.loads( - (repoRoot_ / "CMakePresets.json").read_text(encoding="utf-8") - ) - assert isinstance(presets_, dict) - configurePresets_: object = presets_.get("configurePresets") - buildPresets_: object = presets_.get("buildPresets") - assert isinstance(configurePresets_, list) - assert isinstance(buildPresets_, list) - assert any( - isinstance(preset_, dict) and preset_.get("name") == "docs" - for preset_ in configurePresets_ - ) - assert any( - isinstance(preset_, dict) - and preset_.get("name") == "docs" - and preset_.get("targets") == ["doc"] - for preset_ in buildPresets_ - ) - - issueRoot_ = repoRoot_ / ".github/ISSUE_TEMPLATE" - for issuePath_ in ( - issueRoot_ / "bug_report.yml", - issueRoot_ / "feature_request.yml", - issueRoot_ / "config.yml", - ): - parsed_: object = yaml.safe_load(issuePath_.read_text(encoding="utf-8")) - assert isinstance(parsed_, dict), issuePath_