Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/Property.hpp
diff options
context:
space:
mode:
authorLeitwolf112026-09-24 22:07:03 +0300
committerLeitwolf112026-09-24 22:07:03 +0300
commit9ad804cedf4cc4a85ef89fe097f97a3b44654360 (patch)
treef088e919776b22d19387377bdbec727daa2429c0 /src/Progmasoft/Catch3/Property.hpp
parent02f8b5593a15cdec8558256e6f40a0c7fb5862d6 (diff)
downloadcatch3-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/Progmasoft/Catch3/Property.hpp')
-rw-r--r--src/Progmasoft/Catch3/Property.hpp244
1 files changed, 244 insertions, 0 deletions
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