diff options
| author | Leitwolf11 | 2026-10-02 03:27:05 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-02 03:27:05 +0300 |
| commit | 5d55a3226360fae1eaa5ef91908b0fa7d7d07234 (patch) | |
| tree | 61e94c3bd1684b4caa256bc34e380c272b67cdb6 /src/Progmasoft/Catch3/Property.hpp | |
| parent | febb5b1e244174da0985b9994f8d5ed1676950e8 (diff) | |
| download | catch3-feature/property-combinators.tar.gz | |
Add composite property generators, single-trial replay and stable counterexample printingfeature/property-combinators
Property tests could only generate one integer or Boolean. This adds the
composite strategies the documentation listed as missing, without changing
any Catch2 file or the existing Progmasoft API.
Generators:
- Tuple(generators...) draws components left to right and shrinks one
component at a time, which is how a property receives several arguments.
- Vector(element, minimumSize, maximumSize) and Text(alphabet,
minimumLength, maximumLength) share one ordered shrink: shortest allowed
prefix, the two halves, single removals, then single element
simplifications. A shrunk value never falls below its minimum, each step
offers at most 256 candidates, and a size limit above 1,048,576 is
rejected.
- Element(values) picks from a fixed list and shrinks toward earlier
entries.
Runner:
- PropertyOptions::ReplayTrialSeed runs exactly one trial with a reported
replay seed, including its shrinking, so a failure from a log can be
reproduced without rerunning earlier trials. The report now ends with the
line that names this option and the seed.
- Counterexamples of tuples, pairs and vectors are printed element by
element instead of relying on Catch2's optional string makers, so the
report does not depend on configuration macros or include order.
The headers still compile for clients built without language exceptions.
Thirteen new test cases cover bounds, determinism, shrink order, candidate
limits, minimized counterexamples, replay and invalid configurations, and
every build description (Bazel, CMake, Meson, Xmake, Premake) lists the new
headers and test file.
Diffstat (limited to 'src/Progmasoft/Catch3/Property.hpp')
| -rw-r--r-- | src/Progmasoft/Catch3/Property.hpp | 97 |
1 files changed, 93 insertions, 4 deletions
diff --git a/src/Progmasoft/Catch3/Property.hpp b/src/Progmasoft/Catch3/Property.hpp index 1a853bf1..57f7ac7b 100644 --- a/src/Progmasoft/Catch3/Property.hpp +++ b/src/Progmasoft/Catch3/Property.hpp @@ -12,8 +12,10 @@ #include <optional> #include <sstream> #include <string> +#include <tuple> #include <type_traits> #include <utility> +#include <vector> namespace Progmasoft::Catch3 { @@ -24,6 +26,11 @@ namespace Progmasoft::Catch3 std::size_t Trials = 100; std::uint64_t Seed = 0xC47C4A53ULL; std::size_t MaxShrinkSteps = 1000; + /// When set, runs exactly one trial with this seed instead of + /// deriving Trials seeds from Seed. Use the "replay seed" of a + /// reported failure to reproduce that single trial, including its + /// shrinking, without rerunning the trials before it. + std::optional<std::uint64_t> ReplayTrialSeed = std::nullopt; }; enum class PropertyStatus : std::uint8_t @@ -88,7 +95,9 @@ namespace Progmasoft::Catch3 << Failure->TrialSeed << ", shrink checks " << Failure->ShrinkSteps << "\ncounterexample: " << Failure->Counterexample - << "\nreason: " << Failure->Reason; + << "\nreason: " << Failure->Reason + << "\nreplay: set PropertyOptions::ReplayTrialSeed to " + << Failure->TrialSeed; } return output.str(); } @@ -99,9 +108,85 @@ namespace Progmasoft::Catch3 template<typename ValueType> [[nodiscard]] std::string + StringifyPropertyValue(const ValueType &value); + + /// Renders one counterexample. Scalars use Catch2's StringMaker, so + /// user specializations apply. Tuples, pairs and vectors are + /// rendered here, element by element, because Catch2 only prints + /// tuples when a configuration macro is defined before its headers + /// and a counterexample must not degrade to "{?}" depending on + /// include order. + template<typename ValueType> + struct PropertyValuePrinter final + { + [[nodiscard]] static std::string + Print(const ValueType &value) + { + return Catch::StringMaker<ValueType>::convert(value); + } + }; + + template<typename... ValueTypes> + struct PropertyValuePrinter<std::tuple<ValueTypes...>> final + { + [[nodiscard]] static std::string + Print(const std::tuple<ValueTypes...> &value) + { + std::string text = "{"; + bool first = true; + std::apply( + [&text, &first](const ValueTypes &...component) { + const auto append = [&text, &first](std::string item) { + text += first ? " " : ", "; + text += item; + first = false; + }; + (append(StringifyPropertyValue(component)), ...); + }, + value); + text += first ? "}" : " }"; + return text; + } + }; + + template<typename FirstType, typename SecondType> + struct PropertyValuePrinter<std::pair<FirstType, SecondType>> final + { + [[nodiscard]] static std::string + Print(const std::pair<FirstType, SecondType> &value) + { + return "{ " + StringifyPropertyValue(value.first) + ", " + + StringifyPropertyValue(value.second) + " }"; + } + }; + + template<typename ElementType, typename AllocatorType> + struct PropertyValuePrinter<std::vector<ElementType, AllocatorType>> + final + { + [[nodiscard]] static std::string + Print(const std::vector<ElementType, AllocatorType> &value) + { + std::string text = "{"; + bool first = true; + for (const auto &element : value) + { + text += first ? " " : ", "; + // The cast turns std::vector<bool>'s proxy into a value. + text += StringifyPropertyValue( + static_cast<ElementType>(element)); + first = false; + } + text += first ? "}" : " }"; + return text; + } + }; + + template<typename ValueType> + [[nodiscard]] std::string StringifyPropertyValue(const ValueType &value) { - return Catch::StringMaker<std::remove_cvref_t<ValueType>>::convert( + return PropertyValuePrinter<std::remove_cvref_t<ValueType>>::Print( value); } @@ -178,11 +263,15 @@ namespace Progmasoft::Catch3 } PropertyResult result; - for (std::size_t trial = 0; trial < options.Trials; ++trial) + const std::size_t trials + = options.ReplayTrialSeed.has_value() ? 1 : options.Trials; + for (std::size_t trial = 0; trial < trials; ++trial) { result.TrialsRun = trial + 1; const std::uint64_t trialSeed - = Random::DeriveSeed(options.Seed, trial); + = options.ReplayTrialSeed.has_value() + ? *options.ReplayTrialSeed + : Random::DeriveSeed(options.Seed, trial); Random random(trialSeed); std::optional<ValueType> sample; |