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

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/Generators
diff options
context:
space:
mode:
authorLeitwolf2026-10-02 15:53:33 +0300
committerGitHub2026-10-02 15:53:33 +0300
commit00bd15ded1dd732472156aece844e7696454bd3a (patch)
tree61e94c3bd1684b4caa256bc34e380c272b67cdb6 /src/Progmasoft/Catch3/Generators
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
Diffstat (limited to 'src/Progmasoft/Catch3/Generators')
-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
4 files changed, 299 insertions, 0 deletions
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