Hegel 0.3.5
Property-based testing for C++
Loading...
Searching...
No Matches
core.h
1#pragma once
2
3#include <functional>
4#include <memory>
5#include <optional>
6#include <stdexcept>
7#include <type_traits>
8
9#include "internal.h"
10#include "nlohmann_reader.h"
11#include "test_case.h"
12
17
18 template <typename T> struct BasicGenerator;
19 template <typename T> class CompositeGenerator;
20 template <typename T, typename U> class MappedGenerator;
21
23 // Default client-side parser used by schema-backed generators whose parse
24 // step is determined solely by T. Primitives use typed accessors on the
25 // raw json_raw_ref; reflectable composite types fall back to reflect-cpp.
26 template <typename T>
27 T default_parse_raw(const hegel::internal::json::json_raw_ref& result) {
28 if constexpr (std::is_same_v<T, std::string>) {
29 return result.get_string();
30 } else if constexpr (std::is_same_v<std::remove_cvref_t<T>, bool>) {
31 return result.get_bool();
32 } else if constexpr (std::is_floating_point_v<T>) {
33 return static_cast<T>(result.get_double());
34 } else if constexpr (std::is_unsigned_v<T>) {
35 return static_cast<T>(result.get_uint64_t());
36 } else if constexpr (std::is_integral_v<T>) {
37 return static_cast<T>(result.get_int64_t());
38 } else {
39 auto parse_result = internal::read_nlohmann<T>(result);
40 if (!parse_result.has_value()) {
41 throw std::runtime_error(
42 "Failed to parse server response into requested type");
43 }
44 return parse_result.value();
45 }
46 }
48
50 // Schema + client-side parser bundle. Every schema-backed generator
51 // exposes one of these via IGenerator<T>::as_basic().
52 template <typename T> struct BasicGenerator {
53 using Parse =
54 std::function<T(const hegel::internal::json::json_raw_ref&)>;
55
56 hegel::internal::json::json schema;
57 Parse parse;
58
59 T parse_raw(const hegel::internal::json::json_raw_ref& raw) const {
60 return parse(raw);
61 }
62
63 T do_draw(const TestCase& tc) const {
64 hegel::internal::json::json response =
65 internal::communicate_with_core(schema, tc);
66 if (!response.contains("result")) {
67 throw std::runtime_error(
68 "Server response missing 'result' field");
69 }
70 return parse(response["result"]);
71 }
72
73 template <typename F, typename U = std::invoke_result_t<F, T>>
74 BasicGenerator<U> map(F f) const {
75 Parse old_parse = parse;
76 hegel::internal::json::json sch = schema;
77 return BasicGenerator<U>{
78 std::move(sch),
79 [old_parse = std::move(old_parse), f = std::move(f)](
80 const hegel::internal::json::json_raw_ref& raw) -> U {
81 return f(old_parse(raw));
82 }};
83 }
84 };
86
94 template <typename T> struct IGenerator {
95 IGenerator() {}
96 virtual ~IGenerator() = default;
97
99 // Returns a BasicGenerator<T> (schema + client-side parser) if this
100 // generator can be driven through the Hegel protocol as a single
101 // schema request. Composed generators (vectors, one_of, ...) use this
102 // to build compound schemas while keeping per-element parsing
103 // client-side; map() uses it to preserve schemas through
104 // transformations. Defaults to nullopt for generators whose
105 // production is fully client-side (filter, flat_map, user closures).
106 virtual std::optional<BasicGenerator<T>> as_basic() const {
107 return std::nullopt;
108 }
109
110 // Get the CBOR schema for this generator, if any. The default
111 // derives it from as_basic(); override only if you need to report a
112 // schema without also providing a parser.
113 virtual std::optional<hegel::internal::json::json> schema() const {
114 auto b = as_basic();
115 return b ? std::optional{b->schema} : std::nullopt;
116 }
117
118 // Produce a value. The default delegates to the basic form when
119 // available; generators without a basic form
120 // must override this to provide a client-side fallback.
121 virtual T do_draw(const TestCase& tc) const {
122 if (auto b = as_basic())
123 return b->do_draw(tc);
124 throw std::logic_error(
125 "IGenerator has no basic form and no do_draw override");
126 }
128 };
129
157 template <typename T> class Generator : IGenerator<T> {
158 public:
160 Generator(IGenerator<T>* p) : IGenerator<T>(), inner_(p) {}
161 Generator(std::shared_ptr<IGenerator<T>> p)
162 : IGenerator<T>(), inner_(std::move(p)) {}
163
164 T do_draw(const TestCase& tc) const override {
165 return inner_->do_draw(tc);
166 }
167
168 std::optional<hegel::internal::json::json> schema() const override {
169 return inner_->schema();
170 }
171
172 std::optional<BasicGenerator<T>> as_basic() const override {
173 return inner_->as_basic();
174 }
176
200 template <typename F>
202 using ResultType = std::invoke_result_t<F, T>;
204 new MappedGenerator<T, ResultType>(inner_, std::move(f)));
205 }
206
228 template <typename F> std::invoke_result_t<F, T> flat_map(F&& f) const {
229 // Relevant types here:
230 // ResultType: some type
231 // gen_fn_: () -> T
232 // F: T -> Generator<ResultType>
233 // Function return type: Generator<ResultType>
234 using ResultType =
235 decltype(std::declval<std::invoke_result_t<F, T>>().do_draw(
236 std::declval<const TestCase&>()));
237 auto inner = inner_;
238 return compose([inner, f = std::forward<F>(f)](
239 const TestCase& tc) -> ResultType {
240 return f(inner->do_draw(tc)).do_draw(tc);
241 });
242 }
243
269 Generator<T> filter(std::function<bool(const T&)> pred) const {
270 auto inner = inner_;
271 return compose([inner, pred](const TestCase& tc) -> T {
272 for (int i = 0; i < 3; ++i) {
273 T value = inner->do_draw(tc);
274 if (pred(value)) {
275 return value;
276 }
277 }
278 tc.assume(false);
279 // unreachable: assume(false) throws
280 throw internal::HegelReject();
281 });
282 }
283
284 private:
285 std::shared_ptr<IGenerator<T>> inner_;
286 };
287
289 // Generator that produces values by invoking a user-provided function.
290 // Users never construct or reference this type directly; it's produced
291 // internally by compose() and by map()/flat_map()/filter().
292 template <typename T> class CompositeGenerator : public IGenerator<T> {
293 public:
294 explicit CompositeGenerator(std::function<T(const TestCase&)> fn)
295 : gen_fn_(std::move(fn)) {}
296
297 T do_draw(const TestCase& tc) const override { return gen_fn_(tc); }
298
299 private:
300 std::function<T(const TestCase&)> gen_fn_;
301 };
303
305 // Generator that applies a client-side transformation to values drawn
306 // from a source generator. Produced internally by Generator<T>::map().
307 //
308 // Preserves basic-ness (and therefore the server-side schema) by
309 // composing the map function into the source's BasicGenerator::parse
310 // step; falls back to `f(source->do_draw(tc))` when the source is not
311 // basic.
312 template <typename T, typename U>
313 class MappedGenerator : public IGenerator<U> {
314 public:
315 MappedGenerator(std::shared_ptr<IGenerator<T>> source,
316 std::function<U(T)> f)
317 : source_(std::move(source)), f_(std::move(f)) {}
318
319 std::optional<BasicGenerator<U>> as_basic() const override {
320 auto basic = source_->as_basic();
321 if (!basic)
322 return std::nullopt;
323 return basic->map(f_);
324 }
325
326 U do_draw(const TestCase& tc) const override {
327 if (auto basic = as_basic()) {
328 return basic->do_draw(tc);
329 }
330 return f_(source_->do_draw(tc));
331 }
332
333 private:
334 std::shared_ptr<IGenerator<T>> source_;
335 std::function<U(T)> f_;
336 };
338
360 template <typename F> auto compose(F&& fn) {
361 using T = std::invoke_result_t<F, const TestCase&>;
363 std::function<T(const TestCase&)>(std::forward<F>(fn))));
364 }
365
366} // namespace hegel::generators
367
368namespace hegel {
369
370 template <typename T>
372 return gen.do_draw(*this);
373 }
374
375} // namespace hegel
Handle to the currently-executing test case.
Definition test_case.h:34
void assume(bool condition) const
Reject the current test case if condition is false.
T draw(const generators::Generator< T > &gen) const
Draw a random value from a generator.
Definition core.h:371
The base class of all generators.
Definition core.h:157
Generator< std::invoke_result_t< F, T > > map(F f) const
*‍/
Definition core.h:201
Generator< T > filter(std::function< bool(const T &)> pred) const
Filter generated values by a predicate.
Definition core.h:269
std::invoke_result_t< F, T > flat_map(F &&f) const
Chain generators for dependent generation.
Definition core.h:228
Hegel generators.
Definition core.h:16
auto compose(F &&fn)
*‍/
Definition core.h:360
Main namespace.
Definition core.h:16
*‍/
Definition core.h:94