Hegel 0.3.9
Property-based testing for C++
Loading...
Searching...
No Matches
combinators.h
1#pragma once
2
3#include <variant>
4
5#include "hegel/core.h"
6#include "hegel/generators/numeric.h"
7#include "hegel/generators/primitives.h"
8
9namespace hegel::generators {
10
12 // Concrete IGenerator for sampled_from(). Schema asks the server for an
13 // integer index into the captured `elements_` vector; the client parser
14 // does the lookup.
15 template <typename T> class SampledFromGenerator : public IGenerator<T> {
16 public:
17 explicit SampledFromGenerator(std::vector<T> elements)
18 : elements_(std::move(elements)) {
19 if (elements_.empty()) {
20 throw std::invalid_argument(
21 "sampled_from requires a non-empty vector");
22 }
23 }
24
25 std::optional<BasicGenerator<T>> as_basic() const override {
26 hegel::internal::json::json schema = {
27 {"type", "integer"},
28 {"min_value", 0},
29 {"max_value", static_cast<int64_t>(elements_.size() - 1)}};
30 auto elements = elements_;
31 return BasicGenerator<T>{
32 std::move(schema),
33 [elements = std::move(elements)](
34 const hegel::internal::json::json_raw_ref& raw) {
35 return elements[static_cast<size_t>(raw.get_int64_t())];
36 }};
37 }
38
39 private:
40 std::vector<T> elements_;
41 };
42
43 // Concrete IGenerator for one_of(). Schema path requires every branch
44 // to be basic; fallback draws an index and delegates to that branch.
45 template <typename T> class OneOfGenerator : public IGenerator<T> {
46 public:
47 explicit OneOfGenerator(std::vector<Generator<T>> gens)
48 : gens_(std::move(gens)) {
49 if (gens_.empty()) {
50 throw std::invalid_argument(
51 "one_of requires a non-empty vector of generators");
52 }
53 }
54
55 std::optional<BasicGenerator<T>> as_basic() const override {
56 std::vector<BasicGenerator<T>> basics;
57 basics.reserve(gens_.size());
58 for (const auto& gen : gens_) {
59 auto b = gen.as_basic();
60 if (!b)
61 return std::nullopt;
62 basics.push_back(std::move(*b));
63 }
64
65 // The protocol guarantees `one_of` responses arrive as
66 // `[index, value]`, so the schema is just the raw children
67 // without any per-branch tagging. The index tells us which
68 // branch's parser (which carries any per-branch transforms
69 // composed in via map()) to apply to the value.
70 hegel::internal::json::json children =
71 hegel::internal::json::json::array();
72 for (const auto& b : basics) {
73 children.push_back(b.schema);
74 }
75 hegel::internal::json::json schema = {{"type", "one_of"},
76 {"generators", children}};
77
78 return BasicGenerator<T>{
79 std::move(schema),
80 [basics = std::move(basics)](
81 const hegel::internal::json::json_raw_ref& raw) -> T {
82 size_t idx = static_cast<size_t>(raw[0].get_int64_t());
83 return basics[idx].parse_raw(raw[1]);
84 }};
85 }
86
87 T do_draw(const TestCase& tc) const override {
88 if (auto basic = as_basic()) {
89 return basic->do_draw(tc);
90 }
91 auto idx = integers<size_t>(
92 {.min_value = 0, .max_value = gens_.size() - 1})
93 .do_draw(tc);
94 return gens_[idx].do_draw(tc);
95 }
96
97 private:
98 std::vector<Generator<T>> gens_;
99 };
101
104
117 template <typename T>
118 Generator<T> sampled_from(const std::vector<T>& elements) {
119 return Generator<T>(new SampledFromGenerator<T>(elements));
120 }
121
128 template <typename T>
129 Generator<T> sampled_from(std::initializer_list<T> elements) {
130 return sampled_from(std::vector<T>(elements));
131 }
132
138 inline Generator<std::string>
139 sampled_from(std::initializer_list<const char*> elements) {
140 std::vector<std::string> strings;
141 strings.reserve(elements.size());
142 for (const char* s : elements) {
143 strings.push_back(s);
144 }
145 return sampled_from(strings);
146 }
147
149
152
169 template <typename T> Generator<T> one_of(std::vector<Generator<T>> gens) {
170 return Generator<T>(new OneOfGenerator<T>(std::move(gens)));
171 }
172
179 template <typename T>
180 Generator<T> one_of(std::initializer_list<Generator<T>> gens) {
181 return one_of(std::vector<Generator<T>>(gens));
182 }
183
185 namespace detail {
186
187 template <typename Variant, typename GenTuple, size_t I = 0>
188 Variant draw_variant_impl(const GenTuple& gens, size_t idx,
189 const TestCase& tc) {
190 if constexpr (I < std::tuple_size_v<GenTuple>) {
191 if (idx == I) {
192 return std::get<I>(gens).do_draw(tc);
193 }
194 return draw_variant_impl<Variant, GenTuple, I + 1>(gens, idx,
195 tc);
196 } else {
197 return Variant{};
198 }
199 }
200
201 template <typename Variant, typename Parsers, size_t I = 0>
202 Variant
203 parse_variant_impl(const Parsers& parsers, size_t idx,
204 const hegel::internal::json::json_raw_ref& raw) {
205 if constexpr (I < std::tuple_size_v<Parsers>) {
206 if (idx == I) {
207 return Variant{std::in_place_index<I>,
208 std::get<I>(parsers)(raw)};
209 }
210 return parse_variant_impl<Variant, Parsers, I + 1>(parsers, idx,
211 raw);
212 } else {
213 return Variant{};
214 }
215 }
216
217 } // namespace detail
218
219 // Concrete IGenerator for variant(). Schema path requires every branch
220 // to be basic; uses the same one_of protocol as OneOfGenerator, but
221 // branches can have heterogeneous types so each branch has its own
222 // typed parser.
223 template <typename... Ts>
224 class VariantGenerator : public IGenerator<std::variant<Ts...>> {
225 public:
226 using Result = std::variant<Ts...>;
227
228 explicit VariantGenerator(Generator<Ts>... gens)
229 : gens_(std::move(gens)...) {}
230
231 std::optional<BasicGenerator<Result>> as_basic() const override {
232 auto basics = std::apply(
233 [](const auto&... g) {
234 return std::make_tuple(g.as_basic()...);
235 },
236 gens_);
237 bool all_basic = std::apply(
238 [](const auto&... b) { return (b.has_value() && ...); },
239 basics);
240 if (!all_basic)
241 return std::nullopt;
242
243 // Server returns `[index, value]` for `one_of` schemas, so we
244 // can emit the children directly without per-branch tagging.
245 hegel::internal::json::json children =
246 hegel::internal::json::json::array();
247 std::apply(
248 [&children](const auto&... b) {
249 (children.push_back(b->schema), ...);
250 },
251 basics);
252
253 hegel::internal::json::json schema = {{"type", "one_of"},
254 {"generators", children}};
255
256 auto parsers = std::apply(
257 [](const auto&... b) { return std::make_tuple(b->parse...); },
258 basics);
259
260 return BasicGenerator<Result>{
261 std::move(schema),
262 [parsers = std::move(parsers)](
263 const hegel::internal::json::json_raw_ref& raw) -> Result {
264 size_t idx = static_cast<size_t>(raw[0].get_int64_t());
265 return detail::parse_variant_impl<Result,
266 decltype(parsers)>(
267 parsers, idx, raw[1]);
268 }};
269 }
270
271 Result do_draw(const TestCase& tc) const override {
272 if (auto basic = as_basic()) {
273 return basic->do_draw(tc);
274 }
275 constexpr size_t N = sizeof...(Ts);
276 auto index_gen =
277 integers<size_t>({.min_value = 0, .max_value = N - 1});
278 size_t idx = index_gen.do_draw(tc);
279 return detail::draw_variant_impl<Result, decltype(gens_)>(gens_,
280 idx, tc);
281 }
282
283 private:
284 std::tuple<Generator<Ts>...> gens_;
285 };
286
287 // Concrete IGenerator for optional(). Schema path wraps the inner's
288 // schema in a one_of with a null branch; fallback uses booleans() to
289 // gate presence.
290 template <typename T>
291 class OptionalGenerator : public IGenerator<std::optional<T>> {
292 public:
293 explicit OptionalGenerator(Generator<T> gen) : gen_(std::move(gen)) {}
294
295 std::optional<BasicGenerator<std::optional<T>>>
296 as_basic() const override {
297 auto basic = gen_.as_basic();
298 if (!basic)
299 return std::nullopt;
300
301 hegel::internal::json::json generators =
302 hegel::internal::json::json::array();
303 generators.push_back(hegel::internal::json::json{
304 {"type", "constant"}, {"value", nullptr}});
305 generators.push_back(basic->schema);
306 hegel::internal::json::json schema = {{"type", "one_of"},
307 {"generators", generators}};
308
309 auto parse = basic->parse;
310 return BasicGenerator<std::optional<T>>{
311 std::move(schema),
312 [parse = std::move(parse)](
313 const hegel::internal::json::json_raw_ref& raw)
314 -> std::optional<T> {
315 // `one_of` responses arrive as `[index, value]`. Index
316 // 0 is the null branch, index 1 is the inner value.
317 size_t idx = static_cast<size_t>(raw[0].get_int64_t());
318 if (idx == 0) {
319 return std::nullopt;
320 }
321 return parse(raw[1]);
322 }};
323 }
324
325 std::optional<T> do_draw(const TestCase& tc) const override {
326 if (auto basic = as_basic()) {
327 return basic->do_draw(tc);
328 }
329 bool is_none = booleans().do_draw(tc);
330 if (is_none) {
331 return std::nullopt;
332 }
333 return gen_.do_draw(tc);
334 }
335
336 private:
337 Generator<T> gen_;
338 };
340
355 template <typename... Ts>
356 Generator<std::variant<Ts...>> variant(Generator<Ts>... gens) {
357 return Generator<std::variant<Ts...>>(
358 new VariantGenerator<Ts...>(std::move(gens)...));
359 }
360
376 template <typename T>
379 new OptionalGenerator<T>(std::move(gen)));
380 }
381
383
384} // namespace hegel::generators
Handle to the currently-executing test case.
Definition test_case.h:34
The base class of all generators.
Definition core.h:157
Hegel generators.
Definition core.h:16
Generator< T > sampled_from(const std::vector< T > &elements)
Sample from a fixed set of values.
Definition combinators.h:118
Generator< bool > booleans()
Generate random boolean values.
Generator< std::variant< Ts... > > variant(Generator< Ts >... gens)
Generate std::variant from heterogeneous generators.
Definition combinators.h:356
Generator< std::optional< T > > optional(Generator< T > gen)
Generate optional values (present or absent).
Definition combinators.h:377
Generator< T > one_of(std::vector< Generator< T > > gens)
Choose from multiple generators of the same type.
Definition combinators.h:169
*‍/
Definition core.h:94