diff options
| author | Leitwolf | 2026-10-02 15:53:33 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-02 15:53:33 +0300 |
| commit | 00bd15ded1dd732472156aece844e7696454bd3a (patch) | |
| tree | 61e94c3bd1684b4caa256bc34e380c272b67cdb6 /src/Progmasoft/Catch3/Generators | |
| parent | febb5b1e244174da0985b9994f8d5ed1676950e8 (diff) | |
| parent | 5d55a3226360fae1eaa5ef91908b0fa7d7d07234 (diff) | |
| download | catch3-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.hpp | 63 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generators/Text.hpp | 93 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generators/Tuple.hpp | 83 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Generators/Vector.hpp | 60 |
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 |