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

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf2026-10-02 15:53:33 +0300
committerGitHub2026-10-02 15:53:33 +0300
commit00bd15ded1dd732472156aece844e7696454bd3a (patch)
tree61e94c3bd1684b4caa256bc34e380c272b67cdb6
parentfebb5b1e244174da0985b9994f8d5ed1676950e8 (diff)
parent5d55a3226360fae1eaa5ef91908b0fa7d7d07234 (diff)
downloadcatch3-00bd15ded1dd732472156aece844e7696454bd3a.tar.gz
Merge pull request #1 from Progmasoft/feature/property-combinators
Add composite property generators, single-trial replay and stable counterexample printing
-rw-r--r--docs/progmasoft-catch3.md66
-rw-r--r--docs/why-catch3.md2
-rw-r--r--premake5.lua1
-rw-r--r--src/Progmasoft/BUILD.bazel5
-rw-r--r--src/Progmasoft/Catch3.hpp4
-rw-r--r--src/Progmasoft/Catch3/Detail/Sequence.hpp97
-rw-r--r--src/Progmasoft/Catch3/Generators/Element.hpp63
-rw-r--r--src/Progmasoft/Catch3/Generators/Text.hpp93
-rw-r--r--src/Progmasoft/Catch3/Generators/Tuple.hpp83
-rw-r--r--src/Progmasoft/Catch3/Generators/Vector.hpp60
-rw-r--r--src/Progmasoft/Catch3/Property.hpp97
-rw-r--r--src/Progmasoft/meson.build5
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/Progmasoft/BUILD.bazel1
-rw-r--r--tests/Progmasoft/Generators.tests.cpp336
-rw-r--r--tests/meson.build1
-rw-r--r--xmake.lua1
17 files changed, 906 insertions, 10 deletions
diff --git a/docs/progmasoft-catch3.md b/docs/progmasoft-catch3.md
index 43a73f03..55114f32 100644
--- a/docs/progmasoft-catch3.md
+++ b/docs/progmasoft-catch3.md
@@ -33,7 +33,10 @@ The current property API is split into these headers:
bounded sampling, inclusive integer ranges, and per-trial seed derivation.
- `<Progmasoft/Catch3/Generator.hpp>`: user-defined generation/shrinking pair.
- `<Progmasoft/Catch3/Generators/Integer.hpp>` and `Boolean.hpp`: built-in
- strategies with deterministic shrink candidates.
+ scalar strategies with deterministic shrink candidates.
+- `<Progmasoft/Catch3/Generators/Vector.hpp>`, `Tuple.hpp`, `Element.hpp` and
+ `Text.hpp`: composite strategies described under
+ [Composite generators](#composite-generators).
- `<Progmasoft/Catch3/Property.hpp>`: trial execution and structured results.
- `<Progmasoft/Catch3/Assertions.hpp>`: `CATCH3_CHECK_PROPERTY`, which reports
the failing trial, replay seed, reason, and minimized counterexample through
@@ -53,10 +56,63 @@ minimal value. A false predicate or an exception from the predicate fails the
property. Exceptions from generation or shrinking are reported as execution
errors, distinct from an ordinary failing property.
-The API currently accepts one generated value per property. Tuple strategies,
-state-machine commands, parallel trials, and adaptive shrinking are deliberately
-not implied by this first module; they need their own deterministic contracts and
-tests before being added.
+A failing report ends with the line
+`replay: set PropertyOptions::ReplayTrialSeed to <seed>`. Setting that option
+runs exactly one trial with the given seed, including its shrinking, and
+ignores `Seed` and `Trials`; the reported trial number is then 1. This
+reproduces a failure from a CI log without rerunning the trials before it.
+
+```cpp
+Progmasoft::Catch3::PropertyOptions{ .ReplayTrialSeed = 1234567890123456789ULL }
+```
+
+### Composite generators
+
+A property still receives one generated value. Several arguments are passed
+as one tuple:
+
+```cpp
+using namespace Progmasoft::Catch3;
+
+CATCH3_CHECK_PROPERTY(
+ Tuple(Vector(Integer<int>(0, 100), 0, 20), Text("abc", 1, 8)),
+ [](const std::tuple<std::vector<int>, std::string> &input) {
+ return Accepts(std::get<0>(input), std::get<1>(input));
+ },
+ PropertyOptions{ .Trials = 500 });
+```
+
+- `Tuple(generators...)` draws its components strictly left to right from the
+ trial's random source and shrinks one component at a time: every candidate
+ of the first component, then every candidate of the second, and so on.
+- `Vector(element, minimumSize, maximumSize)` draws a length in the inclusive
+ range and then the elements in index order. `Text(alphabet, minimumLength,
+ maximumLength)` does the same for byte strings over the given alphabet;
+ `PrintableAscii()` is the default alphabet. Both shrink in a fixed order:
+ the shortest allowed prefix, the first and second half, the value with one
+ element removed, and finally the value with one element simplified. A
+ shrunk value is never shorter than its minimum, each step offers at most
+ 256 candidates, and a size limit above 1,048,576 elements is rejected.
+- `Element(values)` picks uniformly from a fixed non-empty list. The list
+ order is the simplicity order: a value shrinks to the values listed before
+ it. `Text` treats its alphabet the same way, so put the simplest value or
+ character first.
+
+Counterexamples of tuples, pairs and vectors are printed element by element,
+for example `{ 7, { true, false }, "text" }`. This does not depend on Catch2's
+optional tuple or range string makers, so the report is the same whatever
+configuration macros a client defines. Scalars still use `Catch::StringMaker`,
+including user specializations.
+
+Invalid factory arguments, such as an empty alphabet or a minimum above its
+maximum, throw `std::invalid_argument`; a client built without exceptions
+gets the message on standard error followed by `std::abort`.
+
+State-machine commands, parallel trials, mapped or filtered generators, and
+adaptive shrinking are deliberately not implied by this module; they need
+their own deterministic contracts and tests before being added. Shrinking a
+composite value is greedy and local like scalar shrinking: it finds a small
+counterexample, not a provably smallest one.
## Snapshots
diff --git a/docs/why-catch3.md b/docs/why-catch3.md
index 30dd5159..7fbeef44 100644
--- a/docs/why-catch3.md
+++ b/docs/why-catch3.md
@@ -25,7 +25,7 @@ Catch2 runner.
The current property runner handles one generated value per property. Shrinking
is deterministic and bounded, but greedy rather than globally minimal. There is
-not yet built-in support for state-machine properties, generated tuples,
+not yet built-in support for state-machine properties, mapped or filtered generators,
discard/classification policies, parallel execution, or a standalone Catch3
runner. Those features are not implied by the initial API.
diff --git a/premake5.lua b/premake5.lua
index 7a7d5e2a..ca773a60 100644
--- a/premake5.lua
+++ b/premake5.lua
@@ -81,6 +81,7 @@ project "ProgmasoftPropertyTests"
targetdir "build/premake/bin/%{cfg.buildcfg}"
objdir "build/premake/obj/ProgmasoftPropertyTests/%{cfg.buildcfg}"
files {
+ "tests/Progmasoft/Generators.tests.cpp",
"tests/Progmasoft/Property.tests.cpp",
"tests/Progmasoft/Results.tests.cpp",
"tests/Progmasoft/Snapshot.tests.cpp",
diff --git a/src/Progmasoft/BUILD.bazel b/src/Progmasoft/BUILD.bazel
index ed49083c..14a67bf6 100644
--- a/src/Progmasoft/BUILD.bazel
+++ b/src/Progmasoft/BUILD.bazel
@@ -15,9 +15,14 @@ cc_library(
"Catch3.hpp",
"Catch3/Assertions.hpp",
"Catch3/Detail/InvalidArgument.hpp",
+ "Catch3/Detail/Sequence.hpp",
"Catch3/Generator.hpp",
"Catch3/Generators/Boolean.hpp",
+ "Catch3/Generators/Element.hpp",
"Catch3/Generators/Integer.hpp",
+ "Catch3/Generators/Text.hpp",
+ "Catch3/Generators/Tuple.hpp",
+ "Catch3/Generators/Vector.hpp",
"Catch3/Property.hpp",
"Catch3/Random.hpp",
"Catch3/Results.hpp",
diff --git a/src/Progmasoft/Catch3.hpp b/src/Progmasoft/Catch3.hpp
index d2d34e74..5fca369a 100644
--- a/src/Progmasoft/Catch3.hpp
+++ b/src/Progmasoft/Catch3.hpp
@@ -8,7 +8,11 @@
#include <Progmasoft/Catch3/Assertions.hpp>
#include <Progmasoft/Catch3/Generator.hpp>
#include <Progmasoft/Catch3/Generators/Boolean.hpp>
+#include <Progmasoft/Catch3/Generators/Element.hpp>
#include <Progmasoft/Catch3/Generators/Integer.hpp>
+#include <Progmasoft/Catch3/Generators/Text.hpp>
+#include <Progmasoft/Catch3/Generators/Tuple.hpp>
+#include <Progmasoft/Catch3/Generators/Vector.hpp>
#include <Progmasoft/Catch3/Property.hpp>
#include <Progmasoft/Catch3/Random.hpp>
#include <Progmasoft/Catch3/Results.hpp>
diff --git a/src/Progmasoft/Catch3/Detail/Sequence.hpp b/src/Progmasoft/Catch3/Detail/Sequence.hpp
new file mode 100644
index 00000000..b1446a79
--- /dev/null
+++ b/src/Progmasoft/Catch3/Detail/Sequence.hpp
@@ -0,0 +1,97 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#pragma once
+
+#include <cstddef>
+#include <iterator>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3::Detail
+{
+
+ /// Upper bound on the candidates one sequence value offers per shrink
+ /// step. The property runner re-shrinks after every accepted candidate,
+ /// so a bounded list still reaches small counterexamples while keeping
+ /// each step linear in this constant instead of in the sequence length.
+ inline constexpr std::size_t kMaximumSequenceCandidates = 256;
+
+ /// Largest sequence a built-in generator will create. A size range above
+ /// this is rejected instead of silently allocating gigabytes in a test.
+ inline constexpr std::size_t kMaximumSequenceLength = std::size_t{ 1 }
+ << 20U;
+
+ /**
+ * @brief Ordered shrink candidates shared by Vector and Text.
+ *
+ * Candidates are produced in a fixed order, simplest first:
+ *
+ * 1. the shortest allowed prefix,
+ * 2. the first half and then the second half,
+ * 3. the value with one element removed, position by position,
+ * 4. the value with one element replaced by each of that element's own
+ * shrink candidates, position by position.
+ *
+ * No candidate is shorter than @p minimumSize, every candidate differs
+ * from @p value in length or in exactly one element, and the list is cut
+ * at kMaximumSequenceCandidates. The same candidate may appear twice,
+ * for example when removing the last element equals the shortest prefix;
+ * the runner simply tests it again.
+ */
+ template<typename Sequence, typename ShrinkElement>
+ [[nodiscard]] std::vector<Sequence>
+ ShrinkSequence(const Sequence &value,
+ std::size_t minimumSize,
+ ShrinkElement &&shrinkElement)
+ {
+ using Element = typename Sequence::value_type;
+ using Difference = typename std::iterator_traits<
+ typename Sequence::const_iterator>::difference_type;
+
+ std::vector<Sequence> candidates;
+ const std::size_t size = value.size();
+ const auto full = [&candidates] {
+ return candidates.size() >= kMaximumSequenceCandidates;
+ };
+ const auto at = [&value](std::size_t index) {
+ return value.begin() + static_cast<Difference>(index);
+ };
+
+ if (size > minimumSize)
+ {
+ candidates.push_back(Sequence(at(0), at(minimumSize)));
+
+ const std::size_t kept = size - size / 2;
+ if (kept > minimumSize && kept < size)
+ {
+ candidates.push_back(Sequence(at(0), at(kept)));
+ candidates.push_back(Sequence(at(size - kept), at(size)));
+ }
+
+ for (std::size_t index = 0; index < size && !full(); ++index)
+ {
+ Sequence shorter(at(0), at(index));
+ shorter.insert(shorter.end(), at(index + 1), at(size));
+ candidates.push_back(std::move(shorter));
+ }
+ }
+
+ for (std::size_t index = 0; index < size && !full(); ++index)
+ {
+ const Element current = static_cast<Element>(*at(index));
+ for (const auto &simpler : shrinkElement(current))
+ {
+ if (full())
+ {
+ break;
+ }
+ Sequence replaced = value;
+ *(replaced.begin() + static_cast<Difference>(index))
+ = static_cast<Element>(simpler);
+ candidates.push_back(std::move(replaced));
+ }
+ }
+ return candidates;
+ }
+
+} // namespace Progmasoft::Catch3::Detail
diff --git a/src/Progmasoft/Catch3/Generators/Element.hpp b/src/Progmasoft/Catch3/Generators/Element.hpp
new file mode 100644
index 00000000..087b6355
--- /dev/null
+++ b/src/Progmasoft/Catch3/Generators/Element.hpp
@@ -0,0 +1,63 @@
+// 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 <cstddef>
+#include <initializer_list>
+#include <iterator>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3
+{
+
+ /// Picks uniformly from a fixed, non-empty list of values.
+ ///
+ /// The list order is the simplicity order: a value shrinks to the values
+ /// listed before it, earliest first, so place the simplest case first.
+ /// A value that is not in the list has no candidates.
+ template<typename ValueType>
+ requires std::equality_comparable<ValueType>
+ [[nodiscard]] Generator<ValueType>
+ Element(std::vector<ValueType> values)
+ {
+ if (values.empty())
+ {
+ Detail::RejectInvalidArgument(
+ "Element requires at least one value");
+ }
+
+ // Both callbacks share one immutable copy of the list.
+ const auto shared
+ = std::make_shared<const std::vector<ValueType>>(std::move(values));
+ return MakeGenerator<ValueType>(
+ [shared](Random &random) {
+ const std::size_t index
+ = random.Between(std::size_t{ 0 }, shared->size() - 1);
+ return static_cast<ValueType>((*shared)[index]);
+ },
+ [shared](const ValueType &value) {
+ const auto found
+ = std::find(shared->begin(), shared->end(), value);
+ if (found == shared->end())
+ {
+ return std::vector<ValueType>{};
+ }
+ return std::vector<ValueType>(shared->begin(), found);
+ });
+ }
+
+ /// Convenience overload for a literal list of values.
+ template<typename ValueType>
+ requires std::equality_comparable<ValueType>
+ [[nodiscard]] Generator<ValueType>
+ Element(std::initializer_list<ValueType> values)
+ {
+ return Element(std::vector<ValueType>(values));
+ }
+
+} // namespace Progmasoft::Catch3
diff --git a/src/Progmasoft/Catch3/Generators/Text.hpp b/src/Progmasoft/Catch3/Generators/Text.hpp
new file mode 100644
index 00000000..05df8c00
--- /dev/null
+++ b/src/Progmasoft/Catch3/Generators/Text.hpp
@@ -0,0 +1,93 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#pragma once
+
+#include <Progmasoft/Catch3/Detail/Sequence.hpp>
+#include <Progmasoft/Catch3/Generator.hpp>
+#include <cstddef>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3
+{
+
+ /// The 95 printable ASCII characters, space through tilde, with the
+ /// lower-case letter `a` first so that it is the simplest character.
+ [[nodiscard]] inline std::string
+ PrintableAscii()
+ {
+ std::string alphabet = "a";
+ for (char character = ' '; character <= '~'; ++character)
+ {
+ if (character != 'a')
+ {
+ alphabet.push_back(character);
+ }
+ }
+ return alphabet;
+ }
+
+ /// Generates byte strings whose characters are drawn from @p alphabet
+ /// and whose length lies in the inclusive range.
+ ///
+ /// The alphabet order is the simplicity order: shrinking first shortens
+ /// the string and then replaces single characters with earlier alphabet
+ /// characters, earliest first. Characters are bytes; the generator does
+ /// not interpret an encoding, so supply an alphabet that is valid for
+ /// the code under test.
+ [[nodiscard]] inline Generator<std::string>
+ Text(std::string alphabet = PrintableAscii(),
+ std::size_t minimumLength = 0,
+ std::size_t maximumLength = 32)
+ {
+ if (alphabet.empty())
+ {
+ Detail::RejectInvalidArgument(
+ "Text requires a non-empty alphabet");
+ }
+ if (minimumLength > maximumLength)
+ {
+ Detail::RejectInvalidArgument(
+ "Text requires minimumLength <= maximumLength");
+ }
+ if (maximumLength > Detail::kMaximumSequenceLength)
+ {
+ Detail::RejectInvalidArgument(
+ "Text maximumLength exceeds the supported sequence length");
+ }
+
+ const auto shared
+ = std::make_shared<const std::string>(std::move(alphabet));
+ return MakeGenerator<std::string>(
+ [shared, minimumLength, maximumLength](Random &random) {
+ const std::size_t length
+ = random.Between(minimumLength, maximumLength);
+ std::string text;
+ text.reserve(length);
+ for (std::size_t index = 0; index < length; ++index)
+ {
+ text.push_back((*shared)[random.Between(
+ std::size_t{ 0 },
+ shared->size() - 1)]);
+ }
+ return text;
+ },
+ [shared, minimumLength](const std::string &value) {
+ return Detail::ShrinkSequence(
+ value,
+ minimumLength,
+ [&shared](char character) {
+ const std::size_t position = shared->find(character);
+ if (position == std::string::npos)
+ {
+ return std::string{};
+ }
+ return shared->substr(0, position);
+ });
+ });
+ }
+
+} // namespace Progmasoft::Catch3
diff --git a/src/Progmasoft/Catch3/Generators/Tuple.hpp b/src/Progmasoft/Catch3/Generators/Tuple.hpp
new file mode 100644
index 00000000..df58da05
--- /dev/null
+++ b/src/Progmasoft/Catch3/Generators/Tuple.hpp
@@ -0,0 +1,83 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#pragma once
+
+#include <Progmasoft/Catch3/Generator.hpp>
+#include <cstddef>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3
+{
+ namespace Detail
+ {
+
+ template<std::size_t Index, typename... ValueTypes>
+ void
+ AppendComponentCandidates(
+ const std::tuple<Generator<ValueTypes>...> &generators,
+ const std::tuple<ValueTypes...> &value,
+ std::vector<std::tuple<ValueTypes...>> &candidates)
+ {
+ for (const auto &simpler :
+ std::get<Index>(generators).Shrink(std::get<Index>(value)))
+ {
+ std::tuple<ValueTypes...> replaced = value;
+ std::get<Index>(replaced) = simpler;
+ candidates.push_back(std::move(replaced));
+ }
+ }
+
+ template<typename... ValueTypes, std::size_t... Indices>
+ [[nodiscard]] std::vector<std::tuple<ValueTypes...>>
+ ShrinkTuple(const std::tuple<Generator<ValueTypes>...> &generators,
+ const std::tuple<ValueTypes...> &value,
+ std::index_sequence<Indices...>)
+ {
+ std::vector<std::tuple<ValueTypes...>> candidates;
+ // The comma fold visits components strictly left to right.
+ (AppendComponentCandidates<Indices>(generators, value, candidates),
+ ...);
+ return candidates;
+ }
+
+ } // namespace Detail
+
+ /// Combines independent generators into one generator of tuples, which
+ /// is how a property receives several generated arguments.
+ ///
+ /// Components are drawn strictly left to right from the trial's random
+ /// source, so a tuple is reproducible from its replay seed. Shrinking
+ /// changes one component at a time: all candidates of the first
+ /// component, then all of the second, and so on. Combined with the
+ /// runner's greedy restart this minimizes every component without
+ /// trying the cross product of candidates.
+ template<typename... ValueTypes>
+ requires(sizeof...(ValueTypes) > 0)
+ [[nodiscard]] Generator<std::tuple<ValueTypes...>>
+ Tuple(Generator<ValueTypes>... generators)
+ {
+ using Value = std::tuple<ValueTypes...>;
+ const std::tuple<Generator<ValueTypes>...> components{ std::move(
+ generators)... };
+
+ return MakeGenerator<Value>(
+ [components](Random &random) {
+ return std::apply(
+ [&random](const Generator<ValueTypes> &...component) {
+ // Braced initialization fixes the evaluation order;
+ // a function-call argument list would not.
+ return Value{ component.Generate(random)... };
+ },
+ components);
+ },
+ [components](const Value &value) {
+ return Detail::ShrinkTuple(
+ components,
+ value,
+ std::index_sequence_for<ValueTypes...>{});
+ });
+ }
+
+} // namespace Progmasoft::Catch3
diff --git a/src/Progmasoft/Catch3/Generators/Vector.hpp b/src/Progmasoft/Catch3/Generators/Vector.hpp
new file mode 100644
index 00000000..65c81e3b
--- /dev/null
+++ b/src/Progmasoft/Catch3/Generators/Vector.hpp
@@ -0,0 +1,60 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#pragma once
+
+#include <Progmasoft/Catch3/Detail/Sequence.hpp>
+#include <Progmasoft/Catch3/Generator.hpp>
+#include <cstddef>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3
+{
+
+ /// Generates vectors whose length lies in the inclusive size range and
+ /// whose elements come from @p element, drawn in index order.
+ ///
+ /// Shrinking first shortens the vector, then simplifies single elements
+ /// with the element generator's own candidates; see
+ /// Detail::ShrinkSequence for the exact order. A shrunk vector never
+ /// becomes shorter than @p minimumSize, so a property may rely on it.
+ template<typename ValueType>
+ [[nodiscard]] Generator<std::vector<ValueType>>
+ Vector(Generator<ValueType> element,
+ std::size_t minimumSize = 0,
+ std::size_t maximumSize = 32)
+ {
+ if (minimumSize > maximumSize)
+ {
+ Detail::RejectInvalidArgument(
+ "Vector requires minimumSize <= maximumSize");
+ }
+ if (maximumSize > Detail::kMaximumSequenceLength)
+ {
+ Detail::RejectInvalidArgument(
+ "Vector maximumSize exceeds the supported sequence length");
+ }
+
+ return MakeGenerator<std::vector<ValueType>>(
+ [element, minimumSize, maximumSize](Random &random) {
+ const std::size_t size
+ = random.Between(minimumSize, maximumSize);
+ std::vector<ValueType> values;
+ values.reserve(size);
+ for (std::size_t index = 0; index < size; ++index)
+ {
+ values.push_back(element.Generate(random));
+ }
+ return values;
+ },
+ [element, minimumSize](const std::vector<ValueType> &value) {
+ return Detail::ShrinkSequence(
+ value,
+ minimumSize,
+ [&element](const ValueType &item) {
+ return element.Shrink(item);
+ });
+ });
+ }
+
+} // namespace Progmasoft::Catch3
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;
diff --git a/src/Progmasoft/meson.build b/src/Progmasoft/meson.build
index f9acedb2..1f9afbda 100644
--- a/src/Progmasoft/meson.build
+++ b/src/Progmasoft/meson.build
@@ -21,6 +21,7 @@ if get_option('install')
install_headers('Catch3.hpp', subdir: 'Progmasoft')
install_headers('Catch3/Assertions.hpp', subdir: 'Progmasoft/Catch3')
install_headers('Catch3/Detail/InvalidArgument.hpp', subdir: 'Progmasoft/Catch3/Detail')
+ install_headers('Catch3/Detail/Sequence.hpp', subdir: 'Progmasoft/Catch3/Detail')
install_headers('Catch3/Generator.hpp', subdir: 'Progmasoft/Catch3')
install_headers('Catch3/Property.hpp', subdir: 'Progmasoft/Catch3')
install_headers('Catch3/Random.hpp', subdir: 'Progmasoft/Catch3')
@@ -29,4 +30,8 @@ if get_option('install')
install_headers('Catch3/XmlWriter.hpp', subdir: 'Progmasoft/Catch3')
<