diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 9a463d5..f2475d5 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -10,14 +10,13 @@ on: workflow_dispatch: schedule: - cron: '30 15 * * *' - - cron: "0 0 * * 0" jobs: beman-submodule-check: - uses: ./.github/workflows/reusable-beman-submodule-check.yml + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.2.1 preset-test: - uses: ./.github/workflows/reusable-beman-preset-test.yml + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.2.1 with: matrix_config: > [ @@ -25,10 +24,14 @@ jobs: {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "appleclang-debug", "runner": "macos-latest"}, + {"preset": "appleclang-release", "runner": "macos-latest"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} ] build-and-test: - uses: ./.github/workflows/reusable-beman-build-and-test.yml + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.2.1 with: matrix_config: > { @@ -51,7 +54,7 @@ jobs: } ] }, - { "versions": ["14", "13"], + { "versions": ["14"], "tests": [ { "cxxversions": ["c++26", "c++23", "c++20"], "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] @@ -60,11 +63,11 @@ jobs: } ], "clang": [ - { "versions": ["20"], + { "versions": ["21"], "tests": [ {"cxxversions": ["c++26"], "tests": [ - { "stdlibs": ["libstdc++"], + { "stdlibs": ["libc++"], "tests": [ "Debug.Default", "Release.Default", "Release.TSan", "Release.MaxSan", "Debug.Werror", "Debug.Dynamic" @@ -74,16 +77,38 @@ jobs: }, { "cxxversions": ["c++23", "c++20"], "tests": [ - {"stdlibs": ["libstdc++"], "tests": ["Release.Default"]} + {"stdlibs": ["libc++"], "tests": ["Release.Default"]} ] } ] }, - { "versions": ["19"], + { "versions": ["20", "19"], "tests": [ { "cxxversions": ["c++26", "c++23", "c++20"], "tests": [ - {"stdlibs": ["libstdc++"], "tests": ["Release.Default"]} + {"stdlibs": ["libc++"], "tests": ["Release.Default"]} + ] + } + ] + } + ], + "appleclang": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23", "c++20"], + "tests": [{ "stdlibs": ["libc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "msvc": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23"], + "tests": [ + { "stdlibs": ["stl"], + "tests": ["Debug.Default", "Release.Default", "Release.MaxSan"] + } ] } ] @@ -93,9 +118,5 @@ jobs: create-issue-when-fault: needs: [preset-test, build-and-test] - if: failure() && github.event.schedule == '30 15 * * *' - uses: ./.github/workflows/reusable-beman-create-issue-when-fault.yml - - auto-update-pre-commit: - if: github.event.schedule == '00 16 * * 0' - uses: ./.github/workflows/reusable-beman-update-pre-commit.yml + if: failure() && github.event_name == 'schedule' + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.2.1 diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..2f52cbf --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,79 @@ +--- +name: CMake + +on: + push: + branches: ["develop"] + pull_request: + branches: ["develop"] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + schedule: + # run at 15:30 on day-of-month 7. + - cron: '30 15 7 * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: release + CTEST_OUTPUT_ON_FAILURE: 1 + +jobs: + build: + strategy: + fail-fast: false + + matrix: + os: [windows] + include: + - { os: macos, uname: appleclang } + # XXX - { os: ubuntu, uname: gcc } + - { os: ubuntu, uname: llvm } + - { os: windows, uname: msvc } + + # TODO(CK): + # type: [shared, static] + # include: + # - { type: shared, shared: YES } + # - { type: static, shared: NO } + + runs-on: ${{ matrix.os }}-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup build environment + uses: lukka/get-cmake@latest + with: + cmakeVersion: "~4.2.1" + ninjaVersion: "^1.13.0" + + - name: Setup MSVC + if: startsWith(matrix.os, 'windows') + uses: TheMrMilchmann/setup-msvc-dev@v3 + with: + arch: x64 + + - name: Setup Cpp + if: matrix.os != 'windows' + uses: aminya/setup-cpp@v1 + with: + # XXX compiler: ${{matrix.uname}} + compiler: llvm + + - name: Configure CMake + run: cmake --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} --log-level=VERBOSE # XXX -Wdev + + - name: Build + # Build your program with the given configuration + run: cmake --build --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} + + - name: Test + # Execute tests defined by the CMake configuration + run: ctest --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} + + # - name: Install + # # Install the project artefacts to CMAKE_INSTALL_PREFIX + # run: cmake --build --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} --target install diff --git a/.github/workflows/pre-commit-check.yml b/.github/workflows/pre-commit-check.yml new file mode 100644 index 0000000..5749343 --- /dev/null +++ b/.github/workflows/pre-commit-check.yml @@ -0,0 +1,13 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-pre-commit.yml@1.2.1 diff --git a/.github/workflows/pre-commit-update.yml b/.github/workflows/pre-commit-update.yml new file mode 100644 index 0000000..9261dbf --- /dev/null +++ b/.github/workflows/pre-commit-update.yml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Weekly pre-commit autoupdate + +on: + workflow_dispatch: + schedule: + - cron: "0 16 * * 0" + +jobs: + auto-update-pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.2.1 + secrets: + APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} + PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} diff --git a/.gitignore b/.gitignore index baadefc..0e4b215 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ -.cache +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + /compile_commands.json +/CMakeUserPresets.json /build +**/_deps/ +**/CMakeFiles/ +/cmake/presets # ignore emacs temp files *~ @@ -8,3 +13,4 @@ # ignore vscode settings .vscode +.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4daacd..cecf800 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,14 @@ repos: # This brings in a portable version of clang-format. # See also: https://github.com/ssciwr/clang-format-wheel - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.6 + rev: v21.1.8 hooks: - id: clang-format types_or: [c++, c, json] # CMake linting and formatting - repo: https://github.com/BlankSpruce/gersemi - rev: 0.23.1 + rev: 0.25.2 hooks: - id: gersemi name: CMake linting diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b36f29..a434572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,16 +11,20 @@ project( # gersemi: off -# Modules opt in only on compilers that support g++-15 and clang-20+ -if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 20) +# Modules opt in only on compilers that support it: msvc, g++-15 and clang-20+ +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 20) set(CMAKE_CXX_SCAN_FOR_MODULES 1) -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + set(CMAKE_CXX_SCAN_FOR_MODULES 1) +elseif(MSVC) set(CMAKE_CXX_SCAN_FOR_MODULES 1) else() set(CMAKE_CXX_SCAN_FOR_MODULES 0) endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) # [CMAKE.SKIP_TESTS] option( @@ -43,33 +47,54 @@ option( ) message( - "Compiler is: ${CMAKE_CXX_COMPILER_ID} version: ${CMAKE_CXX_COMPILER_VERSION}" + "Compiler is: ${CMAKE_CXX_COMPILER_ID} version: ${CMAKE_CXX_COMPILER_VERSION}" ) message( - "cmake is: ${CMAKE_VERSION} modules scan : ${CMAKE_CXX_SCAN_FOR_MODULES}" + "cmake is: ${CMAKE_VERSION} modules scan: ${CMAKE_CXX_SCAN_FOR_MODULES}" ) +# gersemi: on + if(CMAKE_CXX_SCAN_FOR_MODULES) add_library(beman.scope) + + include(GenerateExportHeader) + + generate_export_header( + beman.scope + BASE_NAME beman.scope + EXPORT_FILE_NAME beman/scope/modules_export.hpp + ) +else() + add_library(beman.scope INTERFACE) +endif() + +if(CMAKE_CXX_SCAN_FOR_MODULES) target_sources( beman.scope PUBLIC FILE_SET HEADERS - BASE_DIRS include - FILES include/beman/scope/scope.hpp + BASE_DIRS include ${CMAKE_CURRENT_BINARY_DIR} + FILES + include/beman/scope/scope.hpp + include/beman/scope/scope_impl.hpp + ${CMAKE_CURRENT_BINARY_DIR}/beman/scope/modules_export.hpp PUBLIC FILE_SET CXX_MODULES - BASE_DIRS include - FILES include/beman/scope/beman.scope.cppm + BASE_DIRS include + FILES include/beman/scope/beman.scope.cppm ) + target_compile_features(beman.scope PUBLIC cxx_std_20) else() - add_library(beman.scope INTERFACE) target_sources( beman.scope INTERFACE FILE_SET HEADERS - BASE_DIRS include - FILES include/beman/scope/scope.hpp + BASE_DIRS include ${CMAKE_CURRENT_BINARY_DIR} + FILES + include/beman/scope/scope.hpp + include/beman/scope/scope_impl.hpp + # NO! ${CMAKE_CURRENT_BINARY_DIR}/beman/scope/modules_export.hpp ) endif() @@ -77,27 +102,25 @@ add_library(beman::scope ALIAS beman.scope) set_target_properties( beman.scope - PROPERTIES - VERIFY_INTERFACE_HEADER_SETS ON - EXPORT_NAME scope + PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON EXPORT_NAME scope ) include(GNUInstallDirs) +set(package_install_dir ${CMAKE_INSTALL_LIBDIR}/cmake/beman.scope) + +# TBD: always? CK install( TARGETS beman.scope COMPONENT beman.scope EXPORT beman.scope-targets - - FILE_SET CXX_MODULES - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} CXX_MODULES_BMI - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman.scope/bmi-${CMAKE_CXX_COMPILER_ID}_$ + DESTINATION + ${package_install_dir}/bmi-${CMAKE_CXX_COMPILER_ID}_$ FILE_SET HEADERS ) -# gersemi: on - if(BEMAN_SCOPE_INSTALL_CONFIG_FILE_PACKAGE) include(CMakePackageConfigHelpers) @@ -110,13 +133,13 @@ if(BEMAN_SCOPE_INSTALL_CONFIG_FILE_PACKAGE) FILES cmake/beman.scope-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/beman.scope-config-version.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman.scope + DESTINATION ${package_install_dir} COMPONENT beman.scope ) install( EXPORT beman.scope-targets - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman.scope + DESTINATION ${package_install_dir} NAMESPACE beman:: CXX_MODULES_DIRECTORY cxx-modules @@ -124,8 +147,9 @@ if(BEMAN_SCOPE_INSTALL_CONFIG_FILE_PACKAGE) ) endif() +enable_testing() + if(BEMAN_SCOPE_BUILD_TESTS) - enable_testing() add_subdirectory(tests) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 483e1a3..194a873 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -7,14 +7,30 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_CXX_STANDARD": "20", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "./infra/cmake/use-fetch-content.cmake" + "BEMAN_USE_STD_MODULE": true, + "CMAKE_CXX_EXTENSIONS": true, + "CMAKE_CXX_SCAN_FOR_MODULES": true, + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": true, + "CMAKE_EXPORT_COMPILE_COMMANDS": true, + "CMAKE_SKIP_TEST_ALL_DEPENDENCY": false, + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "infra/cmake/use-fetch-content.cmake" } }, { "name": "_debug-base", "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": false, + "deprecated": false + }, "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "BEMAN_BUILDSYS_SANITIZER": "MaxSan" @@ -57,7 +73,12 @@ "_debug-base" ], "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + "BEMAN_USE_STD_MODULE": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-libc++-toolchain.cmake" + }, + "environment": { + "CXX": "clang++", + "CMAKE_CXX_FLAGS": "-stdlib=libc++" } }, { @@ -68,7 +89,12 @@ "_release-base" ], "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + "BEMAN_USE_STD_MODULE": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-libc++-toolchain.cmake" + }, + "environment": { + "CXX": "clang++", + "CMAKE_CXX_FLAGS": "-stdlib=libc++" } }, { @@ -79,6 +105,8 @@ "_debug-base" ], "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "CMAKE_CXX_SCAN_FOR_MODULES": false, "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" } }, @@ -90,6 +118,8 @@ "_release-base" ], "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "CMAKE_CXX_SCAN_FOR_MODULES": false, "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" } }, @@ -101,6 +131,7 @@ "_debug-base" ], "cacheVariables": { + "BUILD_SHARED_LIBS": false, "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" } }, @@ -112,6 +143,7 @@ "_release-base" ], "cacheVariables": { + "BUILD_SHARED_LIBS": false, "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" } } diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 23ffaf7..60bcdea 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,9 +1,27 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +cmake_minimum_required(VERSION 3.28...4.2) + +project(beman.scope.example LANGUAGES CXX) + +if(PROJECT_IS_TOP_LEVEL) + if(NOT DEFINED CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_EXTENSIONS YES) + set(CMAKE_CXX_STANDARD_REQUIRED YES) + endif() + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + + find_package(beman.scope REQUIRED) + + enable_testing() +endif() + set(ALL_EXAMPLES scope_example unique_resource unique_resource-file) # module tests will only compile with gcc15 or clang20 and above -if(CMAKE_CXX_SCAN_FOR_MODULES AND CMAKE_CXX_MODULE_STD) +# NOTE: needs C++23 or newer! CK +if(CMAKE_CXX_SCAN_FOR_MODULES) list(APPEND ALL_EXAMPLES scope-module) endif() @@ -13,4 +31,5 @@ foreach(example ${ALL_EXAMPLES}) add_executable(${example}) target_sources(${example} PRIVATE ${example}.cpp) target_link_libraries(${example} PRIVATE beman::scope) + add_test(NAME ${example} COMMAND ${example}) endforeach() diff --git a/examples/scope-module.cpp b/examples/scope-module.cpp index 22dd6d3..2bb67a8 100644 --- a/examples/scope-module.cpp +++ b/examples/scope-module.cpp @@ -15,34 +15,50 @@ // destroy noisy // scope exit: true success: true fail: false -// #ifdef HAS_MODULE_STD +#include + +// NOTE: this needs C++23! CK +#ifdef HAS_MODULE_STD import std; -// #else -// NOTE: this needs C++23! -// #include -// #endif +#endif +// for g++-15 the order is important -- import after #includes import beman.scope; -// clang-format off -struct noisy_resource { - noisy_resource() { std::print( "construct noisy\n" ); } - ~noisy_resource() { std::print( "destroy noisy\n" ); } +namespace { + +struct DummyResource { + bool& cleaned; + + DummyResource(bool& flag) : cleaned(flag) { cleaned = false; } + + [[nodiscard]] bool is_clean() const { return cleaned; } }; +} // namespace + int main() { - bool exit_ran, success_ran, fail_ran = false; + bool exit_ran{}; + bool success_ran{}; + bool fail_ran{}; + bool cleaned{true}; { - std::print("--> scope start\n"); - beman::scope::scope_exit _([&exit_ran] { exit_ran = true; }); - beman::scope::scope_success _([&success_ran] { success_ran = true; }); - beman::scope::scope_fail _([&fail_ran] { fail_ran = true; }); - auto resource_ptr = beman::scope::unique_resource(new noisy_resource(), - // Cleanup function - [](noisy_resource* ptr) { delete ptr; }); - std::print("--> scope end\n"); + // clang-format off + beman::scope::scope_exit _se([&exit_ran] { exit_ran = true; }); + beman::scope::scope_success _ss([&success_ran] { success_ran = true; }); + beman::scope::scope_fail _sf([&fail_ran] { fail_ran = true; }); + auto resource_ptr = beman::scope::unique_resource(new DummyResource(cleaned), + [](DummyResource* ptr) { ptr->cleaned = true; delete ptr; }); + // clang-format on + + assert(cleaned == false); + assert(resource_ptr->is_clean() == false); } // Normal scope exit - std::print("scope exit: {} success: {} fail: {} \n", exit_ran, success_ran, fail_ran); + assert(exit_ran == true); + assert(success_ran == true); + assert(fail_ran == false); + assert(cleaned == true); } +// clang-format on diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 0000000..52add0f --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,21 @@ +root = . +search-path = build + +filter = examples/* +# filter = src/* +filter = include/* + +exclude-directories = stagedir +exclude-directories = build/*/*/_deps +exclude-directories = tests +exclude-directories = conan + +gcov-ignore-parse-errors = all +print-summary = yes + +html-details = build/coverage/index.html + +cobertura-pretty = yes +cobertura = build/cobertura.xml + +#TBD delete-gcov-files = yes diff --git a/include/beman/scope/beman.scope.cppm b/include/beman/scope/beman.scope.cppm index 59ce381..39f6a91 100644 --- a/include/beman/scope/beman.scope.cppm +++ b/include/beman/scope/beman.scope.cppm @@ -3,7 +3,7 @@ // g++-15 -std=c++26 -O2 -fmodules -fmodule-only -c ${scopetop}/include/beman/scope/beman.scope.cppm module; -#include "scope.hpp" +#include export module beman.scope; diff --git a/include/beman/scope/scope.hpp b/include/beman/scope/scope.hpp index 7d33b2c..0222c5c 100644 --- a/include/beman/scope/scope.hpp +++ b/include/beman/scope/scope.hpp @@ -5,17 +5,38 @@ #include #include +#include #include #include #include // clang-format off -#if __cplusplus < 202002L +#include + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907L + // C++20 concepts supported +#elif __cplusplus < 202002L #error "C++20 or later is required" #endif -// clang-format on -#include //todo unconditional for unique_resource +// detect standard header first, then experimental, otherwise use local implementation +#if defined(__has_include) +# if __has_include() +# include +# define BEMAN_SCOPE_USE_STD +# elif __has_include() +# include +# define BEMAN_SCOPE_USE_STD_EXPERIMENTAL +# else +// no std scope header — fall through to local implementation below +# endif +#elif defined(__cpp_lib_scope) && __cpp_lib_scope >= 2023xxxxL +# include +# define BEMAN_SCOPE_USE_STD +#else +# warning "Missing feature __cpp_lib_scope" +#endif +// clang-format on #ifdef BEMAN_SCOPE_USE_STD_EXPERIMENTAL @@ -31,19 +52,19 @@ template using scope_success = std::experimental::scope_success; // todo temporary -// template -// using unique_resource = std::experimental::unique_resource; +template +using unique_resource = std::experimental::fundamentals_v3::unique_resource; -// template > -// unique_resource, std::decay_t> -// make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( -// std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { -// return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); -//} +template > +unique_resource, std::decay_t> +make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( + std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { + return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); +} } // namespace beman::scope -#else // ! BEMAN_SCOPE__USE_STD_EXPERIMENTAL +#elif defined(BEMAN_SCOPE_USE_STD) namespace beman::scope { @@ -52,8 +73,8 @@ template using unique_resource = std::experimental::unique_resource; // todo temporary -template > -unique_resource, std::decay_t > +template > +unique_resource, std::decay_t> make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); @@ -421,6 +442,9 @@ using scope_fail = scope_guard + +#endif // BEMAN_SCOPE_USE_STD_EXPERIMENTAL #endif // BEMAN_SCOPE_HPP diff --git a/include/beman/scope/scope_impl.hpp b/include/beman/scope/scope_impl.hpp new file mode 100644 index 0000000..161d397 --- /dev/null +++ b/include/beman/scope/scope_impl.hpp @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef SCOPE_IMPL_HPP +#define SCOPE_IMPL_HPP + +// clang-format off +#include + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907L + // C++20 concepts supported +#elif __cplusplus < 202002L +# error "C++20 or later is required" +#endif + +// detect standard header first, then experimental, otherwise use local implementation +#ifdef __has_include +# if __has_include() +# include +# define BEMAN_SCOPE_USE_STD +// XXX #warning "Set BEMAN_SCOPE_USE_STD" +# elif __has_include() +# include +# define BEMAN_SCOPE_USE_STD_EXPERIMENTAL +// XXX #warning "Set BEMAN_SCOPE_USE_STD_EXPERIMENTAL" +# else +# define BEMAN_SCOPE_USE_FALLBACK +# endif +#else +# define BEMAN_SCOPE_USE_FALLBACK +#endif + +#ifdef BEMAN_SCOPE_USE_STD +# if !defined(__cpp_lib_scope_exit) +# error "Standard present but __cpp_lib_scope_exit not defined" +# endif +#endif + +#ifdef BEMAN_SCOPE_USE_FALLBACK +# if __has_include("beman/scope/modules_export.hpp") +# include "beman/scope/modules_export.hpp" +# else +# define BEMAN_SCOPE_EXPORT +# endif +// clang-format on + +#include +#include +#include + +namespace beman::scope { + +// TODO(CK): make a std::experimental::scope_exit::scope_exit conform +// implementation +template +class [[nodiscard]] BEMAN_SCOPE_EXPORT scope_exit { + F f; + bool active = true; + + public: + constexpr explicit scope_exit(F func) noexcept(std::is_nothrow_move_constructible_v) : f(std::move(func)) {} + + // Move constructor + constexpr scope_exit(scope_exit&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active) { + other.active = false; + } + + // Deleted copy + auto operator=(const scope_exit&) -> scope_exit& = delete; + scope_exit(const scope_exit&) = delete; + + // Deleted move assignment + // Does scope_exit need to be move-assignable? LEWG: NO! + constexpr auto operator=(scope_exit&& other) noexcept(std::is_nothrow_move_assignable_v) + -> scope_exit& = delete; + + // Destructor: call only if scope is exiting normally + ~scope_exit() noexcept(noexcept(f())) { + if (active) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } + + // Helper to tests if active + constexpr auto is_active() -> bool { return active; } +}; + +// Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +template +auto make_scope_exit(F f) -> scope_exit { + return scope_exit(std::move(f)); +} + +// TODO(CK): make a std::experimental::scope_fail::scope_fail conform +// implementation +template +class [[nodiscard]] BEMAN_SCOPE_EXPORT scope_fail { + F f; + bool active = true; + int exception_count{}; + + public: + // Constructor: capture current uncaught exceptions + constexpr explicit scope_fail(F func) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(func)), exception_count(std::uncaught_exceptions()) {} + + // Move constructor + constexpr scope_fail(scope_fail&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active), exception_count(other.exception_count) { + other.active = false; + } + + // Deleted copy + scope_fail(const scope_fail&) = delete; + auto operator=(const scope_fail&) -> scope_fail& = delete; + + // Deleted move assignment + // Move assignment + constexpr auto operator=(scope_fail&& other) noexcept(std::is_nothrow_move_assignable_v) + -> scope_fail& = delete; +#if MOVE_ASSIGNMENT_NEEDED + G { + if (this != &other) { + f = std::move(other.f); + active = other.active; + exception_count = other.exception_count; + other.active = false; + } + return *this; + } +#endif + + // Destructor: call if scope is exiting due to an exception + ~scope_fail() noexcept(noexcept(f())) { + if (active && std::uncaught_exceptions() > exception_count) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } + + // Helper to tests if active + constexpr auto is_active() -> bool { return active; } +}; + +// Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +template +constexpr auto make_scope_fail(F&& f) -> scope_fail> { + return scope_fail>(std::forward(f)); +} + +// TODO(CK): make a std::experimental::scope_success::scope_success conform +// implementation +template +class [[nodiscard]] BEMAN_SCOPE_EXPORT scope_success { + F f; + bool active = true; + int exception_count{}; + + public: + // Constructor: capture current uncaught exceptions + constexpr explicit scope_success(F func) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(func)), exception_count(std::uncaught_exceptions()) {} + + // Move constructor + constexpr scope_success(scope_success&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active), exception_count(other.exception_count) { + other.active = false; + } + + // Deleted copy + scope_success(const scope_success&) = delete; + auto operator=(const scope_success&) -> scope_success& = delete; + + // Deleted move assignment + // Move assignment + constexpr auto operator=(scope_success&& other) noexcept(std::is_nothrow_move_assignable_v) + -> scope_success& = delete; +#if MOVE_ASSIGNMENT_NEEDED + { + if (this != &other) { + f = std::move(other.f); + active = other.active; + exception_count = other.exception_count; + other.active = false; + } + return *this; + } +#endif + + // Destructor: call only if scope is exiting normally + ~scope_success() noexcept(noexcept(f())) { + if (active && std::uncaught_exceptions() == exception_count) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } + + // Helper to tests if active + constexpr auto is_active() -> bool { return active; } +}; + +// Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +template +constexpr auto make_scope_success(F&& f) -> scope_success> { + return scope_success>(std::forward(f)); +} + +template +class [[nodiscard]] BEMAN_SCOPE_EXPORT unique_resource { + Resource resource; + Deleter deleter; + bool active = true; + + public: + // Constructor + constexpr unique_resource(Resource r, Deleter d) noexcept(std::is_nothrow_move_constructible_v) + : resource(std::move(r)), deleter(std::move(d)) {} + + // Move constructor + constexpr unique_resource(unique_resource&& other) noexcept(std::is_nothrow_move_constructible_v) + : resource(std::move(other.resource)), deleter(std::move(other.deleter)) { + active = std::exchange(other.active, false); + } + + // Move assignment + constexpr auto operator=(unique_resource&& other) noexcept(std::is_nothrow_move_assignable_v) + -> unique_resource& { + if (this != &other) { + reset(std::move(other.resource)); + deleter = std::move(other.deleter); + active = std::exchange(other.active, false); + } + return *this; + } + + // Deleted copy operations + unique_resource(const unique_resource&) = delete; + auto operator=(const unique_resource&) -> unique_resource& = delete; + + // Destructor + ~unique_resource() noexcept(noexcept(deleter(resource))) { reset(); } + + // Release ownership + constexpr void release() noexcept { active = false; } + + // Reset resource + constexpr void reset() noexcept(noexcept(deleter(resource))) { + if (active) { + active = false; + deleter(resource); + } + } + + // Reset the resource and call deleter if engaged + constexpr void reset(Resource new_resource) noexcept(noexcept(deleter(resource))) { + if (active) { + deleter(resource); + } + resource = std::move(new_resource); + active = true; + } + + // Accessors + constexpr auto get() const -> const Resource& { return resource; } + constexpr auto get() -> Resource& { return resource; } + + // operator* — only for non-void pointer resources + constexpr auto operator*() const noexcept -> std::add_lvalue_reference_t> + requires(std::is_pointer_v && !std::is_void_v>) + { + return *resource; + } + + // Optional pointer convenience + constexpr auto operator->() const noexcept -> Resource + requires std::is_pointer_v + { + return resource; + } + + // TODO(CK): missing usecase? + constexpr auto get_deleter() const noexcept -> Deleter; + + // Helper to tests is_active() + // NOTE: check if active; not required from LWG? + constexpr explicit operator bool() const noexcept { return active; } +}; + +// Deduction guide +template +unique_resource(Resource&&, Deleter&&) -> unique_resource, std::decay_t>; + +// Factory: conditionally engaged +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +template +constexpr auto make_unique_resource_checked(R&& r, const Invalid& invalid, D&& d) { + using resource_type = std::decay_t; + using deleter_type = std::decay_t; + + unique_resource ur(resource_type{}, std::forward(d)); + if (r == invalid) { + ur.release(); // disengage immediately + } + return ur; +} + +} // namespace beman::scope + +#elifdef BEMAN_SCOPE_USE_STD_EXPERIMENTAL + +namespace beman::scope { +using ::std::experimental::scope_exit; +using ::std::experimental::scope_fail; +using ::std::experimental::scope_success; +using ::std::experimental::unique_resource; +} // namespace beman::scope + // +#endif // BEMAN_SCOPE_USE_FALLBACK + +#endif // SCOPE_IMPL_HPP diff --git a/infra/.beman_submodule b/infra/.beman_submodule index bfed167..10ea6a3 100644 --- a/infra/.beman_submodule +++ b/infra/.beman_submodule @@ -1,3 +1,3 @@ [beman_submodule] remote=https://github.com/bemanproject/infra.git -commit_hash=bb58b2a1cc894d58a55bf745be78f5d27029e245 +commit_hash=b3545a45640abd1fedc01441ca3f220d9ac5a8e3 diff --git a/infra/.pre-commit-config.yaml b/infra/.pre-commit-config.yaml index e806e59..bc4dd84 100644 --- a/infra/.pre-commit-config.yaml +++ b/infra/.pre-commit-config.yaml @@ -19,14 +19,3 @@ repos: - id: gersemi name: CMake linting exclude: ^.*/tests/.*/data/ # Exclude test data directories - - # Python linting and formatting - # config file: ruff.toml (not currently present but add if needed) - # https://docs.astral.sh/ruff/configuration/ - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 - hooks: - - id: ruff-check - files: ^tools/beman-tidy/ - - id: ruff-format - files: ^tools/beman-tidy/ diff --git a/infra/.pre-commit-hooks.yaml b/infra/.pre-commit-hooks.yaml deleted file mode 100644 index d327587..0000000 --- a/infra/.pre-commit-hooks.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- id: beman-tidy - name: "beman-tidy: bemanification your repo" - entry: ./tools/beman-tidy/beman-tidy - language: script - pass_filenames: false - always_run: true - args: [".", "--verbose"] diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake index e7fd0ad..c40959d 100644 --- a/infra/cmake/beman-install-library-config.cmake +++ b/infra/cmake/beman-install-library-config.cmake @@ -84,8 +84,8 @@ function(beman_install_library name) option( ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE - "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." - ${PROJECT_IS_TOP_LEVEL} + "Enable creating and installing a CMake config-file package. Default: ON. Values: { ON, OFF }." + ON ) # By default, install the config package @@ -121,7 +121,7 @@ function(beman_install_library name) find_file( config_file_template NAMES "${package_name}-config.cmake.in" - PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + PATHS "${PROJECT_SOURCE_DIR}/cmake" NO_DEFAULT_PATH NO_CACHE REQUIRED diff --git a/makefile b/makefile new file mode 100644 index 0000000..6c4875a --- /dev/null +++ b/makefile @@ -0,0 +1,72 @@ +# Standard stuff + +.SUFFIXES: + +MAKEFLAGS+= --no-builtin-rules # Disable the built-in implicit rules. +# MAKEFLAGS+= --warn-undefined-variables # Warn when an undefined variable is referenced. +# MAKEFLAGS+= --include-dir=$(CURDIR)/conan # Search DIRECTORY for included makefiles (*.mk). + +export hostSystemName=$(shell uname) + +ifeq (${hostSystemName},Darwin) + export LLVM_PREFIX:=$(shell brew --prefix llvm) + export LLVM_DIR:=$(shell realpath ${LLVM_PREFIX}) + export PATH:=${LLVM_DIR}/bin:${PATH} + + export CMAKE_CXX_STDLIB_MODULES_JSON=${LLVM_DIR}/lib/c++/libc++.modules.json + export CXX=clang++ + export LDFLAGS=-L$(LLVM_DIR)/lib/c++ -lc++abi # XXX -lc++ -lc++experimental + # FIXME: export GCOV="llvm-cov gcov" + + ### TODO: to test g++-15: + export GCC_PREFIX:=$(shell brew --prefix gcc) + export GCC_DIR:=$(shell realpath ${GCC_PREFIX}) + + # export CMAKE_CXX_STDLIB_MODULES_JSON=${GCC_DIR}/lib/gcc/current/libstdc++.modules.json + # export CXX:=g++-15 + # export CXXFLAGS:=-stdlib=libstdc++ + # export GCOV="gcov" +else ifeq (${hostSystemName},Linux) + export LLVM_DIR=/usr/lib/llvm-20 + export PATH:=${LLVM_DIR}/bin:${PATH} + export CXX=clang++-20 +endif + +.PHONY: all install coverage gclean distclean format + +all: build/compile_commands.json + ln -sf $< . + ninja -C build + +build/compile_commands.json: CMakeLists.txt makefile + cmake -S . -B build -G Ninja --log-level=DEBUG -D CMAKE_BUILD_TYPE=Release \ + -D CMAKE_EXPERIMENTAL_CXX_IMPORT_STD="d0edc3af-4c50-42ea-a356-e2862fe7a444" \ + -D CMAKE_CXX_STDLIB_MODULES_JSON=${CMAKE_CXX_STDLIB_MODULES_JSON} \ + -D CMAKE_CXX_STANDARD=23 -D CMAKE_CXX_EXTENSIONS=YES -D CMAKE_CXX_STANDARD_REQUIRED=YES \ + -D CMAKE_CXX_FLAGS='-fno-inline --coverage' \ + -D CMAKE_CXX_MODULE_STD=NO \ + -D CMAKE_INSTALL_MESSAGE=LAZY # XXX -D CMAKE_SKIP_INSTALL_RULES=YES # --fresh + +install: build/cmake_install.cmake + cmake --install build + +distclean: # XXX clean + rm -rf build compile_commands.json + find . -name '*~' -delete + +gclean: clean + find build -name '*.gc..' -delete + +build/coverage: test + mkdir -p $@ + +coverage: build/coverage + gcovr --merge-mode-functions separate + +format: distclean + pre-commit autoupdate + pre-commit run --all + +# Anything we don't know how to build will use this rule. +% :: + ninja -C build $(@) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7cbcfc..8e89080 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,16 +2,28 @@ include(FetchContent) +if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(_FIND_PACKAGE_ARGS) +else() + set(_FIND_PACKAGE_ARGS FIND_PACKAGE_ARGS 3.11) +endif() + FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.11.0 EXCLUDE_FROM_ALL - # FIND_PACKAGE_ARGS 3.11 + ${_FIND_PACKAGE_ARGS} ) FetchContent_MakeAvailable(Catch2) -set(ALL_TESTNAMES scope_success scope_exit scope_fail unique_resource) +set(ALL_TESTNAMES + scope_success + scope_exit + scope_fail + unique_resource + unique_resource_2 +) # module tests will only compile with gcc15 or clang20 and above if(CMAKE_CXX_SCAN_FOR_MODULES) @@ -31,3 +43,29 @@ foreach(testname ${ALL_TESTNAMES}) ) catch_discover_tests(test.${testname}) endforeach() + +if(BEMAN_SCOPE_INSTALL_CONFIG_FILE_PACKAGE) + # test if the targets are usable from the install directory + add_test( + NAME install-to-stagedir + COMMAND + ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix + ${CMAKE_BINARY_DIR}/stagedir --config $ + ) + add_test( + NAME find-package-test + COMMAND + ${CMAKE_CTEST_COMMAND} # --verbose + --output-on-failure -C $ --build-and-test + "${CMAKE_SOURCE_DIR}/examples" + "${CMAKE_CURRENT_BINARY_DIR}/find-package-test" --build-generator + ${CMAKE_GENERATOR} --build-makeprogram ${CMAKE_MAKE_PROGRAM} + --build-options "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}" + "-DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}" + "-DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}" + "-DCMAKE_CXX_MODULE_STD=${CMAKE_CXX_MODULE_STD}" + "-DCMAKE_CXX_SCAN_FOR_MODULES=${CMAKE_CXX_SCAN_FOR_MODULES}" + "-DCMAKE_BUILD_TYPE=$" + "-DCMAKE_PREFIX_PATH=${CMAKE_BINARY_DIR}/stagedir" + ) +endif() diff --git a/tests/module.test.cpp b/tests/module.test.cpp index a4b22fc..d5f7ec6 100644 --- a/tests/module.test.cpp +++ b/tests/module.test.cpp @@ -3,35 +3,41 @@ #define CATCH_CONFIG_MAIN #include -// for g++-15 the order is important -- import after includes +// for g++-15 the order is important -- import after #includes import beman.scope; +namespace { + struct DummyResource { bool& cleaned; DummyResource(bool& flag) : cleaned(flag) { cleaned = false; } - bool is_clean() const { return cleaned; } + [[nodiscard]] bool is_clean() const { return cleaned; } }; +} // namespace + TEST_CASE("module-test", "[scope_module_test]") { - bool exit_ran, success_ran, fail_ran = false; - bool cleaned = true; + bool exit_ran{}; + bool success_ran{}; + bool fail_ran{}; + bool cleaned{true}; { // clang-format off beman::scope::scope_exit _se([&exit_ran] { exit_ran = true; }); beman::scope::scope_success _ss([&success_ran] { success_ran = true; }); beman::scope::scope_fail _sf([&fail_ran] { fail_ran = true; }); - auto resource_ptr = beman::scope::unique_resource(new DummyResource(cleaned), - [](DummyResource* ptr) { ptr->cleaned =true; delete ptr; }); - REQUIRE(cleaned == false); - REQUIRE(resource_ptr->is_clean() == false); + auto resource_ptr = beman::scope::unique_resource(new DummyResource(cleaned), + [](DummyResource* ptr) { ptr->cleaned = true; delete ptr; }); // clang-format on + + REQUIRE(cleaned == false); + REQUIRE(resource_ptr->is_clean() == false); } // Normal scope exit REQUIRE(exit_ran == true); REQUIRE(success_ran == true); REQUIRE(fail_ran == false); REQUIRE(cleaned == true); - } diff --git a/tests/scope_exit.test.cpp b/tests/scope_exit.test.cpp index 65acf7a..51a4477 100644 --- a/tests/scope_exit.test.cpp +++ b/tests/scope_exit.test.cpp @@ -125,6 +125,25 @@ TEST_CASE("scope_exit handles nested guards in correct order", "[scope_exit][adv REQUIRE(trace == "second first "); } +// fails +// #include +// +// TEST_CASE("scope_exit move assignment transfers ownership", "[scope_exit][advanced]") { +// bool cleanup_ran = false; +// +// { +// scope_exit> guard1([&]{ cleanup_ran = true; }); +// scope_exit> guard2([&]{}); +// +// guard2 = std::move(guard1); +// +// REQUIRE_FALSE(guard1.is_active()); +// REQUIRE(guard2.is_active()); +// } +// +// REQUIRE(cleanup_ran == false); +// } + // fails // TEST_CASE("scope_exit cleanup handles custom object with side effects", "[scope_exit][advanced]") { // struct Tracer { diff --git a/tests/unique_resource.test.cpp b/tests/unique_resource.test.cpp index 48f611d..9edd61e 100644 --- a/tests/unique_resource.test.cpp +++ b/tests/unique_resource.test.cpp @@ -68,7 +68,7 @@ TEST_CASE("unique_resource does not clean up after release", "[unique_resource]" [](DummyResource r) { *(r.cleanedUp) = true; } ); - res.release(); //no cleanup run + res.release(); // no cleanup run } REQUIRE(cleaned == false); diff --git a/tests/unique_resource_2.test.cpp b/tests/unique_resource_2.test.cpp new file mode 100644 index 0000000..b613d5c --- /dev/null +++ b/tests/unique_resource_2.test.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include "beman/scope/scope.hpp" + +#include +#include +#include + +namespace { + +struct Counter { + int value = 0; +}; + +struct CountingDeleter { + // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes) + Counter* counter{nullptr}; + + void operator()(int& /*unused*/) const noexcept { ++counter->value; } +}; + +} // namespace + +TEST_CASE("Construct file unique_resource", "[unique_resource]") { + bool open_file_good = false; + bool close_file_good = false; + + { + auto file = beman::scope::unique_resource(fopen("example.txt", "w"), // Acquire the FILE* + [&close_file_good](FILE* f) -> void { + if (f) { + (void)fclose(f); // Release (cleanup) the resource + close_file_good = true; + } + }); + + if (file.get() == nullptr) { + throw std::runtime_error("file didn't open"); + } + open_file_good = true; + } + + REQUIRE(open_file_good == true); + REQUIRE(close_file_good == true); +} + +TEST_CASE("unique_resource basic construction and engagement", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + { + beman::scope::unique_resource r(42, CountingDeleter{&c}); + + // XXX REQUIRE(static_cast(r)); + REQUIRE(r.get() == 42); + REQUIRE(c.value == 0); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource release disengages without deleting", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + { + beman::scope::unique_resource r(7, CountingDeleter{&c}); + + r.release(); + + // XXX REQUIRE_FALSE(r); + } + + REQUIRE(c.value == 0); +} + +TEST_CASE("unique_resource reset() destroys current resource", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(1, CountingDeleter{&c}); + + r.reset(); + // XXX REQUIRE_FALSE(r); + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource reset(new_resource) replaces resource", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(1, CountingDeleter{&c}); + + r.reset(2); + + // XXX REQUIRE(r); + REQUIRE(r.get() == 2); + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 2); +} + +TEST_CASE("unique_resource move constructor transfers ownership", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r1(10, CountingDeleter{&c}); + beman::scope::unique_resource r2(std::move(r1)); + + // XXX REQUIRE_FALSE(r1); + // XXX REQUIRE(r2); + REQUIRE(r2.get() == 10); + + r2.reset(); + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource move assignment destroys target before transfer", "[unique_resource]") { + Counter c1{}; // NOLINT(misc-const-correctness) + Counter c2{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r1(1, CountingDeleter{&c1}); + beman::scope::unique_resource r2(2, CountingDeleter{&c2}); + + r2 = std::move(r1); + + // XXX REQUIRE_FALSE(r1); + // XXX REQUIRE(r2); + REQUIRE(r2.get() == 1); + + REQUIRE(c2.value == 1); // old r2 destroyed + REQUIRE(c1.value == 0); + + r2.reset(); + REQUIRE(c1.value == 1); +} + +TEST_CASE("unique_resource destructor is idempotent after release", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(99, CountingDeleter{&c}); + + r.release(); + } + + REQUIRE(c.value == 0); +} +#ifdef BEMAN_SCOPE_USE_FALLBACK +TEST_CASE("make_unique_resource_checked disengages on invalid", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + auto r = beman::scope::make_unique_resource_checked(-1, -1, CountingDeleter{&c}); + + // XXX REQUIRE_FALSE(r); + } + + REQUIRE(c.value == 0); +} + +TEST_CASE("make_unique_resource_checked engages on valid", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + auto r = beman::scope::make_unique_resource_checked(3, -1, CountingDeleter{&c}); + + // XXX REQUIRE(r); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("Open a nonexisting file with make_unique_resource_checked", "[unique_resource]") { + bool open_file_good = false; + bool close_file_good = false; + + { + auto file = + beman::scope::make_unique_resource_checked(fopen("nonexisting.txt", "r"), // Acquire the FILE* + nullptr, + [&close_file_good](FILE* f) -> void { + if (f) { + (void)fclose(f); // Release (cleanup) the resource + close_file_good = true; + } + }); + + if (file.get() != nullptr) { + open_file_good = true; + } + } + + REQUIRE(open_file_good == false); + REQUIRE(close_file_good == false); +} +#endif + +TEST_CASE("unique_resource supports deduction guide", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r(123, CountingDeleter{&c}); + + static_assert(std::is_same_v>); + + r.reset(); + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource does not double-delete after move", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r1(1, CountingDeleter{&c}); + + { + auto r2 = std::move(r1); + } + + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource operator* returns reference to resource", "[unique_resource]") { + int value = 42; + + // Define the deleter type explicitly (function pointer) + using DeleterType = void (*)(int*); + + // Empty deleter + auto empty_deleter = [](int*) {}; + + // Create unique_resource instance (modifiable) + beman::scope::unique_resource r(&value, empty_deleter); + + // operator* should return a reference + int& ref = *r; + + // Check that the reference refers to the original value + REQUIRE(&ref == &value); + REQUIRE(ref == 42); + + // Modify the value through the reference + ref = 100; + REQUIRE(value == 100); + + // Create a const unique_resource instance + const beman::scope::unique_resource r2(&value, empty_deleter); + + // operator* should return const reference + const int& cref = *r2; + REQUIRE(cref == 100); + + // Modifying through cref would fail to compile (correct) +} + +struct Foo { + int value = 0; +}; + +TEST_CASE("unique_resource operator-> works", "[unique_resource]") { + bool deleted = false; + Foo* raw = new Foo{42}; + + // Use std::function for the deleter + beman::scope::unique_resource> r(raw, [&](Foo* p) { + deleted = true; + delete p; + }); + + REQUIRE(r->value == 42); + r->value = 100; + REQUIRE(r->value == 100); + + REQUIRE_FALSE(deleted); // deleter not called yet +}