Hegel 0.13.1
Property-based testing for C++
Loading...
Searching...
No Matches
internal.h
1#pragma once
2
3#include "test_case.h"
4
5#include <cstddef>
6#include <cstdint>
7#include <exception>
8#include <optional>
9#include <string>
10#include <string_view>
11#include <vector>
12
14// opaque libhegel handle types.
15struct hegel_collection_t;
16struct hegel_pool_t;
17struct hegel_state_machine_t;
19
23namespace hegel::internal {
25
26 /* Typed draw primitives over libhegel's C ABI. Declared here (rather
27 * than in a private header) because the generator templates in the
28 * public headers call them from their do_draw implementations. Each
29 * throws HegelStopTest when the engine's choice budget is exhausted,
30 * HegelReject when the draw rejects itself, and std::runtime_error on
31 * any other engine failure.
32 */
33
34 // The kinds of compound draw the generators open spans around. The
35 // engine identifies a span only by its label: spans with the same label
36 // come from the same kind of generator, which makes them candidates for
37 // swapping and reordering when it shrinks. src/engine.cpp derives one
38 // stable label per kind from a name.
39 enum class SpanLabel {
40 List,
41 Set,
42 Map,
43 Tuple,
44 OneOf,
45 Optional,
46 FlatMap,
47 Filter,
48 Mapped,
49 StatefulRule,
50 };
51 inline constexpr std::size_t span_label_count =
52 static_cast<std::size_t>(SpanLabel::StatefulRule) + 1;
53
54 // Open / close a labeled span around a group of draws so the shrinker
55 // can treat them as a unit. stop_span(tc, true) marks the span rejected
56 // (e.g. a filter predicate failed) so the engine retries from before it.
57 void start_span(const TestCase& tc, SpanLabel label);
58 void stop_span(const TestCase& tc, bool discard = false);
59
60 int64_t draw_integer(const TestCase& tc, int64_t min_value,
61 int64_t max_value);
62 // Uses the engine's big-integer draw when the range exceeds int64_t.
63 uint64_t draw_integer_unsigned(const TestCase& tc, uint64_t min_value,
64 uint64_t max_value);
65 bool draw_boolean(const TestCase& tc, double p,
66 std::optional<bool> forced = std::nullopt);
67 double draw_float(const TestCase& tc, uint32_t width, double min_value,
68 double max_value, bool allow_nan, bool allow_infinity,
69 bool exclude_min, bool exclude_max,
70 double smallest_nonzero_magnitude);
71
72 inline constexpr uint64_t no_max_size = ~uint64_t{0};
73 class CollectionHandle {
74 public:
75 CollectionHandle(const TestCase& tc, uint64_t min_size,
76 uint64_t max_size);
77 ~CollectionHandle();
78 CollectionHandle(const CollectionHandle&) = delete;
79 CollectionHandle& operator=(const CollectionHandle&) = delete;
80
81 bool more(const TestCase& tc);
82
83 void reject(const TestCase& tc, const char* why);
84
85 private:
86 hegel_collection_t* handle_ = nullptr;
87 };
88
89 class PoolHandle {
90 public:
91 explicit PoolHandle(const TestCase& tc);
92 ~PoolHandle();
93 PoolHandle(const PoolHandle&) = delete;
94 PoolHandle& operator=(const PoolHandle&) = delete;
95
96 // Returns a fresh variable id.
97 int64_t add(const TestCase& tc);
98 // Returns the variable id the engine picked, removing it from the
99 // pool when consume is true.
100 int64_t draw_variable(const TestCase& tc, bool consume);
101
102 private:
103 hegel_pool_t* handle_ = nullptr;
104 };
105
106 inline constexpr int64_t state_machine_done = INT64_MIN;
107 class StateMachineHandle {
108 public:
109 StateMachineHandle(const TestCase& tc,
110 const std::vector<std::string>& rule_names,
111 const std::vector<int64_t>& rule_groups,
112 const std::vector<std::string>& invariant_names,
113 int64_t min_concurrency, int64_t max_concurrency);
114 ~StateMachineHandle();
115 StateMachineHandle(const StateMachineHandle&) = delete;
116 StateMachineHandle& operator=(const StateMachineHandle&) = delete;
117
118 // Starts the next round and returns its concurrency group, or
119 // state_machine_done once the engine's step budget for this test case
120 // is used up.
121 int64_t next_group(const TestCase& tc);
122 // Returns the index of the next rule to run this round, or
123 // state_machine_done once the round's rule stream is exhausted.
124 int64_t next_rule(const TestCase& tc, int64_t worker_index);
125 // Report that an assumption failed in the last rule, so it does not
126 // count against the step budget.
127 void rule_rejected(const TestCase& tc, int64_t worker_index);
128 bool should_check_invariant(const TestCase& tc,
129 int64_t invariant_index);
130
131 int64_t concurrency() const { return concurrency_; }
132
133 private:
134 hegel_state_machine_t* handle_ = nullptr;
135 int64_t concurrency_ = 0;
136 };
137
138 void set_concurrent_worker(TestCase& tc, std::size_t worker_index);
139
140 struct CapturedException {
141 std::exception_ptr exception;
142 std::string origin;
143
144 [[noreturn]] void rethrow() const;
145 };
146
147 CapturedException capture_current_exception();
148
149 class NoteIndentScope {
150 public:
151 explicit NoteIndentScope(const TestCase& tc);
152 ~NoteIndentScope();
153 NoteIndentScope(const NoteIndentScope&) = delete;
154 NoteIndentScope& operator=(const NoteIndentScope&) = delete;
155
156 private:
157 const TestCase& tc_;
158 };
159
160 class DrawLogScope {
161 public:
162 DrawLogScope(const TestCase& tc, std::string_view name,
163 bool repeatable);
164 ~DrawLogScope();
165 DrawLogScope(const DrawLogScope&) = delete;
166 DrawLogScope& operator=(const DrawLogScope&) = delete;
167
168 bool should_log() const;
169
170 void log(const std::string& rendered) const;
171
172 private:
173 const TestCase& tc_;
174 bool outermost_;
175 std::string display_;
176 };
177 /* Exception thrown when a test case is rejected and should be
178 * discarded (e.g. by `TestCase::assume(false)`, an exhausted
179 * `filter()`, or an `UnsatisfiedAssumption` from the engine).
180 * The runner records the case as INVALID and continues.
181 */
182 class HegelReject : public std::exception {
183 public:
184 const char* what() const noexcept override {
185 return "test case rejected";
186 }
187 };
188
189 /* Exception thrown when the backend tells us to abandon the current
190 * test iteration entirely (StopTest, Overflow, FlakyStrategyDefinition,
191 * FlakyReplay). The runner unwinds the test body and skips
192 * `mark_complete` — the engine already knows the iteration is over.
193 */
194 class HegelStopTest : public std::exception {
195 public:
196 const char* what() const noexcept override {
197 return "test case stopped by backend";
198 }
199 };
200
201} // namespace hegel::internal
202