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

summaryrefslogtreecommitdiffhomepage
path: root/tests
diff options
context:
space:
mode:
authorLeitwolf112026-09-24 22:07:03 +0300
committerLeitwolf112026-09-24 22:07:03 +0300
commit9ad804cedf4cc4a85ef89fe097f97a3b44654360 (patch)
treef088e919776b22d19387377bdbec727daa2429c0 /tests
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 'tests')
-rw-r--r--tests/CMakeLists.txt7
-rw-r--r--tests/Progmasoft/BUILD.bazel19
-rw-r--r--tests/Progmasoft/Property.tests.cpp233
-rw-r--r--tests/meson.build12
4 files changed, 271 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 33daefb8..d307ecd8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -154,6 +154,13 @@ include(CTest)
add_executable(SelfTest ${TEST_SOURCES} ${TEST_HEADERS})
target_include_directories(SelfTest PRIVATE ${SELF_TEST_DIR})
target_link_libraries(SelfTest PRIVATE Catch2WithMain)
+
+# Exercise the C++20 Progmasoft layer independently so the upstream Catch2
+# SelfTest continues to compile at its supported baseline language level.
+add_executable(ProgmasoftSelfTest Progmasoft/Property.tests.cpp)
+target_link_libraries(ProgmasoftSelfTest PRIVATE Progmasoft::Catch3 Catch2WithMain)
+target_compile_features(ProgmasoftSelfTest PRIVATE cxx_std_20)
+add_test(NAME Progmasoft::PropertyTests COMMAND $<TARGET_FILE:ProgmasoftSelfTest>)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(TARGET SelfTest PRE_LINK
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Catch2>
diff --git a/tests/Progmasoft/BUILD.bazel b/tests/Progmasoft/BUILD.bazel
new file mode 100644
index 00000000..c0cb14ac
--- /dev/null
+++ b/tests/Progmasoft/BUILD.bazel
@@ -0,0 +1,19 @@
+# Copyright (c) 2026 Progmasoft.
+# SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+load("@rules_cc//cc:defs.bzl", "cc_test")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_test(
+ name = "property_tests",
+ srcs = ["Property.tests.cpp"],
+ copts = select({
+ "@platforms//os:windows": ["/std:c++20"],
+ "//conditions:default": ["-std=c++20"],
+ }),
+ deps = [
+ "//:catch2",
+ "//:catch2_main",
+ "//src/Progmasoft:catch3",
+ ],
+)
diff --git a/tests/Progmasoft/Property.tests.cpp b/tests/Progmasoft/Property.tests.cpp
new file mode 100644
index 00000000..5cec3979
--- /dev/null
+++ b/tests/Progmasoft/Property.tests.cpp
@@ -0,0 +1,233 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#include <Progmasoft/Catch3.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <limits>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+TEST_CASE( "Progmasoft random sequences are stable and bounded",
+ "[progmasoft][random]" ) {
+ using Progmasoft::Catch3::Random;
+
+ Random random( 0 );
+ CHECK( random.Next() == 0xE220A8397B1DCDAFULL );
+ CHECK( random.Next() == 0x6E789E6AA1B965F4ULL );
+
+ Random bounded( 0xA55A );
+ for ( int index = 0; index < 1000; ++index ) {
+ CHECK( bounded.Below( 17 ) < 17 );
+ CHECK( bounded.Between<std::int32_t>( -31, 47 ) >= -31 );
+ CHECK( bounded.Between<std::int32_t>( -31, 47 ) <= 47 );
+ }
+
+ CHECK_THROWS_AS( bounded.Between<std::int32_t>( 2, 1 ),
+ std::invalid_argument );
+}
+
+TEST_CASE( "Progmasoft random supports full-width signed intervals",
+ "[progmasoft][random]" ) {
+ using Progmasoft::Catch3::Random;
+
+ Random random( 0x12345678 );
+ for ( int index = 0; index < 100; ++index ) {
+ const std::int64_t value =
+ random.Between( std::numeric_limits<std::int64_t>::lowest(),
+ std::numeric_limits<std::int64_t>::max() );
+ CHECK( value <= std::numeric_limits<std::int64_t>::max() );
+ CHECK( value >= std::numeric_limits<std::int64_t>::lowest() );
+ }
+
+ const std::uint64_t first = Random::DeriveSeed( 99, 0 );
+ CHECK( first == Random::DeriveSeed( 99, 0 ) );
+ CHECK( first != Random::DeriveSeed( 99, 1 ) );
+}
+
+TEST_CASE( "Integer generators preserve bounds and useful shrink order",
+ "[progmasoft][generator]" ) {
+ using Progmasoft::Catch3::Integer;
+ using Progmasoft::Catch3::Random;
+
+ const auto generator = Integer<int>( -100, 100 );
+ Random random( 0xBADC0DE );
+ for ( int index = 0; index < 100; ++index ) {
+ const int value = generator.Generate( random );
+ CHECK( value >= -100 );
+ CHECK( value <= 100 );
+ }
+
+ const std::vector<int> positiveShrinks =
+ Integer<int>( -100, 100 ).Shrink( 6 );
+ REQUIRE( !positiveShrinks.empty() );
+ CHECK( positiveShrinks.front() == 0 );
+ CHECK( std::ranges::find( positiveShrinks, 5 ) != positiveShrinks.end() );
+
+ const std::vector<int> negativeShrinks = generator.Shrink( -6 );
+ CHECK( negativeShrinks.front() == 0 );
+ CHECK( std::ranges::find( negativeShrinks, -5 ) != negativeShrinks.end() );
+
+ CHECK_THROWS_AS( Integer<int>( 5, -5 ), std::invalid_argument );
+}
+
+TEST_CASE( "Boolean generator shrinks true to false",
+ "[progmasoft][generator]" ) {
+ using Progmasoft::Catch3::Boolean;
+ using Progmasoft::Catch3::Random;
+
+ const auto generator = Boolean();
+ Random random( 0xB001 );
+ CHECK( generator.Shrink( true ) == std::vector<bool>{ false } );
+ CHECK( generator.Shrink( false ).empty() );
+ for ( int index = 0; index < 100; ++index ) {
+ (void)generator.Generate( random );
+ }
+}
+
+TEST_CASE( "Properties count trials and reproduce their seed",
+ "[progmasoft][property]" ) {
+ using Progmasoft::Catch3::CheckProperty;
+ using Progmasoft::Catch3::Integer;
+ using Progmasoft::Catch3::PropertyOptions;
+
+ int checks = 0;
+ const PropertyOptions options{
+ .Trials = 64, .Seed = 0xC0FFEE, .MaxShrinkSteps = 100 };
+ const auto property = [&]( int value ) {
+ ++checks;
+ return value >= -100 && value <= 100;
+ };
+
+ const auto first =
+ CheckProperty( Integer<int>( -100, 100 ), property, options );
+ const auto second =
+ CheckProperty( Integer<int>( -100, 100 ), property, options );
+
+ CHECK( first.Succeeded() );
+ CHECK( first.TrialsRun == options.Trials );
+ CHECK( checks == static_cast<int>( options.Trials * 2 ) );
+ CHECK( first.Describe() == second.Describe() );
+}
+
+TEST_CASE( "Property failures shrink to and report a replayable counterexample",
+ "[progmasoft][property]" ) {
+ using Progmasoft::Catch3::CheckProperty;
+ using Progmasoft::Catch3::MakeGenerator;
+ using Progmasoft::Catch3::PropertyOptions;
+
+ const auto generator =
+ MakeGenerator<int>( []( Progmasoft::Catch3::Random& ) { return -100; },
+ []( const int& value ) {
+ if ( value == -100 ) {
+ return std::vector<int>{ -50, -1, 0 };
+ }
+ if ( value == -50 ) {
+ return std::vector<int>{ -10, -1 };
+ }
+ if ( value == -10 ) {
+ return std::vector<int>{ -3, -1 };
+ }
+ if ( value == -3 ) {
+ return std::vector<int>{ -1, 0 };
+ }
+ return std::vector<int>{ 0 };
+ } );
+
+ const auto result = CheckProperty(
+ generator,
+ []( int value ) { return value >= 0; },
+ PropertyOptions{ .Trials = 4, .Seed = 0xFACE, .MaxShrinkSteps = 20 } );
+
+ CHECK_FALSE( result.Succeeded() );
+ CHECK( result.Status == Progmasoft::Catch3::PropertyStatus::kFailed );
+ REQUIRE( result.Failure.has_value() );
+ CHECK( result.Failure->Counterexample == "-1" );
+ CHECK( result.Failure->RootSeed == 0xFACE );
+ CHECK( result.Failure->TrialSeed ==
+ Progmasoft::Catch3::Random::DeriveSeed( 0xFACE, 0 ) );
+ CHECK( result.Failure->ShrinkSteps > 0 );
+ CHECK( result.Describe().find( "counterexample: -1" ) !=
+ std::string::npos );
+ CHECK( result.Describe().find( "replay seed" ) != std::string::npos );
+}
+
+TEST_CASE( "Property shrink work is bounded", "[progmasoft][property]" ) {
+ using Progmasoft::Catch3::CheckProperty;
+ using Progmasoft::Catch3::MakeGenerator;
+ using Progmasoft::Catch3::PropertyOptions;
+
+ const auto generator =
+ MakeGenerator<int>( []( Progmasoft::Catch3::Random& ) { return 100; },
+ []( const int& value ) {
+ return value > 1 ? std::vector<int>{ value - 1 }
+ : std::vector<int>{};
+ } );
+ const auto result = CheckProperty(
+ generator,
+ []( int ) { return false; },
+ PropertyOptions{ .Trials = 1, .Seed = 4, .MaxShrinkSteps = 3 } );
+
+ REQUIRE( result.Failure.has_value() );
+ CHECK( result.Failure->ShrinkSteps == 3 );
+ CHECK( result.Failure->Counterexample == "97" );
+}
+
+TEST_CASE(
+ "Property errors distinguish invalid configuration and generator failures",
+ "[progmasoft][property]" ) {
+ using Progmasoft::Catch3::CheckProperty;
+ using Progmasoft::Catch3::Generator;
+ using Progmasoft::Catch3::PropertyOptions;
+ using Progmasoft::Catch3::PropertyStatus;
+
+ const Generator<int> throwingGenerator(
+ []( Progmasoft::Catch3::Random& ) -> int {
+ throw std::runtime_error( "sample unavailable" );
+ },
+ []( const int& ) { return std::vector<int>{}; } );
+ const auto generatorError =
+ CheckProperty( throwingGenerator, []( int ) { return true; } );
+ CHECK( generatorError.Status == PropertyStatus::kErrored );
+ CHECK( generatorError.Describe().find( "sample unavailable" ) !=
+ std::string::npos );
+
+ const Generator<int> constantGenerator(
+ []( Progmasoft::Catch3::Random& ) { return 1; },
+ []( const int& ) { return std::vector<int>{}; } );
+ const auto invalidOptions = CheckProperty(
+ constantGenerator,
+ []( int ) { return true; },
+ PropertyOptions{ .Trials = 0, .Seed = 1, .MaxShrinkSteps = 1 } );
+ CHECK( invalidOptions.Status == PropertyStatus::kInvalidConfiguration );
+}
+
+TEST_CASE( "Predicate exceptions are ordinary reproducible property failures",
+ "[progmasoft][property]" ) {
+ using Progmasoft::Catch3::CheckProperty;
+ using Progmasoft::Catch3::Generator;
+ using Progmasoft::Catch3::PropertyStatus;
+
+ const Generator<int> generator(
+ []( Progmasoft::Catch3::Random& ) { return 7; },
+ []( const int& ) { return std::vector<int>{}; } );
+ const auto result = CheckProperty( generator, []( int ) -> bool {
+ throw std::runtime_error( "predicate failed" );
+ } );
+
+ CHECK( result.Status == PropertyStatus::kFailed );
+ REQUIRE( result.Failure.has_value() );
+ CHECK( result.Failure->Reason.find( "predicate failed" ) !=
+ std::string::npos );
+ CHECK( result.Failure->Counterexample == "7" );
+}
+
+TEST_CASE( "Catch3 property assertions report successful runs",
+ "[progmasoft][property]" ) {
+ CATCH3_CHECK_PROPERTY(
+ Progmasoft::Catch3::Integer<int>( 1, 100 ),
+ []( int value ) { return value > 0; },
+ Progmasoft::Catch3::PropertyOptions{
+ .Trials = 25, .Seed = 42, .MaxShrinkSteps = 10 } );
+}
diff --git a/tests/meson.build b/tests/meson.build
index 0e44eaab..1d20b9ce 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -76,3 +76,15 @@ self_test = executable(
)
test('SelfTest', self_test)
+
+# Validate Progmasoft's C++20 extension without changing Catch2's library
+# language requirement or the upstream SelfTest compilation mode.
+progmasoft_property_tests = executable(
+ 'ProgmasoftPropertyTests',
+ 'Progmasoft/Property.tests.cpp',
+ include_directories: '../src',
+ dependencies: [progmasoft_catch3_dep, catch2_with_main_dep],
+ override_options: ['cpp_std=c++20'],
+)
+
+test('Progmasoft_PropertyTests', progmasoft_property_tests)