diff options
| author | Leitwolf11 | 2026-09-24 22:07:03 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-09-24 22:07:03 +0300 |
| commit | 9ad804cedf4cc4a85ef89fe097f97a3b44654360 (patch) | |
| tree | f088e919776b22d19387377bdbec727daa2429c0 /src | |
| parent | 02f8b5593a15cdec8558256e6f40a0c7fb5862d6 (diff) | |
| download | catch3-0.1.0.tar.gz | |
Add Progmasoft Catch3 property testing and build support0.1.0
Introduce deterministic property execution, built-in generators, shrinking, replayable seeds, and a separate Progmasoft-owned header API while preserving Catch2 compatibility targets and the C++14 engine baseline.
Add Bazel, Meson, Xmake, and Premake integration and tests alongside the existing CMake build. Replace Linux-only workflow coverage with Windows and macOS jobs and document the Catch2/Catch3 distinction.
Diffstat (limited to 'src')
| -rw-r--r-- | src/CMakeLists.txt | 43 | ||||
| -rw-r--r-- | src/Progmasoft/BUILD.bazel | 20 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3.hpp | 13 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Assertions.hpp | 17 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generator.hpp | 61 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generators/Boolean.hpp | 18 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generators/Integer.hpp | 82 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Property.hpp | 244 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Random.hpp | 113 | ||||
| -rw-r--r-- | src/Progmasoft/ProgmasoftCatch3Config.cmake.in | 7 | ||||
| -rw-r--r-- | src/Progmasoft/meson.build | 18 |
11 files changed, 636 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 886605f1..cb8c339d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -360,6 +360,19 @@ if(CATCH_ENABLE_REPRODUCIBLE_BUILD) endif() add_library(Catch2::Catch2 ALIAS Catch2) +# Progmasoft's C++20 additions are a separate target so the Catch2 +# compatibility engine can keep its existing, broader C++14 baseline. +add_library(ProgmasoftCatch3 INTERFACE) +add_library(Progmasoft::Catch3 ALIAS ProgmasoftCatch3) +set_target_properties(ProgmasoftCatch3 PROPERTIES EXPORT_NAME Catch3) +target_link_libraries(ProgmasoftCatch3 INTERFACE Catch2::Catch2) +target_include_directories(ProgmasoftCatch3 + INTERFACE + $<BUILD_INTERFACE:${CATCH_DIR}/src> + $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> +) +target_compile_features(ProgmasoftCatch3 INTERFACE cxx_std_20) + if(ANDROID) target_link_libraries(Catch2 PRIVATE log) endif() @@ -436,6 +449,36 @@ if(NOT_SUBPROJECT) FILES_MATCHING PATTERN "*.hpp" ) + + # Keep the independently licensed Progmasoft headers beside Catch2's public + # headers while preserving a separate CMake target and package namespace. + install( + DIRECTORY "${CATCH_DIR}/src/Progmasoft" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILES_MATCHING PATTERN "*.hpp" + ) + install(TARGETS ProgmasoftCatch3 EXPORT ProgmasoftCatch3Targets) + install( + EXPORT ProgmasoftCatch3Targets + NAMESPACE Progmasoft:: + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ProgmasoftCatch3" + ) + configure_package_config_file( + "${CATCH_DIR}/src/Progmasoft/ProgmasoftCatch3Config.cmake.in" + "${PROJECT_BINARY_DIR}/ProgmasoftCatch3Config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ProgmasoftCatch3" + ) + write_basic_package_version_file( + "${PROJECT_BINARY_DIR}/ProgmasoftCatch3ConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion + ) + install( + FILES + "${PROJECT_BINARY_DIR}/ProgmasoftCatch3Config.cmake" + "${PROJECT_BINARY_DIR}/ProgmasoftCatch3ConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ProgmasoftCatch3" + ) endif() # Some tests require a full recompilation of Catch2 lib with different diff --git a/src/Progmasoft/BUILD.bazel b/src/Progmasoft/BUILD.bazel new file mode 100644 index 00000000..9927aafa --- /dev/null +++ b/src/Progmasoft/BUILD.bazel @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Progmasoft. +# SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +load("@rules_cc//cc:defs.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "catch3", + hdrs = [ + "Catch3.hpp", + "Catch3/Assertions.hpp", + "Catch3/Generator.hpp", + "Catch3/Generators/Boolean.hpp", + "Catch3/Generators/Integer.hpp", + "Catch3/Property.hpp", + "Catch3/Random.hpp", + ], + strip_include_prefix = "/src", + deps = ["//:catch2"], +) diff --git a/src/Progmasoft/Catch3.hpp b/src/Progmasoft/Catch3.hpp new file mode 100644 index 00000000..1ae73e53 --- /dev/null +++ b/src/Progmasoft/Catch3.hpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +// Convenience include: prefer narrow headers when compile time matters. +#include <catch2/catch_all.hpp> + +#include <Progmasoft/Catch3/Assertions.hpp> +#include <Progmasoft/Catch3/Generator.hpp> +#include <Progmasoft/Catch3/Generators/Boolean.hpp> +#include <Progmasoft/Catch3/Generators/Integer.hpp> +#include <Progmasoft/Catch3/Property.hpp> +#include <Progmasoft/Catch3/Random.hpp> diff --git a/src/Progmasoft/Catch3/Assertions.hpp b/src/Progmasoft/Catch3/Assertions.hpp new file mode 100644 index 00000000..19ac13ef --- /dev/null +++ b/src/Progmasoft/Catch3/Assertions.hpp @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <catch2/catch_message.hpp> +#include <catch2/catch_test_macros.hpp> + +#include <Progmasoft/Catch3/Property.hpp> + +/// Runs a property and reports its replay seed and minimized counterexample. +#define CATCH3_CHECK_PROPERTY( ... ) \ + do { \ + const auto catch3PropertyResult = \ + ::Progmasoft::Catch3::CheckProperty( __VA_ARGS__ ); \ + INFO( catch3PropertyResult.Describe() ); \ + CHECK( catch3PropertyResult.Succeeded() ); \ + } while ( false ) diff --git a/src/Progmasoft/Catch3/Generator.hpp b/src/Progmasoft/Catch3/Generator.hpp new file mode 100644 index 00000000..0e48aaa3 --- /dev/null +++ b/src/Progmasoft/Catch3/Generator.hpp @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <Progmasoft/Catch3/Random.hpp> + +#include <functional> +#include <stdexcept> +#include <utility> +#include <vector> + +namespace Progmasoft::Catch3 { + + /// A value source paired with an ordered sequence of simpler candidates. + /// + /// The property runner owns the shrinking policy; generators only describe + /// which candidates are valid and which order is most useful for + /// minimization. + template <typename ValueType> + class Generator final { + public: + using GenerationFunction = std::function<ValueType( Random& )>; + using ShrinkingFunction = + std::function<std::vector<ValueType>( const ValueType& )>; + + Generator( GenerationFunction generate, ShrinkingFunction shrink ): + generate_( std::move( generate ) ), shrink_( std::move( shrink ) ) { + if ( !generate_ || !shrink_ ) { + throw std::invalid_argument( + "a generator requires both callbacks" ); + } + } + + [[nodiscard]] ValueType Generate( Random& random ) const { + return generate_( random ); + } + + [[nodiscard]] std::vector<ValueType> + Shrink( const ValueType& value ) const { + return shrink_( value ); + } + + private: + GenerationFunction generate_; + ShrinkingFunction shrink_; + }; + + /// Adapts user callables into a reusable generator. + template <typename ValueType, + typename GenerateFunction, + typename ShrinkFunction> + [[nodiscard]] Generator<ValueType> + MakeGenerator( GenerateFunction&& generate, ShrinkFunction&& shrink ) { + return Generator<ValueType>( + typename Generator<ValueType>::GenerationFunction( + std::forward<GenerateFunction>( generate ) ), + typename Generator<ValueType>::ShrinkingFunction( + std::forward<ShrinkFunction>( shrink ) ) ); + } + +} // namespace Progmasoft::Catch3 diff --git a/src/Progmasoft/Catch3/Generators/Boolean.hpp b/src/Progmasoft/Catch3/Generators/Boolean.hpp new file mode 100644 index 00000000..3f881927 --- /dev/null +++ b/src/Progmasoft/Catch3/Generators/Boolean.hpp @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <Progmasoft/Catch3/Generator.hpp> + +namespace Progmasoft::Catch3 { + + /// Generates booleans and shrinks true to the simplest value, false. + [[nodiscard]] inline Generator<bool> Boolean() { + return MakeGenerator<bool>( + []( Random& random ) { return ( random.Next() & 1U ) != 0; }, + []( const bool& value ) { + return value ? std::vector<bool>{ false } : std::vector<bool>{}; + } ); + } + +} // namespace Progmasoft::Catch3 diff --git a/src/Progmasoft/Catch3/Generators/Integer.hpp b/src/Progmasoft/Catch3/Generators/Integer.hpp new file mode 100644 index 00000000..2a50525e --- /dev/null +++ b/src/Progmasoft/Catch3/Generators/Integer.hpp @@ -0,0 +1,82 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <Progmasoft/Catch3/Generator.hpp> + +#include <algorithm> +#include <concepts> +#include <limits> +#include <type_traits> + +namespace Progmasoft::Catch3 { + namespace Detail { + + template <std::integral IntegerType> + requires( !std::same_as<IntegerType, bool> ) + [[nodiscard]] std::vector<IntegerType> ShrinkInteger( + IntegerType value, IntegerType minimum, IntegerType maximum ) { + std::vector<IntegerType> candidates; + const auto addCandidate = [&]( IntegerType candidate ) { + if ( candidate < minimum || candidate > maximum || + candidate == value || + std::ranges::find( candidates, candidate ) != + candidates.end() ) { + return; + } + candidates.push_back( candidate ); + }; + + if ( value == 0 ) { return candidates; } + + // First try the conventional simplest value, then halve toward it. + // The adjacent value at the end lets the runner find boundary + // counterexamples (for example, the first failing integer in a + // threshold property). + if ( minimum <= 0 && maximum >= 0 ) { addCandidate( 0 ); } + + IntegerType smaller = value; + while ( smaller != 0 ) { + smaller = static_cast<IntegerType>( smaller / 2 ); + if ( smaller == 0 ) { break; } + addCandidate( smaller ); + } + + if ( value > 0 ) { + if ( value > minimum ) { + addCandidate( static_cast<IntegerType>( value - 1 ) ); + } + if ( minimum > 0 ) { addCandidate( minimum ); } + } else { + if ( value < maximum ) { + addCandidate( static_cast<IntegerType>( value + 1 ) ); + } + if ( maximum < 0 ) { addCandidate( maximum ); } + } + return candidates; + } + + } // namespace Detail + + /// Generates integers over the inclusive range and shrinks toward simple + /// values. + template <std::integral IntegerType> + requires( !std::same_as<IntegerType, bool> ) + [[nodiscard]] Generator<IntegerType> Integer( + IntegerType minimum = std::numeric_limits<IntegerType>::lowest(), + IntegerType maximum = std::numeric_limits<IntegerType>::max() ) { + if ( minimum > maximum ) { + throw std::invalid_argument( + "Integer requires minimum <= maximum" ); + } + + return MakeGenerator<IntegerType>( + [minimum, maximum]( Random& random ) { + return random.Between( minimum, maximum ); + }, + [minimum, maximum]( const IntegerType& value ) { + return Detail::ShrinkInteger( value, minimum, maximum ); + } ); + } + +} // namespace Progmasoft::Catch3 diff --git a/src/Progmasoft/Catch3/Property.hpp b/src/Progmasoft/Catch3/Property.hpp new file mode 100644 index 00000000..57e74408 --- /dev/null +++ b/src/Progmasoft/Catch3/Property.hpp @@ -0,0 +1,244 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <catch2/catch_tostring.hpp> + +#include <Progmasoft/Catch3/Generator.hpp> + +#include <concepts> +#include <cstddef> +#include <cstdint> +#include <exception> +#include <functional> +#include <optional> +#include <sstream> +#include <string> +#include <type_traits> +#include <utility> + +namespace Progmasoft::Catch3 { + + /// Controls a property run without changing the compatibility runner's CLI. + struct PropertyOptions final { + std::size_t Trials = 100; + std::uint64_t Seed = 0xC47C4A53ULL; + std::size_t MaxShrinkSteps = 1000; + }; + + enum class PropertyStatus : std::uint8_t { + kPassed, + kFailed, + kErrored, + kInvalidConfiguration, + }; + + struct PropertyFailure final { + std::uint64_t RootSeed = 0; + std::uint64_t TrialSeed = 0; + std::size_t Trial = 0; + std::size_t ShrinkSteps = 0; + std::string Counterexample; + std::string Reason; + }; + + /// Stable, machine-readable outcome with a deterministic human-readable + /// report. + struct PropertyResult final { + PropertyStatus Status = PropertyStatus::kPassed; + std::size_t TrialsRun = 0; + std::optional<PropertyFailure> Failure; + std::string Error; + + [[nodiscard]] bool Succeeded() const noexcept { + return Status == PropertyStatus::kPassed; + } + + [[nodiscard]] std::string Describe() const { + std::ostringstream output; + switch ( Status ) { + case PropertyStatus::kPassed: + output << "property passed: " << TrialsRun << " trials"; + return output.str(); + case PropertyStatus::kInvalidConfiguration: + output << "invalid property configuration: " << Error; + return output.str(); + case PropertyStatus::kErrored: + output << "property execution errored after " << TrialsRun + << " trials: " << Error; + break; + case PropertyStatus::kFailed: + output << "property failed after " << TrialsRun << " trials"; + break; + } + + if ( Failure.has_value() ) { + output << "; failing trial " << Failure->Trial << ", root seed " + << Failure->RootSeed << ", replay seed " + << Failure->TrialSeed << ", shrink checks " + << Failure->ShrinkSteps + << "\ncounterexample: " << Failure->Counterexample + << "\nreason: " << Failure->Reason; + } + return output.str(); + } + }; + + namespace Detail { + + template <typename ValueType> + [[nodiscard]] std::string + StringifyPropertyValue( const ValueType& value ) { + return Catch::StringMaker<std::remove_cvref_t<ValueType>>::convert( + value ); + } + + template <typename Predicate, typename ValueType> + [[nodiscard]] std::optional<std::string> + EvaluateProperty( Predicate& predicate, const ValueType& value ) { + try { + if ( static_cast<bool>( std::invoke( predicate, value ) ) ) { + return std::nullopt; + } + return "predicate returned false"; + } catch ( const std::exception& exception ) { + return std::string( "predicate threw std::exception: " ) + + exception.what(); + } catch ( ... ) { + return "predicate threw a non-standard exception"; + } + } + + template <typename ValueType> + [[nodiscard]] std::string + DescribeValue( const ValueType& value ) noexcept { + try { + return StringifyPropertyValue( value ); + } catch ( const std::exception& exception ) { + return std::string( "<stringification failed: " ) + + exception.what() + ">"; + } catch ( ... ) { + return "<stringification failed with a non-standard exception>"; + } + } + + } // namespace Detail + + /// Runs independently seeded trials and greedily accepts failing shrink + /// candidates. + template <typename ValueType, typename Predicate> + requires std::invocable<Predicate&, const ValueType&>&& + std::convertible_to<std::invoke_result_t<Predicate&, const ValueType&>, + bool> [[nodiscard]] PropertyResult + CheckProperty( const Generator<ValueType>& generator, + Predicate predicate, + PropertyOptions options = {} ) { + if ( options.Trials == 0 ) { + return { PropertyStatus::kInvalidConfiguration, + 0, + std::nullopt, + "Trials must be greater than zero" }; + } + + PropertyResult result; + for ( std::size_t trial = 0; trial < options.Trials; ++trial ) { + result.TrialsRun = trial + 1; + const std::uint64_t trialSeed = + Random::DeriveSeed( options.Seed, trial ); + Random random( trialSeed ); + std::optional<ValueType> sample; + + try { + sample.emplace( generator.Generate( random ) ); + } catch ( const std::exception& exception ) { + result.Status = PropertyStatus::kErrored; + result.Error = + std::string( "generator threw std::exception: " ) + + exception.what(); + result.Failure = PropertyFailure{ options.Seed, + trialSeed, + trial + 1, + 0, + "<not generated>", + result.Error }; + return result; + } catch ( ... ) { + result.Status = PropertyStatus::kErrored; + result.Error = "generator threw a non-standard exception"; + result.Failure = PropertyFailure{ options.Seed, + trialSeed, + trial + 1, + 0, + "<not generated>", + result.Error }; + return result; + } + + auto reason = Detail::EvaluateProperty( predicate, *sample ); + if ( !reason.has_value() ) { continue; } + + std::size_t shrinkChecks = 0; + try { + bool improved = true; + while ( improved && shrinkChecks < options.MaxShrinkSteps ) { + improved = false; + const std::vector<ValueType> candidates = + generator.Shrink( *sample ); + for ( std::size_t candidateIndex = 0; + candidateIndex < candidates.size(); + ++candidateIndex ) { + if ( shrinkChecks >= options.MaxShrinkSteps ) { break; } + ++shrinkChecks; + const ValueType candidate = static_cast<ValueType>( + candidates[candidateIndex] ); + auto candidateReason = + Detail::EvaluateProperty( predicate, candidate ); + if ( candidateReason.has_value() ) { + sample.emplace( candidate ); + reason = std::move( candidateReason ); + improved = true; + break; + } + } + } + } catch ( const std::exception& exception ) { + result.Status = PropertyStatus::kErrored; + result.Error = + std::string( "shrinker threw std::exception: " ) + + exception.what(); + result.Failure = + PropertyFailure{ options.Seed, + trialSeed, + trial + 1, + shrinkChecks, + Detail::DescribeValue( *sample ), + *reason }; + return result; + } catch ( ... ) { + result.Status = PropertyStatus::kErrored; + result.Error = "shrinker threw a non-standard exception"; + result.Failure = + PropertyFailure{ options.Seed, + trialSeed, + trial + 1, + shrinkChecks, + Detail::DescribeValue( *sample ), + *reason }; + return result; + } + + result.Status = PropertyStatus::kFailed; + result.Failure = PropertyFailure{ options.Seed, + trialSeed, + trial + 1, + shrinkChecks, + Detail::DescribeValue( *sample ), + *reason }; + return result; + } + + result.Status = PropertyStatus::kPassed; + return result; + } + +} // namespace Progmasoft::Catch3 diff --git a/src/Progmasoft/Catch3/Random.hpp b/src/Progmasoft/Catch3/Random.hpp new file mode 100644 index 00000000..0a95cb84 --- /dev/null +++ b/src/Progmasoft/Catch3/Random.hpp @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <bit> +#include <concepts> +#include <cstdint> +#include <limits> +#include <stdexcept> +#include <type_traits> + +namespace Progmasoft::Catch3 { + + /// Small, deterministic random source for repeatable property-test cases. + /// + /// The SplitMix64 sequence is specified here instead of delegating to a + /// standard-library engine, whose output sequence is not portable between + /// library implementations. A Random object is intentionally not + /// thread-safe; property trials receive independent instances derived from + /// the root seed. + class Random final { + public: + explicit constexpr Random( std::uint64_t seed ) noexcept: + state_( seed ) {} + + /// Returns the next value in the stable SplitMix64 sequence. + [[nodiscard]] constexpr std::uint64_t Next() noexcept { + state_ += kGamma; + std::uint64_t value = state_; + value = ( value ^ ( value >> 30U ) ) * kMultiplierOne; + value = ( value ^ ( value >> 27U ) ) * kMultiplierTwo; + return value ^ ( value >> 31U ); + } + + /// Returns a value in [0, bound); bound == 0 requests the full uint64 + /// range. + [[nodiscard]] constexpr std::uint64_t + Below( std::uint64_t bound ) noexcept { + if ( bound == 0 ) { return Next(); } + + // Rejection sampling avoids the modulo bias of a single `% bound`. + const std::uint64_t threshold = + ( std::uint64_t{ 0 } - bound ) % bound; + while ( true ) { + const std::uint64_t value = Next(); + if ( value >= threshold ) { return value % bound; } + } + } + + /// Returns an inclusive integral value without overflowing at type + /// limits. + template <std::integral IntegerType> + requires( !std::same_as<IntegerType, bool> ) [[nodiscard]] IntegerType + Between( IntegerType minimum, IntegerType maximum ) { + if ( minimum > maximum ) { + throw std::invalid_argument( + "Random::Between requires minimum <= maximum" ); + } + + using UnsignedType = std::make_unsigned_t<IntegerType>; + static_assert( std::numeric_limits<UnsignedType>::digits <= 64 ); + + constexpr unsigned kBitCount = static_cast<unsigned>( + std::numeric_limits<UnsignedType>::digits ); + constexpr UnsignedType kSignBit = UnsignedType{ 1 } + << ( kBitCount - 1U ); + + const auto encode = + []( IntegerType value ) constexpr -> UnsignedType { + if constexpr ( std::is_signed_v<IntegerType> ) { + return std::bit_cast<UnsignedType>( value ) ^ kSignBit; + } else { + return value; + } + }; + const auto decode = + []( UnsignedType value ) constexpr -> IntegerType { + if constexpr ( std::is_signed_v<IntegerType> ) { + return std::bit_cast<IntegerType>( value ^ kSignBit ); + } else { + return value; + } + }; + + const UnsignedType lower = encode( minimum ); + const UnsignedType upper = encode( maximum ); + // Unsigned wrap to zero represents the complete value domain. + const UnsignedType width = + static_cast<UnsignedType>( upper - lower + UnsignedType{ 1 } ); + const UnsignedType offset = + width == 0 ? static_cast<UnsignedType>( Next() ) + : static_cast<UnsignedType>( + Below( static_cast<std::uint64_t>( width ) ) ); + return decode( static_cast<UnsignedType>( lower + offset ) ); + } + + /// Derives an independent, reproducible seed for one trial. + [[nodiscard]] static constexpr std::uint64_t + DeriveSeed( std::uint64_t rootSeed, + std::uint64_t trialIndex ) noexcept { + Random trialRandom( rootSeed + kGamma * ( trialIndex + 1U ) ); + return trialRandom.Next(); + } + + private: + static constexpr std::uint64_t kGamma = 0x9E3779B97F4A7C15ULL; + static constexpr std::uint64_t kMultiplierOne = 0xBF58476D1CE4E5B9ULL; + static constexpr std::uint64_t kMultiplierTwo = 0x94D049BB133111EBULL; + + std::uint64_t state_; + }; + +} // namespace Progmasoft::Catch3 diff --git a/src/Progmasoft/ProgmasoftCatch3Config.cmake.in b/src/Progmasoft/ProgmasoftCatch3Config.cmake.in new file mode 100644 index 00000000..a4b3ff31 --- /dev/null +++ b/src/Progmasoft/ProgmasoftCatch3Config.cmake.in @@ -0,0 +1,7 @@ +# Copyright (c) 2026 Progmasoft. +# SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Catch2 @PROJECT_VERSION@ CONFIG REQUIRED) +include("${CMAKE_CURRENT_LIST_DIR}/ProgmasoftCatch3Targets.cmake") diff --git a/src/Progmasoft/meson.build b/src/Progmasoft/meson.build new file mode 100644 index 00000000..109edaa4 --- /dev/null +++ b/src/Progmasoft/meson.build @@ -0,0 +1,18 @@ +# Copyright (c) 2026 Progmasoft. +# SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 + +# Expose the header-only C++20 layer without raising Catch2's C++14 baseline. +progmasoft_catch3_dep = declare_dependency( + dependencies: catch2_dep, + include_directories: include_directories('..'), +) + +if get_option('install') + install_headers('Catch3.hpp', subdir: 'Progmasoft') + install_headers('Catch3/Assertions.hpp', subdir: 'Progmasoft/Catch3') + install_headers('Catch3/Generator.hpp', subdir: 'Progmasoft/Catch3') + install_headers('Catch3/Property.hpp', subdir: 'Progmasoft/Catch3') + install_headers('Catch3/Random.hpp', subdir: 'Progmasoft/Catch3') + install_headers('Catch3/Generators/Boolean.hpp', subdir: 'Progmasoft/Catch3/Generators') + install_headers('Catch3/Generators/Integer.hpp', subdir: 'Progmasoft/Catch3/Generators') +endif |