Hegel 0.11.4
Property-based testing for C++
Loading...
Searching...
No Matches
stateful.h
1#pragma once
2
3#include <cstddef>
4#include <cstdint>
5#include <functional>
6#include <map>
7#include <stdexcept>
8#include <string>
9#include <type_traits>
10#include <utility>
11#include <vector>
12
13#include "hegel/core.h"
14
59namespace hegel::stateful {
60
63
64 template <typename T> class VariablesGenerator;
65
122 template <typename T> class Pool {
123 public:
130 explicit Pool(const TestCase& tc) : tc_(tc), pool_handle_(tc) {}
131
137 void add(const T& element) {
138 int64_t var_id = pool_handle_.add(tc_);
139 // GCOVR_EXCL_START
140 if (pool_.find(var_id) != pool_.end()) {
141 throw std::runtime_error("unexpected variable id in map");
142 }
143 // GCOVR_EXCL_STOP
144 pool_.emplace(var_id, element);
145 }
146
152 void add(T&& element) {
153 int64_t var_id = pool_handle_.add(tc_);
154 // GCOVR_EXCL_START
155 if (pool_.find(var_id) != pool_.end()) {
156 throw std::runtime_error("unexpected variable id in map");
157 }
158 // GCOVR_EXCL_STOP
159 pool_.emplace(var_id, std::move(element));
160 }
161
167 std::size_t size() const { return pool_.size(); }
168
169 Pool(const Pool&) = delete;
170
171 private:
172 const TestCase& tc_;
173 hegel::internal::PoolHandle pool_handle_;
174 std::map<int64_t, T> pool_;
175
176 friend class VariablesGenerator<T>;
177 };
178
180 // Concrete IGenerator for variables.
181 template <typename T> class VariablesGenerator : public IGenerator<T> {
182 public:
183 explicit VariablesGenerator(Pool<T>& p, bool consume)
184 : p_(p), consume_(consume) {}
185
186 T do_draw(const TestCase& tc) const override {
187 int64_t variable = p_.pool_handle_.draw_variable(tc, consume_);
188
189 auto it = p_.pool_.find(variable);
190 // GCOVR_EXCL_START
191 if (it == p_.pool_.end()) {
192 throw std::runtime_error(
193 "Pool state diverged between the engine and the "
194 "client, or a bug in the pool bookkeeping.");
195 }
196 // GCOVR_EXCL_STOP
197 if (consume_) {
198 auto val = std::move(it->second);
199 p_.pool_.erase(it);
200 return val;
201 }
202 return it->second;
203 }
204
205 private:
206 Pool<T>& p_;
207 bool consume_;
208 };
210
213
242 template <typename T> Generator<T> values_consumed(Pool<T>& p) {
243 return Generator<T>(new VariablesGenerator<T>(p, true));
244 }
245
273 template <typename T> Generator<T> values_reusable(Pool<T>& p) {
274 return Generator<T>(new VariablesGenerator<T>(p, false));
275 }
276
289 template <typename T> class Rule {
290 public:
300 explicit Rule(std::string name, std::function<void(TestCase&, T&)> step)
301 : name_(std::move(name)), step_(std::move(step)) {}
302
308 const std::string& name() const { return name_; }
314 const std::function<void(TestCase&, T&)>& step() const { return step_; }
315
316 private:
317 std::string name_;
318 std::function<void(TestCase&, T&)> step_;
319 };
320
329 template <typename T> class Invariant {
330 public:
337 explicit Invariant(std::string name,
338 std::function<void(const T&)> invariant)
339 : name_(std::move(name)), invariant_(std::move(invariant)) {}
340
346 const std::string& name() const { return name_; }
353 const std::function<void(const T&)>& invariant() const {
354 return invariant_;
355 }
356
357 private:
358 std::string name_;
359 std::function<void(const T&)> invariant_;
360 };
361
367 template <typename State> struct StateMachineParams {
370 };
371
406 template <typename Derived, typename State> class StateMachine {
407 public:
409 using state_type = State;
420 : state(std::move(params.initial_state)) {}
421
424 State state;
425
432 std::vector<Invariant<Derived>> invariants() { return {}; }
433
434 protected:
435 ~StateMachine() = default;
436 };
437
441 struct RunParams {
457 bool print_state = true;
458 };
459
461 // prints the machine's state.
462 template <typename M>
463 void print_state(TestCase& tc, const M& machine, const RunParams& params) {
464 if (params.print_state) {
465 tc.note("state = " + internal::repr(machine.state));
466 }
467 }
468
469 // check if invariants hold on a given state
470 template <typename T>
471 void check_invariants(TestCase& tc, const std::string& origin,
472 const T& state,
473 const std::vector<Invariant<T>>& invariants) {
474 for (const auto& inv : invariants) {
475 try {
476 (inv.invariant())(state);
477 } catch (...) {
478 tc.note("Invariant " + inv.name() + " violated " + origin);
479 throw;
480 }
481 }
482 }
484
507 template <typename M>
508 void run(M& machine, TestCase& tc, const RunParams& params = {}) {
509 static_assert(
510 std::is_base_of<StateMachine<M, typename M::state_type>, M>::value,
511 "run() requires a machine deriving from "
512 "StateMachine<M, State>.");
513 std::vector<Rule<M>> rules = machine.rules();
514 std::vector<Invariant<M>> invariants = machine.invariants();
515 if (rules.empty()) {
516 throw std::invalid_argument(
517 "Cannot run a state machine with no rules.");
518 }
519 std::vector<std::string> rule_names;
520 rule_names.reserve(rules.size());
521 for (const Rule<M>& rule : rules)
522 rule_names.push_back(rule.name());
523
524 std::vector<std::string> invariant_names;
525 invariant_names.reserve(invariants.size());
526 for (const Invariant<M>& invariant : invariants)
527 invariant_names.push_back(invariant.name());
528
529 print_state(tc, machine, params);
530 check_invariants(tc, "in the initial state", machine, invariants);
531
532 internal::StateMachineHandle machine_handle(tc, rule_names,
533 invariant_names);
534 int64_t steps_run = 0;
535
536 while (true) {
537 internal::start_span(tc, internal::SpanLabel::StatefulRule);
538 int64_t next_rule_idx = machine_handle.next_rule(tc);
539 if (next_rule_idx == internal::state_machine_done) {
540 break;
541 }
542 // GCOVR_EXCL_START
543 if (next_rule_idx < 0 ||
544 static_cast<size_t>(next_rule_idx) >= rules.size()) {
545 throw std::runtime_error(
546 "state_machine_next_rule returned out-of-range "
547 "rule index. Please report this as a bug.");
548 }
549 // GCOVR_EXCL_STOP
550 steps_run++;
551 const Rule<M>& rule = rules[static_cast<size_t>(next_rule_idx)];
552 tc.note("Step " + std::to_string(steps_run) + ": " + rule.name());
553
554 try {
555 // nest the draws the step makes under its "Step N" header.
556 {
557 internal::NoteIndentScope indent(tc);
558 rule.step()(tc, machine);
559 }
560 print_state(tc, machine, params);
561 check_invariants(tc, "after step " + std::to_string(steps_run),
562 machine, invariants);
563 internal::stop_span(tc);
564 } catch (const internal::HegelReject&) {
565 tc.note("Rule stopped early due to violated assumption.");
566 machine_handle.rule_rejected(tc);
567 internal::stop_span(tc, true);
568 } catch (...) {
569 internal::stop_span(tc);
570 throw;
571 }
572 }
573 }
574
575
576} // namespace hegel::stateful
Handle to the currently-executing test case.
Definition test_case.h:40
void note(std::string_view message) const
Record a message that will be printed on the final replay of a failing test case.
The base class of all generators.
Definition core.h:67
Invariant(std::string name, std::function< void(const T &)> invariant)
Declare a new invariant.
Definition stateful.h:337
const std::string & name() const
Returns the name of the invariant.
Definition stateful.h:346
const std::function< void(const T &)> & invariant() const
Returns the function representing the predicate of the invariant.
Definition stateful.h:353
A pool of previously generated values. A pool lets data flow from one rule to another,...
Definition stateful.h:122
std::size_t size() const
Returns the number of variables in the pool.
Definition stateful.h:167
Pool(const TestCase &tc)
Creates an empty pool. Pools are tied to a test case. Do not reuse one across test cases.
Definition stateful.h:130
void add(T &&element)
Adds element to the pool. Overload for moving rvalues.
Definition stateful.h:152
void add(const T &element)
Adds element to the pool. Overload for copying lvalues.
Definition stateful.h:137
A rule is one possible action in a stateful test.
Definition stateful.h:289
Rule(std::string name, std::function< void(TestCase &, T &)> step)
Declares a new Rule.
Definition stateful.h:300
const std::function< void(TestCase &, T &)> & step() const
Returns the underlying step function of the rule.
Definition stateful.h:314
const std::string & name() const
Returns the name of the rule.
Definition stateful.h:308
Base class for a state machine. Holds the state and declares the rules and the invariants that act on...
Definition stateful.h:406
std::vector< Invariant< Derived > > invariants()
Invariants checked before the first step and after every valid step. Override to add them....
Definition stateful.h:432
State state
Definition stateful.h:424
State state_type
The type of the state the rules act on.
Definition stateful.h:409
StateMachine(StateMachineParams< State > params)
Builds a machine holding the given initial state.
Definition stateful.h:419
Definition stateful.h:64
Stateful (model-based) property testing.
Definition stateful.h:59
Generator< T > values_consumed(Pool< T > &p)
Returns a value from the pool and removes it.
Definition stateful.h:242
Generator< T > values_reusable(Pool< T > &p)
Returns a value from the pool without removing it.
Definition stateful.h:273
void run(M &machine, TestCase &tc, const RunParams &params={})
Executes a stateful test by repeatedly applying randomly chosen rules from machine to it and checking...
Definition stateful.h:508
Base interface for generators.
Definition core.h:29
Base interface for generators.
Definition core.h:29
Options for run.
Definition stateful.h:441
bool print_state
Definition stateful.h:457
Arguments for the StateMachine constructor.
Definition stateful.h:367
State initial_state
The state the first rule acts on.
Definition stateful.h:369