diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index a3f4690..8e71e91 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -1,30 +1,25 @@ name: Sidelobe Build Matrix Linux on: - push: - branches: - - main - - develop - pull_request: - types: [opened, synchronize, reopened] + workflow_call: jobs: build: name: Linux, ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: cxx: - - g++-7 - - g++-11 + - g++-10 + - g++-12 build_type: [Debug, Release] std: [14] include: - - cxx: g++-7 - cc: gcc-7 - other_pkgs: g++-7 gcc-7 - - cxx: g++-11 - cc: gcc-11 - other_pkgs: g++-11 gcc-11 + - cxx: g++-10 + cc: gcc-10 + other_pkgs: g++-10 gcc-10 + - cxx: g++-12 + cc: gcc-12 + other_pkgs: g++-12 gcc-12 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 0aa21c2..9159a57 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -1,11 +1,6 @@ name: Sidelobe Build Matrix macos on: - push: - branches: - - main - - develop - pull_request: - types: [opened, synchronize, reopened] + workflow_call: jobs: build: name: ${{matrix.os}}, ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} @@ -13,10 +8,10 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-11] + os: [macos-latest] cxx: - clang++ - build_type: [Debug] + build_type: [Debug, Release] std: [14] steps: @@ -39,5 +34,5 @@ jobs: CTEST_OUTPUT_ON_FAILURE: 1 working-directory: deploy/build run: | - cmake --build . --parallel 2 + cmake --build . --parallel 2 --target MemorySentinelTest ctest -C ${{ matrix.build_type }} -L MemorySentinelTest -j 2 diff --git a/.github/workflows/build-sonarqube.yml b/.github/workflows/build-sonarqube.yml index b9042cc..16d8b75 100644 --- a/.github/workflows/build-sonarqube.yml +++ b/.github/workflows/build-sonarqube.yml @@ -1,33 +1,29 @@ name: Sidelobe Build Sonarqube on: - push: - branches: - - main - - develop - pull_request: - types: [opened, synchronize, reopened] + workflow_call: jobs: build: name: Linux, GCC, Release, Coverage & SonarQube - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 env: BUILD_WRAPPER_OUT_DIR: '$GITHUB_WORKSPACE/deploy/build/bw-output' # Directory where build-wrapper output will be placed cxx: g++-11 cc: gcc-11 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'true' + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Install Toolchain run: | pip3 install gcovr echo "Gcovr Updated!" gcovr --version - sudo apt-get install -y g++-11 gcc-11 - - name: Install sonar-scanner and build-wrapper - uses: SonarSource/sonarcloud-github-c-cpp@v2 + # sudo apt-get install -y g++-11 gcc-11 + - name: Install Build Wrapper + uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v4 - name: Setup & Cmake working-directory: deploy env: @@ -50,9 +46,11 @@ jobs: # Generate coverage report gcovr -r .. -f ../source --exclude-unreachable-branches --exclude-throw-branches --sonarqube -o report/coverage.xml - - name: Run sonar-scanner + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + with: + args: > + --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index ab5e364..5b1d79f 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -1,11 +1,6 @@ name: Sidelobe Build Matrix Windows on: - push: - branches: - - main - - develop - pull_request: - types: [opened, synchronize, reopened] + workflow_call: jobs: build: name: ${{matrix.os}}, MSVC, C++${{matrix.std}}, ${{matrix.build_type}}, ${{matrix.platform}} diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..43fa5f4 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,25 @@ +name: Build & Test +on: + push: + branches: + - main + - develop + pull_request: + types: [opened, synchronize, reopened] + schedule: + - cron: '0 3 1 * *' # At 3:00am on the 1st + workflow_dispatch: + +jobs: + call-build-linux: + uses: ./.github/workflows/build-linux.yml + call-build-macos: + if: ${{ always() }} + uses: ./.github/workflows/build-macos.yml + call-build-windows: + if: ${{ always() }} + uses: ./.github/workflows/build-windows.yml + call-build-sonarqube: + if: ${{ always() }} + uses: ./.github/workflows/build-sonarqube.yml + secrets: inherit diff --git a/CMakeLists.txt b/CMakeLists.txt index a647afc..f451e63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9) +cmake_minimum_required(VERSION 3.20) # ╔╦╗┌─┐┌┬┐┌─┐┬─┐┬ ┬ ╔═╗┌─┐┌┐┌┌┬┐┬┌┐┌┌─┐┬ # ║║║├┤ ││││ │├┬┘└┬┘ ╚═╗├┤ │││ │ ││││├┤ │ @@ -48,6 +48,8 @@ if (CODE_COVERAGE) endif() target_link_libraries(${TEST_NAME} ${LIB_NAME}) + +# Link to DL Libs if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU" ) target_link_libraries(${TEST_NAME} dl) endif() @@ -58,7 +60,8 @@ if (POLICY CMP0110) endif() ## ENABLE THE USE OF CTEST -include("test/external-utils/catch2/ParseAndAddCatchTests.cmake") +include("test/external-utils/catch2/Catch.cmake") + #include(CTest) # this will generate lots of additional targets enable_testing() -ParseAndAddCatchTests(${TEST_NAME}) +catch_discover_tests(${TEST_NAME}) diff --git a/README.md b/README.md index f42c5f7..3886e4b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ╩ ╩└─┘┴ ┴└─┘┴└─ ┴ ╚═╝└─┘┘└┘ ┴ ┴┘└┘└─┘┴─┘ ``` -### A utility to detect memory allocation and de-allocation in a given scope. Meant to be used for testing environments. +### A utility to detect memory allocation and de-allocation in a given scope. Meant to be used for testing environments, not production deployments. ![](https://img.shields.io/github/license/Sidelobe/Hyperbuffer) @@ -13,7 +13,7 @@ The `MemorySentinel` hijacks the all the system's variants of `new` & `delete` as well `malloc` & `free`. When unarmed, it quietly monitors the memory allocation landscape without intervening (quiet infiltration). When armed, and as soon as a "transgression" is detected, the `MemorySentinel` will become active, and either: -* Throw a ` std::bad_alloc` +* Throw a ` std::bad_alloc` (only for allocation funtions) * Log to console (while still allocating normally) * Register the event silently (status can be queried) @@ -60,9 +60,7 @@ sentinel.clearTransgressions(); ### Status ![](https://img.shields.io/badge/branch-main-blue) -[![Sidelobe Build Matrix Linux](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-linux.yml/badge.svg?branch=main)](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-linux.yml) -[![Sidelobe Build Matrix macos](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-macos.yml/badge.svg?branch=main)](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-macos.yml) -[![Sidelobe Build Matrix Windows](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-windows.yml/badge.svg?branch=main)](https://github.com/Sidelobe/MemorySentinel/actions/workflows/build-windows.yml) +[![Build & Test](https://github.com/Sidelobe/MemorySentinel/actions/workflows/workflow.yml/badge.svg)](https://github.com/Sidelobe/MemorySentinel/actions/workflows/workflow.yml) MemorySentinel [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Sidelobe_MemorySentinel&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Sidelobe_MemorySentinel) diff --git a/deploy/generate-vs2019-windows.bat b/deploy/generate-vs2019-windows.bat deleted file mode 100644 index 3c9af90..0000000 --- a/deploy/generate-vs2019-windows.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off - -if not exist "build\vs2017-windows" mkdir build\vs2017-windows -cd build\vs2017-windows - -cmake -G "Visual Studio 16 2019" -DCMAKE_GENERATOR_PLATFORM=x64 ..\..\.. -if ERRORLEVEL 1 exit /b %ERRORLEVEL% - -cd .. \ No newline at end of file diff --git a/deploy/generate-vs2022-x64-windows.bat b/deploy/generate-vs2022-x64-windows.bat new file mode 100644 index 0000000..51f5508 --- /dev/null +++ b/deploy/generate-vs2022-x64-windows.bat @@ -0,0 +1,9 @@ +@echo off + +if not exist "build\vs2017-windows" mkdir build\vs2022-x64-windows +cd build\vs2022-x64-windows + +cmake -G "Visual Studio 17 2022" -DCMAKE_GENERATOR_PLATFORM=x64 ..\..\.. +if ERRORLEVEL 1 exit /b %ERRORLEVEL% + +cd .. diff --git a/sonar-project.properties b/sonar-project.properties index f16ed06..a83deac 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,8 +11,8 @@ sonar.c.file.suffixes=.c,.h sonar.cpp.file.suffixes=.cpp,.hpp # Config of C++ plugin -sonar.cfamily.threads=2 -sonar.cfamily.build-wrapper-output=deploy/build/bw-output +#sonar.cfamily.threads=2 +#sonar.cfamily.build-wrapper-output=deploy/build/bw-output # Coverage Report sonar.coverageReportPaths=deploy/report/coverage.xml diff --git a/source/MemorySentinel.cpp b/source/MemorySentinel.cpp index fc1127b..0294516 100644 --- a/source/MemorySentinel.cpp +++ b/source/MemorySentinel.cpp @@ -3,7 +3,7 @@ // ║║║├┤ ││││ │├┬┘└┬┘ ╚═╗├┤ │││ │ ││││├┤ │ // ╩ ╩└─┘┴ ┴└─┘┴└─ ┴ ╚═╝└─┘┘└┘ ┴ ┴┘└┘└─┘┴─┘ // -// © 2023 Lorenz Bucher - all rights reserved +// © 2025 Lorenz Bucher - all rights reserved // https://github.com/Sidelobe/MemorySentinel #include "MemorySentinel.hpp" @@ -20,14 +20,16 @@ #endif #endif -static bool handleTransgressionException() noexcept(false) +#if defined(__clang__) || defined(__GNUC__) +__attribute__((noreturn)) +#endif +static void handleTransgressionException() noexcept(false) { #ifdef SLB_EXCEPTIONS_DISABLED assert(false && "[Exceptions disabled]"); #else throw std::bad_alloc(); #endif - return false; // never reached } template @@ -48,7 +50,8 @@ static bool handleTransgression(const char* optionalMsg, std::size_t size, Excep switch (MemorySentinel::getTransgressionBehaviour()) { case MemorySentinel::TransgressionBehaviour::THROW_EXCEPTION: { - return exceptionHandler(); + exceptionHandler(); + return false; } case MemorySentinel::TransgressionBehaviour::LOG: { if (size !=0) { @@ -62,11 +65,14 @@ static bool handleTransgression(const char* optionalMsg, std::size_t size, Excep return false; } } + + return false; } // Using pattern described here: https://stackoverflow.com/a/17850402/649700 static bool isHijackActive = false; +/** exception-throwing variant */ static decltype(auto) hijack(const char* msg, std::size_t size = 0) noexcept(false) { // Disabling 'hijack' while running 'trangression handler' @@ -75,12 +81,12 @@ static decltype(auto) hijack(const char* msg, std::size_t size = 0) noexcept(fal isHijackActive = true; return retValue; } - -static decltype(auto) hijack(const char* msg, std::size_t size, std::nothrow_t const&) noexcept +/** no-except variant */ +static decltype(auto) hijack(const char* msg, std::size_t size, std::nothrow_t const&) noexcept(true) { // Disabling 'hijack' while running 'trangression handler' isHijackActive = false; - auto retValue = handleTransgression(msg, size, [](){ return false; }); // simply return false in case an exception occurs + auto retValue = handleTransgression(msg, size, [](){ return false; }); // dummy transgression handler simply return false in case an exception occurs isHijackActive = true; return retValue; } @@ -160,7 +166,8 @@ void free(void* ptr) initMallocHijack(); } if (isHijackActive) { - hijack("deallocation with free"); + std::nothrow_t nt; // force non-throwing overload with tag + hijack("deallocation with free", 0, nt); } builtinFree(ptr); } @@ -201,50 +208,48 @@ void* operator new[](std::size_t size) noexcept(false) return std::malloc(size); } -// MARK: - new nothrow -void* operator new(std::size_t size, std::nothrow_t const& nt) noexcept +// MARK: - new noexcept +void* operator new(std::size_t size, std::nothrow_t const& nt) noexcept(true) { if (isHijackActive) { - if (hijack("allocation with new (nothrow)", size, nt)) { - return builtinMalloc(size); // allocate the memory with the 'un-hijacked' malloc. - } else { - return nullptr; // convention - } + hijack("allocation with new (nothrow)", size, nt); // will always return false + return nullptr; // convention } return std::malloc(size); } -// MARK: - new[] nothrow -void* operator new[](std::size_t size, std::nothrow_t const& nt) noexcept +// MARK: - new[] noexcept +void* operator new[](std::size_t size, std::nothrow_t const& nt) noexcept(true) { if (isHijackActive) { - if (hijack("allocation with new[] (nothrow)", size, nt)) { - return builtinMalloc(size); // allocate the memory with the 'un-hijacked' malloc. - } else { - return nullptr; // convention - } + hijack("allocation with new[] (nothrow)", size, nt); // will always return false + return nullptr; // convention } return std::malloc(size); } -// MARK: - delete -void operator delete(void* ptr) noexcept +// MARK: - delete -- always noexcept +void operator delete(void* ptr) noexcept(true) { if (isHijackActive) { - hijack("deallocation with delete"); - return builtinFree(ptr); // free the memory with the 'un-hijacked' free. + std::nothrow_t nt; // force non-throwing overload with tag + hijack("deallocation with delete", 0, nt); + builtinFree(ptr); // free the memory with the 'un-hijacked' free. + } else { + std::free(ptr); } - std::free(ptr); } -// MARK: - delete[] -void operator delete[](void* ptr) noexcept +// MARK: - delete[] -- always noexcept +void operator delete[](void* ptr) noexcept(true) { if (isHijackActive) { - hijack("deallocation with delete[]"); - return builtinFree(ptr); // free the memory with the 'un-hijacked' free. + std::nothrow_t nt; // force non-throwing overload with tag + hijack("deallocation with delete[]", 0, nt); + builtinFree(ptr); // free the memory with the 'un-hijacked' free. + } else { + std::free(ptr); } - std::free(ptr); } // -------------------------------------------------------------------------------------------------------------------- diff --git a/source/MemorySentinel.hpp b/source/MemorySentinel.hpp index d7f266d..7ebdb96 100644 --- a/source/MemorySentinel.hpp +++ b/source/MemorySentinel.hpp @@ -3,7 +3,7 @@ // ║║║├┤ ││││ │├┬┘└┬┘ ╚═╗├┤ │││ │ ││││├┤ │ // ╩ ╩└─┘┴ ┴└─┘┴└─ ┴ ╚═╝└─┘┘└┘ ┴ ┴┘└┘└─┘┴─┘ // -// © 2023 Lorenz Bucher - all rights reserved +// © 2025 Lorenz Bucher - all rights reserved // https://github.com/Sidelobe/MemorySentinel #pragma once diff --git a/test/MemorySentinelTests.cpp b/test/MemorySentinelTests.cpp index b790c93..9573e0b 100644 --- a/test/MemorySentinelTests.cpp +++ b/test/MemorySentinelTests.cpp @@ -3,7 +3,7 @@ // ║║║├┤ ││││ │├┬┘└┬┘ ╚═╗├┤ │││ │ ││││├┤ │ // ╩ ╩└─┘┴ ┴└─┘┴└─ ┴ ╚═╝└─┘┘└┘ ┴ ┴┘└┘└─┘┴─┘ // -// © 2023 Lorenz Bucher - all rights reserved +// © 2025 Lorenz Bucher - all rights reserved // https://github.com/Sidelobe/MemorySentinel #include @@ -23,6 +23,7 @@ #define REQUIRE_THROWS_AS(...) #endif + static decltype(auto) allocWithNew() { return new std::vector(32); } static decltype(auto) allocWithNewArray() { return new float[32]; } static decltype(auto) allocWithMalloc() { return std::malloc(32*sizeof(float)); } @@ -31,9 +32,68 @@ static decltype(auto) allocWithRealloc() { return std::realloc(nullptr, 32*si static decltype(auto) allocWithNewNoExcept() noexcept { return operator new(sizeof(std::vector(32)), std::nothrow); } static decltype(auto) allocWithNewArrayNoExcept() noexcept { return operator new[](sizeof(float[32]), std::nothrow); } +template +static void testAllocation(MemorySentinel& sentinel, T& allocFunc) +{ + sentinel.setArmed(true); + + volatile float* a = nullptr; // dummy to avoid optimization + REQUIRE_THROWS(a = (float*) allocFunc()); + + sentinel.setArmed(false); + REQUIRE(sentinel.getAndClearTransgressionsOccured()); + // freeing not necessary, since allocation was intercepted by exception +} + +template +static void testFreeing(MemorySentinel& sentinel, T& allocFunc, U& freeFunc) +{ + // allocate with unarmed sentinel + sentinel.setArmed(false); + volatile auto m = allocFunc(); + sentinel.setArmed(true); + + freeFunc(m); // always noexcept + + // freeing took place, no exception was thrown + REQUIRE(sentinel.getAndClearTransgressionsOccured()); + sentinel.setArmed(false); +} + +template +static void testDelete(MemorySentinel& sentinel, T& allocFunc) +{ + // allocate with unarmed sentinel + sentinel.setArmed(false); + auto m = allocFunc(); + sentinel.setArmed(true); + + operator delete(m); // always noexcept + + // deletion took place, no exception was thrown + REQUIRE(sentinel.getAndClearTransgressionsOccured()); + sentinel.setArmed(false); +} + +template +static void testDeleteArray(MemorySentinel& sentinel, T&& allocFunc) +{ + // allocate with unarmed sentinel + sentinel.setArmed(false); + auto m = allocFunc(); + sentinel.setArmed(true); + + operator delete[](m); // always noexcept + + // deletion took place, no exception was thrown + REQUIRE(sentinel.getAndClearTransgressionsOccured()); + sentinel.setArmed(false); +} + TEST_CASE("MemorySentinel Tests: zero allocation quota (default)") { MemorySentinel& sentinel = MemorySentinel::getInstance(); + sentinel.getAndClearTransgressionsOccured(); SECTION("SILENT") { MemorySentinel::setTransgressionBehaviour(MemorySentinel::TransgressionBehaviour::SILENT); @@ -60,6 +120,7 @@ TEST_CASE("MemorySentinel Tests: zero allocation quota (default)") REQUIRE(sentinel.getAndClearTransgressionsOccured()); delete[] heapArray; // clean up REQUIRE(sentinel.getAndClearTransgressionsOccured()); + sentinel.setArmed(false); } SECTION("LOG") { @@ -72,80 +133,59 @@ TEST_CASE("MemorySentinel Tests: zero allocation quota (default)") REQUIRE(sentinel.getAndClearTransgressionsOccured()); delete heapObject; // clean up REQUIRE(sentinel.getAndClearTransgressionsOccured()); + sentinel.setArmed(false); } #ifndef SLB_EXCEPTIONS_DISABLED SECTION("THROW_EXCEPTION - new/delete") { MemorySentinel::setTransgressionBehaviour(MemorySentinel::TransgressionBehaviour::THROW_EXCEPTION); - sentinel.setArmed(true); - REQUIRE_THROWS(allocWithNew()); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception - - sentinel.setArmed(true); - REQUIRE_THROWS(allocWithNewArray()); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception + testAllocation(sentinel, allocWithNew); + testAllocation(sentinel, allocWithNewArray); + sentinel.setArmed(true); void* p1; - REQUIRE_NOTHROW(p1 = allocWithNewNoExcept()); + p1 = allocWithNewNoExcept(); REQUIRE(p1 == nullptr); REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception + // freeing not necessary, since allocation was intercepted by exception sentinel.setArmed(true); void* p2; - REQUIRE_NOTHROW(p2 = allocWithNewArrayNoExcept()); - //REQUIRE(p2 == nullptr); + p2 = allocWithNewArrayNoExcept(); + REQUIRE(p2 == nullptr); REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception + // freeing not necessary, since allocation was intercepted by exception + + testDelete(sentinel, allocWithNew); + testDelete(sentinel, allocWithNewNoExcept); + + testDeleteArray(sentinel, allocWithNewArray); + testDeleteArray(sentinel, allocWithNewArrayNoExcept); sentinel.setArmed(false); } #if (defined(__clang__) || defined(__GNUC__)) && !defined(__GLIBC__) - SECTION("malloc") { - sentinel.setArmed(true); - REQUIRE_THROWS(allocWithMalloc()); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception - - sentinel.setArmed(false); - auto m = allocWithMalloc(); - sentinel.setArmed(true); - REQUIRE_THROWS(free(m)); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - if (m) free(m); - } - SECTION("calloc") { - sentinel.setArmed(true); - REQUIRE_THROWS(allocWithCalloc()); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception - - sentinel.setArmed(false); - auto m = allocWithCalloc(); - sentinel.setArmed(true); - REQUIRE_THROWS(free(m)); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - if (m) free(m); - } - SECTION("realloc") { - sentinel.setArmed(true); - REQUIRE_THROWS(allocWithRealloc()); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - // clean-up not necessary, since allocation was intercepted by exception - - sentinel.setArmed(false); - auto m = allocWithRealloc(); - sentinel.setArmed(true); - REQUIRE_THROWS(free(m)); - REQUIRE(sentinel.getAndClearTransgressionsOccured()); - if (m) free(m); - } - #endif -#endif - sentinel.setArmed(false); + SECTION("THROW_EXCEPTION - malloc/free") { + MemorySentinel::setTransgressionBehaviour(MemorySentinel::TransgressionBehaviour::THROW_EXCEPTION); + testAllocation(sentinel, allocWithMalloc); + testFreeing(sentinel, allocWithMalloc, free); + } + + SECTION("THROW_EXCEPTION - calloc/free") { + MemorySentinel::setTransgressionBehaviour(MemorySentinel::TransgressionBehaviour::THROW_EXCEPTION); + testAllocation(sentinel, allocWithCalloc); + testFreeing(sentinel, allocWithCalloc, free); + } + + SECTION("THROW_EXCEPTION - realloc/free") { + MemorySentinel::setTransgressionBehaviour(MemorySentinel::TransgressionBehaviour::THROW_EXCEPTION); + testAllocation(sentinel, allocWithRealloc); + testFreeing(sentinel, allocWithRealloc, free); + } + #endif // (defined(__clang__) || defined(__GNUC__)) && !defined(__GLIBC__) + +#endif // SLB_EXCEPTIONS_DISABLED // After tests, disarm Sentinel sentinel.clearTransgressions(); @@ -176,7 +216,7 @@ TEST_CASE("ScopedMemorySentinel Tests") std::vector* heapObject; { - // this will throw a std::bad_alloc (allocating 1 byte too many) + // allocation size is just right ScopedMemorySentinel sentinel(bytesAllocatedFor32FloatVector); heapObject = allocWithNew(); } diff --git a/test/external-utils/catch2/Catch.cmake b/test/external-utils/catch2/Catch.cmake new file mode 100644 index 0000000..a388516 --- /dev/null +++ b/test/external-utils/catch2/Catch.cmake @@ -0,0 +1,206 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +Catch +----- + +This module defines a function to help use the Catch test framework. + +The :command:`catch_discover_tests` discovers tests by asking the compiled test +executable to enumerate its tests. This does not require CMake to be re-run +when tests change. However, it may not work in a cross-compiling environment, +and setting test properties is less convenient. + +This command is intended to replace use of :command:`add_test` to register +tests, and will create a separate CTest test for each Catch test case. Note +that this is in some cases less efficient, as common set-up and tear-down logic +cannot be shared by multiple test cases executing in the same instance. +However, it provides more fine-grained pass/fail information to CTest, which is +usually considered as more beneficial. By default, the CTest test name is the +same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. + +.. command:: catch_discover_tests + + Automatically add tests with CTest by querying the compiled test executable + for available tests:: + + catch_discover_tests(target + [TEST_SPEC arg1...] + [EXTRA_ARGS arg1...] + [WORKING_DIRECTORY dir] + [TEST_PREFIX prefix] + [TEST_SUFFIX suffix] + [PROPERTIES name1 value1...] + [TEST_LIST var] + [REPORTER reporter] + [OUTPUT_DIR dir] + [OUTPUT_PREFIX prefix} + [OUTPUT_SUFFIX suffix] + ) + + ``catch_discover_tests`` sets up a post-build command on the test executable + that generates the list of tests by parsing the output from running the test + with the ``--list-test-names-only`` argument. This ensures that the full + list of tests is obtained. Since test discovery occurs at build time, it is + not necessary to re-run CMake when the list of tests changes. + However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set + in order to function in a cross-compiling environment. + + Additionally, setting properties on tests is somewhat less convenient, since + the tests are not available at CMake time. Additional test properties may be + assigned to the set of tests as a whole using the ``PROPERTIES`` option. If + more fine-grained test control is needed, custom content may be provided + through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` + directory property. The set of discovered tests is made accessible to such a + script via the ``_TESTS`` variable. + + The options are: + + ``target`` + Specifies the Catch executable, which must be a known CMake executable + target. CMake will substitute the location of the built executable when + running the test. + + ``TEST_SPEC arg1...`` + Specifies test cases, wildcarded test cases, tags and tag expressions to + pass to the Catch executable with the ``--list-test-names-only`` argument. + + ``EXTRA_ARGS arg1...`` + Any extra arguments to pass on the command line to each test case. + + ``WORKING_DIRECTORY dir`` + Specifies the directory in which to run the discovered test cases. If this + option is not provided, the current binary directory is used. + + ``TEST_PREFIX prefix`` + Specifies a ``prefix`` to be prepended to the name of each discovered test + case. This can be useful when the same test executable is being used in + multiple calls to ``catch_discover_tests()`` but with different + ``TEST_SPEC`` or ``EXTRA_ARGS``. + + ``TEST_SUFFIX suffix`` + Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of + every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may + be specified. + + ``PROPERTIES name1 value1...`` + Specifies additional properties to be set on all tests discovered by this + invocation of ``catch_discover_tests``. + + ``TEST_LIST var`` + Make the list of tests available in the variable ``var``, rather than the + default ``_TESTS``. This can be useful when the same test + executable is being used in multiple calls to ``catch_discover_tests()``. + Note that this variable is only available in CTest. + + ``REPORTER reporter`` + Use the specified reporter when running the test case. The reporter will + be passed to the Catch executable as ``--reporter reporter``. + + ``OUTPUT_DIR dir`` + If specified, the parameter is passed along as + ``--out dir/`` to Catch executable. The actual file name is the + same as the test name. This should be used instead of + ``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output + when using parallel test execution. + + ``OUTPUT_PREFIX prefix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``prefix`` is added to each output file name, like so + ``--out dir/prefix``. + + ``OUTPUT_SUFFIX suffix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``suffix`` is added to each output file name, like so + ``--out dir/suffix``. This can be used to add a file extension to + the output e.g. ".xml". + +#]=======================================================================] + +#------------------------------------------------------------------------------ +function(catch_discover_tests TARGET) + cmake_parse_arguments( + "" + "" + "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST;REPORTER;OUTPUT_DIR;OUTPUT_PREFIX;OUTPUT_SUFFIX" + "TEST_SPEC;EXTRA_ARGS;PROPERTIES" + ${ARGN} + ) + + if(NOT _WORKING_DIRECTORY) + set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endif() + if(NOT _TEST_LIST) + set(_TEST_LIST ${TARGET}_TESTS) + endif() + + ## Generate a unique name based on the extra arguments + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX}") + string(SUBSTRING ${args_hash} 0 7 args_hash) + + # Define rule to generate test list for aforementioned test executable + set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") + set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") + get_property(crosscompiling_emulator + TARGET ${TARGET} + PROPERTY CROSSCOMPILING_EMULATOR + ) + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TEST_TARGET=${TARGET}" + -D "TEST_EXECUTABLE=$" + -D "TEST_EXECUTOR=${crosscompiling_emulator}" + -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" + -D "TEST_SPEC=${_TEST_SPEC}" + -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" + -D "TEST_PROPERTIES=${_PROPERTIES}" + -D "TEST_PREFIX=${_TEST_PREFIX}" + -D "TEST_SUFFIX=${_TEST_SUFFIX}" + -D "TEST_LIST=${_TEST_LIST}" + -D "TEST_REPORTER=${_REPORTER}" + -D "TEST_OUTPUT_DIR=${_OUTPUT_DIR}" + -D "TEST_OUTPUT_PREFIX=${_OUTPUT_PREFIX}" + -D "TEST_OUTPUT_SUFFIX=${_OUTPUT_SUFFIX}" + -D "CTEST_FILE=${ctest_tests_file}" + -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" + VERBATIM + ) + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") + # Add discovered tests to directory TEST_INCLUDE_FILES + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + else() + # Add discovered tests as directory TEST_INCLUDE_FILE if possible + get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) + if (NOT ${test_include_file_set}) + set_property(DIRECTORY + PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" + ) + else() + message(FATAL_ERROR + "Cannot set more than one TEST_INCLUDE_FILE" + ) + endif() + endif() + +endfunction() + +############################################################################### + +set(_CATCH_DISCOVER_TESTS_SCRIPT + ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake + CACHE INTERNAL "Catch2 full path to CatchAddTests.cmake helper file" +) diff --git a/test/external-utils/catch2/CatchAddTests.cmake b/test/external-utils/catch2/CatchAddTests.cmake new file mode 100644 index 0000000..184e506 --- /dev/null +++ b/test/external-utils/catch2/CatchAddTests.cmake @@ -0,0 +1,135 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +set(prefix "${TEST_PREFIX}") +set(suffix "${TEST_SUFFIX}") +set(spec ${TEST_SPEC}) +set(extra_args ${TEST_EXTRA_ARGS}) +set(properties ${TEST_PROPERTIES}) +set(reporter ${TEST_REPORTER}) +set(output_dir ${TEST_OUTPUT_DIR}) +set(output_prefix ${TEST_OUTPUT_PREFIX}) +set(output_suffix ${TEST_OUTPUT_SUFFIX}) +set(script) +set(suite) +set(tests) + +function(add_command NAME) + set(_args "") + # use ARGV* instead of ARGN, because ARGN splits arrays into multiple arguments + math(EXPR _last_arg ${ARGC}-1) + foreach(_n RANGE 1 ${_last_arg}) + set(_arg "${ARGV${_n}}") + if(_arg MATCHES "[^-./:a-zA-Z0-9_]") + set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument + else() + set(_args "${_args} ${_arg}") + endif() + endforeach() + set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) +endfunction() + +# Run test executable to get list of available tests +if(NOT EXISTS "${TEST_EXECUTABLE}") + message(FATAL_ERROR + "Specified test executable '${TEST_EXECUTABLE}' does not exist" + ) +endif() +execute_process( + COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-names-only + OUTPUT_VARIABLE output + RESULT_VARIABLE result + WORKING_DIRECTORY "${TEST_WORKING_DIR}" +) +# Catch --list-test-names-only reports the number of tests, so 0 is... surprising +if(${result} EQUAL 0) + message(WARNING + "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" + ) +elseif(${result} LESS 0) + message(FATAL_ERROR + "Error running test executable '${TEST_EXECUTABLE}':\n" + " Result: ${result}\n" + " Output: ${output}\n" + ) +endif() + +string(REPLACE "\n" ";" output "${output}") + +# Run test executable to get list of available reporters +execute_process( + COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-reporters + OUTPUT_VARIABLE reporters_output + RESULT_VARIABLE reporters_result + WORKING_DIRECTORY "${TEST_WORKING_DIR}" +) +if(${reporters_result} EQUAL 0) + message(WARNING + "Test executable '${TEST_EXECUTABLE}' contains no reporters!\n" + ) +elseif(${reporters_result} LESS 0) + message(FATAL_ERROR + "Error running test executable '${TEST_EXECUTABLE}':\n" + " Result: ${reporters_result}\n" + " Output: ${reporters_output}\n" + ) +endif() +string(FIND "${reporters_output}" "${reporter}" reporter_is_valid) +if(reporter AND ${reporter_is_valid} EQUAL -1) + message(FATAL_ERROR + "\"${reporter}\" is not a valid reporter!\n" + ) +endif() + +# Prepare reporter +if(reporter) + set(reporter_arg "--reporter ${reporter}") +endif() + +# Prepare output dir +if(output_dir AND NOT IS_ABSOLUTE ${output_dir}) + set(output_dir "${TEST_WORKING_DIR}/${output_dir}") + if(NOT EXISTS ${output_dir}) + file(MAKE_DIRECTORY ${output_dir}) + endif() +endif() + +# Parse output +foreach(line ${output}) + set(test ${line}) + # Escape characters in test case names that would be parsed by Catch2 + set(test_name ${test}) + foreach(char , [ ]) + string(REPLACE ${char} "\\${char}" test_name ${test_name}) + endforeach(char) + # ...add output dir + if(output_dir) + string(REGEX REPLACE "[^A-Za-z0-9_]" "_" test_name_clean ${test_name}) + set(output_dir_arg "--out ${output_dir}/${output_prefix}${test_name_clean}${output_suffix}") + endif() + + # ...and add to script + add_command(add_test + "${prefix}${test}${suffix}" + ${TEST_EXECUTOR} + "${TEST_EXECUTABLE}" + "${test_name}" + ${extra_args} + "${reporter_arg}" + "${output_dir_arg}" + ) + add_command(set_tests_properties + "${prefix}${test}${suffix}" + PROPERTIES + WORKING_DIRECTORY "${TEST_WORKING_DIR}" + ${properties} + ) + list(APPEND tests "${prefix}${test}${suffix}") +endforeach() + +# Create a list of all discovered tests, which users may use to e.g. set +# properties on the tests +add_command(set ${TEST_LIST} ${tests}) + +# Write CTest script +file(WRITE "${CTEST_FILE}" "${script}") diff --git a/test/external-utils/catch2/ParseAndAddCatchTests.cmake b/test/external-utils/catch2/ParseAndAddCatchTests.cmake deleted file mode 100644 index 925d932..0000000 --- a/test/external-utils/catch2/ParseAndAddCatchTests.cmake +++ /dev/null @@ -1,225 +0,0 @@ -#==================================================================================================# -# supported macros # -# - TEST_CASE, # -# - SCENARIO, # -# - TEST_CASE_METHOD, # -# - CATCH_TEST_CASE, # -# - CATCH_SCENARIO, # -# - CATCH_TEST_CASE_METHOD. # -# # -# Usage # -# 1. make sure this module is in the path or add this otherwise: # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# 2. make sure that you've enabled testing option for the project by the call: # -# enable_testing() # -# 3. add the lines to the script for testing target (sample CMakeLists.txt): # -# project(testing_target) # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# enable_testing() # -# # -# find_path(CATCH_INCLUDE_DIR "catch.hpp") # -# include_directories(${INCLUDE_DIRECTORIES} ${CATCH_INCLUDE_DIR}) # -# # -# file(GLOB SOURCE_FILES "*.cpp") # -# add_executable(${PROJECT_NAME} ${SOURCE_FILES}) # -# # -# include(ParseAndAddCatchTests) # -# ParseAndAddCatchTests(${PROJECT_NAME}) # -# # -# The following variables affect the behavior of the script: # -# # -# PARSE_CATCH_TESTS_VERBOSE (Default OFF) # -# -- enables debug messages # -# PARSE_CATCH_TESTS_NO_HIDDEN_TESTS (Default OFF) # -# -- excludes tests marked with [!hide], [.] or [.foo] tags # -# PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME (Default ON) # -# -- adds fixture class name to the test name # -# PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME (Default ON) # -# -- adds cmake target name to the test name # -# PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # -# -- causes CMake to rerun when file with tests changes so that new tests will be discovered # -# # -# One can also set (locally) the optional variable OptionalCatchTestLauncher to precise the way # -# a test should be run. For instance to use test MPI, one can write # -# set(OptionalCatchTestLauncher ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${NUMPROC}) # -# just before calling this ParseAndAddCatchTests function # -# # -# The AdditionalCatchParameters optional variable can be used to pass extra argument to the test # -# command. For example, to include successful tests in the output, one can write # -# set(AdditionalCatchParameters --success) # -# # -# After the script, the ParseAndAddCatchTests_TESTS property for the target, and for each source # -# file in the target is set, and contains the list of the tests extracted from that target, or # -# from that file. This is useful, for example to add further labels or properties to the tests. # -# # -#==================================================================================================# - -if (CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.8) - message(FATAL_ERROR "ParseAndAddCatchTests requires CMake 2.8.8 or newer") -endif() - -option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) -option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) -option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) - -function(ParseAndAddCatchTests_PrintDebugMessage) - if(PARSE_CATCH_TESTS_VERBOSE) - message(STATUS "ParseAndAddCatchTests: ${ARGV}") - endif() -endfunction() - -# This removes the contents between -# - block comments (i.e. /* ... */) -# - full line comments (i.e. // ... ) -# contents have been read into '${CppCode}'. -# !keep partial line comments -function(ParseAndAddCatchTests_RemoveComments CppCode) - string(ASCII 2 CMakeBeginBlockComment) - string(ASCII 3 CMakeEndBlockComment) - string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\\*/" "${CMakeEndBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "${CMakeBeginBlockComment}[^${CMakeEndBlockComment}]*${CMakeEndBlockComment}" "" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\n[ \t]*//+[^\n]+" "\n" ${CppCode} "${${CppCode}}") - - set(${CppCode} "${${CppCode}}" PARENT_SCOPE) -endfunction() - -# Worker function -function(ParseAndAddCatchTests_ParseFile SourceFile TestTarget) - # If SourceFile is an object library, do not scan it (as it is not a file). Exit without giving a warning about a missing file. - if(SourceFile MATCHES "\\\$") - ParseAndAddCatchTests_PrintDebugMessage("Detected OBJECT library: ${SourceFile} this will not be scanned for tests.") - return() - endif() - # According to CMake docs EXISTS behavior is well-defined only for full paths. - get_filename_component(SourceFile ${SourceFile} ABSOLUTE) - if(NOT EXISTS ${SourceFile}) - message(WARNING "Cannot find source file: ${SourceFile}") - return() - endif() - ParseAndAddCatchTests_PrintDebugMessage("parsing ${SourceFile}") - file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) - - # Remove block and fullline comments - ParseAndAddCatchTests_RemoveComments(Contents) - - # Find definition of test names - string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") - - if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) - ParseAndAddCatchTests_PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") - set_property( - DIRECTORY - APPEND - PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} - ) - endif() - - foreach(TestName ${Tests}) - # Strip newlines - string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") - - # Get test type and fixture if applicable - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") - string(REGEX REPLACE "${TestType}\\([ \t]*" "" TestFixture "${TestTypeAndFixture}") - - # Get string parts of test definition - string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") - - # Strip wrapping quotation marks - string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") - string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") - - # Validate that a test name and tags have been provided - list(LENGTH TestStrings TestStringsLength) - if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) - message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") - endif() - - # Assign name and tags - list(GET TestStrings 0 Name) - if("${TestType}" STREQUAL "SCENARIO") - set(Name "Scenario: ${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) - set(CTestName "${TestFixture}:${Name}") - else() - set(CTestName "${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) - set(CTestName "${TestTarget}:${CTestName}") - endif() - # add target to labels to enable running all tests added from this target - set(Labels ${TestTarget}) - if(TestStringsLength EQUAL 2) - list(GET TestStrings 1 Tags) - string(TOLOWER "${Tags}" Tags) - # remove target from labels if the test is hidden - if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") - list(REMOVE_ITEM Labels ${TestTarget}) - endif() - string(REPLACE "]" ";" Tags "${Tags}") - string(REPLACE "[" "" Tags "${Tags}") - else() - # unset tags variable from previous loop - unset(Tags) - endif() - - list(APPEND Labels ${Tags}) - - set(HiddenTagFound OFF) - foreach(label ${Labels}) - string(REGEX MATCH "^!hide|^\\." result ${label}) - if(result) - set(HiddenTagFound ON) - break() - endif(result) - endforeach(label) - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_LESS "3.9") - ParseAndAddCatchTests_PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") - else() - ParseAndAddCatchTests_PrintDebugMessage("Adding test \"${CTestName}\"") - if(Labels) - ParseAndAddCatchTests_PrintDebugMessage("Setting labels to ${Labels}") - endif() - - # Escape commas in the test spec - string(REPLACE "," "\\," Name ${Name}) - - # Add the test and set its properties - add_test(NAME "\"${CTestName}\"" COMMAND ${OptionalCatchTestLauncher} $ ${Name} ${AdditionalCatchParameters}) - # Old CMake versions do not document VERSION_GREATER_EQUAL, so we use VERSION_GREATER with 3.8 instead - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_GREATER "3.8") - ParseAndAddCatchTests_PrintDebugMessage("Setting DISABLED test property") - set_tests_properties("\"${CTestName}\"" PROPERTIES DISABLED ON) - else() - set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" - LABELS "${Labels}") - endif() - set_property( - TARGET ${TestTarget} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - set_property( - SOURCE ${SourceFile} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - endif() - - - endforeach() -endfunction() - -# entry point -function(ParseAndAddCatchTests TestTarget) - ParseAndAddCatchTests_PrintDebugMessage("Started parsing ${TestTarget}") - get_target_property(SourceFiles ${TestTarget} SOURCES) - ParseAndAddCatchTests_PrintDebugMessage("Found the following sources: ${SourceFiles}") - foreach(SourceFile ${SourceFiles}) - ParseAndAddCatchTests_ParseFile(${SourceFile} ${TestTarget}) - endforeach() - ParseAndAddCatchTests_PrintDebugMessage("Finished parsing ${TestTarget}") -endfunction()