Hegel 0.13.1
Property-based testing for C++
Loading...
Searching...
No Matches
stateful.h
1#pragma once
2
3#include <condition_variable>
4#include <cstddef>
5#include <cstdint>
6#include <exception>
7#include <functional>
8#include <map>
9#include <memory>
10#include <mutex>
11#include <optional>
12#include <stdexcept>
13#include <string>
14#include <thread>
15#include <type_traits>
16#include <utility>
17#include <vector>
18
19#include "hegel/core.h"
20
66namespace hegel::stateful {
67
70
71 template <typename T> class VariablesGenerator;
72 template <typename T> class Invariant;
73
75 inline constexpr const char* anonymous_group = "<anonymous>";
76
85 template <typename M> class ConcurrentRule {
86 public:
88 using Function = std::function<void(TestCase&, M&)>;
89
98 std::move(function)) {}
99
107 ConcurrentRule(std::string name, std::string group, Function function)
108 : name_(std::move(name)), group_(std::move(group)),
109 function_(std::move(function)) {}
110
116 const std::string& name() const { return name_; }
117
123 const std::string& group() const { return group_; }
124
130 const Function& function() const { return function_; }
131
132 private:
133 std::string name_;
134 std::string group_;
135 Function function_;
136 };
137
260 template <typename Derived> class ConcurrentStateMachine {
261 public:
270 std::vector<Invariant<Derived>> invariants() const { return {}; }
271 };
272
329 template <typename T> class Pool {
330 public:
337 explicit Pool(const TestCase& tc) : tc_(tc), pool_handle_(tc) {}
338
344 void add(const T& element) {
345 int64_t var_id = pool_handle_.add(tc_);
346 // GCOVR_EXCL_START
347 if (pool_.find(var_id) != pool_.end()) {
348 throw std::runtime_error("unexpected variable id in map");
349 }
350 // GCOVR_EXCL_STOP
351 pool_.emplace(var_id, element);
352 }
353
359 void add(T&& element) {
360 int64_t var_id = pool_handle_.add(tc_);
361 // GCOVR_EXCL_START
362 if (pool_.find(var_id) != pool_.end()) {
363 throw std::runtime_error("unexpected variable id in map");
364 }
365 // GCOVR_EXCL_STOP
366 pool_.emplace(var_id, std::move(element));
367 }
368
374 std::size_t size() const { return pool_.size(); }
375
376 Pool(const Pool&) = delete;
377
378 private:
379 const TestCase& tc_;
380 hegel::internal::PoolHandle pool_handle_;
381 std::map<int64_t, T> pool_;
382
383 friend class VariablesGenerator<T>;
384 };
385
387 // Concrete IGenerator for variables.
388 template <typename T> class VariablesGenerator : public IGenerator<T> {
389 public:
390 explicit VariablesGenerator(Pool<T>& p, bool consume)
391 : p_(p), consume_(consume) {}
392
393 T do_draw(const TestCase& tc) const override {
394 int64_t variable = p_.pool_handle_.draw_variable(tc, consume_);
395
396 auto it = p_.pool_.find(variable);
397 // GCOVR_EXCL_START
398 if (it == p_.pool_.end()) {
399 throw std::runtime_error(
400 "Pool state diverged between the engine and the "
401 "client, or a bug in the pool bookkeeping.");
402 }
403 // GCOVR_EXCL_STOP
404 if (consume_) {
405 auto val = std::move(it->second);
406 p_.pool_.erase(it);
407 return val;
408 }
409 return it->second;
410 }
411
412 private:
413 Pool<T>& p_;
414 bool consume_;
415 };
417
420
449 template <typename T> Generator<T> values_consumed(Pool<T>& p) {
450 return Generator<T>(new VariablesGenerator<T>(p, true));
451 }
452
480 template <typename T> Generator<T> values_reusable(Pool<T>& p) {
481 return Generator<T>(new VariablesGenerator<T>(p, false));
482 }
483
485 template <typename T> class ConcurrentVariablesGenerator;
487
502 template <typename T> class ConcurrentPool {
503 public:
509 explicit ConcurrentPool(const TestCase& tc) : pool_handle_(tc) {}
510
519 void add(const TestCase& tc, T value) {
520 std::lock_guard<std::mutex> lock(mutex_);
521 int64_t variable = pool_handle_.add(tc);
522 values_.emplace(variable, std::move(value));
523 }
524
530 bool empty() const {
531 std::lock_guard<std::mutex> lock(mutex_);
532 return values_.empty();
533 }
534
540 std::size_t size() const {
541 std::lock_guard<std::mutex> lock(mutex_);
542 return values_.size();
543 }
544
545 ConcurrentPool(const ConcurrentPool&) = delete;
546 ConcurrentPool& operator=(const ConcurrentPool&) = delete;
547
548 private:
549 internal::PoolHandle pool_handle_;
550 mutable std::mutex mutex_;
551 std::map<int64_t, T> values_;
552
553 friend class ConcurrentVariablesGenerator<T>;
554 };
555
557 template <typename T>
558 class ConcurrentVariablesGenerator : public IGenerator<T> {
559 public:
560 ConcurrentVariablesGenerator(ConcurrentPool<T>& pool, bool consume)
561 : pool_(pool), consume_(consume) {}
562
563 T do_draw(const TestCase& tc) const override {
564 std::lock_guard<std::mutex> lock(pool_.mutex_);
565 int64_t variable = pool_.pool_handle_.draw_variable(tc, consume_);
566 if (consume_) {
567 T result = std::move(pool_.values_.at(variable));
568 pool_.values_.erase(variable);
569 return result;
570 } else {
571 return pool_.values_.at(variable);
572 }
573 }
574
575 private:
576 ConcurrentPool<T>& pool_;
577 bool consume_;
578 };
580
590 template <typename T>
592 return Generator<T>(new ConcurrentVariablesGenerator<T>(pool, true));
593 }
594
605 template <typename T>
607 return Generator<T>(new ConcurrentVariablesGenerator<T>(pool, false));
608 }
609
622 template <typename T> class Rule {
623 public:
633 explicit Rule(std::string name, std::function<void(TestCase&, T&)> step)
634 : name_(std::move(name)), step_(std::move(step)) {}
635
641 const std::string& name() const { return name_; }
647 const std::function<void(TestCase&, T&)>& step() const { return step_; }
648
649 private:
650 std::string name_;
651 std::function<void(TestCase&, T&)> step_;
652 };
653
662 template <typename T> class Invariant {
663 public:
670 explicit Invariant(std::string name,
671 std::function<void(const T&)> invariant)
672 : name_(std::move(name)), invariant_(std::move(invariant)) {}
673
679 const std::string& name() const { return name_; }
686 const std::function<void(const T&)>& invariant() const {
687 return invariant_;
688 }
689
690 private:
691 std::string name_;
692 std::function<void(const T&)> invariant_;
693 };
694
700 template <typename State> struct StateMachineParams {
703 };
704
739 template <typename Derived, typename State> class StateMachine {
740 public:
742 using state_type = State;
753 : state(std::move(params.initial_state)) {}
754
757 State state;
758
766 std::vector<Invariant<Derived>> invariants() { return {}; }
767
768 protected:
769 ~StateMachine() = default;
770 };
771
775 struct RunParams {
791 bool print_state = true;
792 };
793
795 // prints the machine's state.
796 template <typename M>
797 void print_state(TestCase& tc, const M& machine, const RunParams& params) {
798 if (params.print_state) {
799 tc.note("state = " + internal::repr(machine.state));
800 }
801 }
802
803 template <typename T>
804 void check_invariants(
805 TestCase& tc, const std::string& origin, const T& state,
806 const std::vector<Invariant<T>>& invariants,
807 std::optional<std::reference_wrapper<internal::StateMachineHandle>>
808 machine_handle = std::nullopt) {
809 for (std::size_t index = 0; index < invariants.size(); ++index) {
810 if (machine_handle.has_value() &&
811 !machine_handle->get().should_check_invariant(
812 tc, static_cast<int64_t>(index))) {
813 continue;
814 }
815 const Invariant<T>& inv = invariants[index];
816 try {
817 inv.invariant()(state);
818 } catch (...) {
819 tc.note("Invariant " + inv.name() + " violated " + origin);
820 throw;
821 }
822 }
823 }
825
854 template <typename M>
855 void run_concurrent(M& machine, TestCase& tc, int64_t min_concurrency,
856 int64_t max_concurrency) {
857 static_assert(std::is_base_of<ConcurrentStateMachine<M>, M>::value,
858 "run_concurrent() requires a machine deriving from "
859 "ConcurrentStateMachine<M>.");
860 if (min_concurrency < 1 || min_concurrency > max_concurrency) {
861 throw std::invalid_argument(
862 "concurrency bounds must satisfy 1 <= min <= max");
863 }
864
865 std::vector<ConcurrentRule<M>> rules = machine.rules();
866 std::vector<Invariant<M>> invariants = machine.invariants();
867 if (rules.empty()) {
868 throw std::invalid_argument(
869 "Cannot run a concurrent state machine with no rules.");
870 }
871
872 std::vector<std::string> rule_names;
873 std::vector<std::string> invariant_names;
874 std::vector<std::string> group_names; // maps group ID to name
875 std::map<std::string, int64_t> group_ids; // maps group name to ID
876 std::vector<int64_t>
877 rule_groups; // ID of group that rule_names[i] belongs to is the ID
878 // at rule_groups[i].
879 rule_names.reserve(rules.size());
880 rule_groups.reserve(rules.size());
881 for (const ConcurrentRule<M>& rule : rules) {
882 rule_names.push_back(rule.name());
883 auto [group, inserted] = group_ids.emplace(
884 rule.group(), static_cast<int64_t>(group_ids.size()));
885 if (inserted) {
886 group_names.push_back(rule.group());
887 }
888 rule_groups.push_back(group->second);
889 }
890 invariant_names.reserve(invariants.size());
891 for (const Invariant<M>& invariant : invariants) {
892 invariant_names.push_back(invariant.name());
893 }
894
895 internal::StateMachineHandle machine_handle(
896 tc, rule_names, rule_groups, invariant_names, min_concurrency,
897 max_concurrency);
898 tc.note("Concurrency level: " +
899 std::to_string(machine_handle.concurrency()));
900 tc.note("Initial invariant check.");
901 check_invariants(tc, "in the initial state", machine, invariants);
902
903 enum class EventKind { RoundDone, Invalid, Overrun, Control, Panicked };
904 struct Event {
905 EventKind kind = EventKind::RoundDone;
906 std::optional<internal::CapturedException> exception;
907 };
908 struct RoundState {
909 std::mutex mutex;
910 std::condition_variable start;
911 std::condition_variable done;
912 uint64_t round_number = 0;
913 bool stop = false;
914 std::size_t num_workers_completed = 0;
915 std::vector<Event> events;
916 };
917
918 auto classify = [] {
919 Event event;
920 event.exception = internal::capture_current_exception();
921 try {
922 std::rethrow_exception(event.exception->exception);
923 } catch (const internal::HegelStopTest&) {
924 event.kind = EventKind::Overrun;
925 } catch (const internal::HegelReject&) {
926 // not the same as rule rejection - entire test case is rejected
927 // requires some worker race on cloned test cases with same
928 // parent
929 event.kind = EventKind::Invalid; // GCOVR_EXCL_LINE
930 } catch (const std::invalid_argument&) {
931 event.kind = EventKind::Control;
932 } catch (...) {
933 event.kind = EventKind::Panicked;
934 }
935 return event;
936 };
937
938 auto run_round = [&](std::size_t worker, TestCase& worker_tc) {
939 try {
940 while (true) {
941 int64_t rule_index = machine_handle.next_rule(
942 worker_tc, static_cast<int64_t>(worker));
943 if (rule_index == internal::state_machine_done) {
944 return Event{};
945 }
946 if (rule_index < 0 ||
947 static_cast<std::size_t>(rule_index) >= rules.size()) {
948 // GCOVR_EXCL_START
949 throw std::runtime_error(
950 "state_machine_next_rule returned out-of-range "
951 "rule index. Please report this as a bug.");
952 // GCOVR_EXCL_STOP
953 }
954
955 const ConcurrentRule<M>& rule =
956 rules[static_cast<std::size_t>(rule_index)];
957 worker_tc.note("Rule: " + rule.name());
958 try {
959 internal::NoteIndentScope indent(worker_tc);
960 rule.function()(worker_tc, machine);
961 } catch (const internal::HegelReject&) {
962 machine_handle.rule_rejected(
963 worker_tc, static_cast<int64_t>(worker));
964 worker_tc.note(
965 "Rule stopped early due to violated assumption.");
966 }
967 }
968 } catch (...) {
969 return classify();
970 }
971 };
972
973 std::size_t worker_count =
974 static_cast<std::size_t>(machine_handle.concurrency());
975 RoundState state;
976 state.events.resize(worker_count);
977 std::vector<std::thread> workers;
978 workers.reserve(worker_count);
979 for (std::size_t worker = 0; worker < worker_count; ++worker) {
980 TestCase worker_tc = tc.clone();
981 internal::set_concurrent_worker(worker_tc, worker);
982 workers.emplace_back(
983 [&, worker, worker_tc = std::move(worker_tc)]() mutable {
984 uint64_t observed_round_number = 0;
985 while (true) {
986 {
987 std::unique_lock<std::mutex> lock(state.mutex);
988 state.start.wait(lock, [&] {
989 return state.stop ||
990 state.round_number !=
991 observed_round_number; // main thread
992 // started a
993 // new round
994 });
995 if (state.stop) {
996 return;
997 }
998 observed_round_number = state.round_number;
999 }
1000
1001 {
1002 Event event = run_round(worker, worker_tc);
1003 std::lock_guard<std::mutex> lock(state.mutex);
1004 state.events[worker] = std::move(event);
1005 ++state.num_workers_completed;
1006 }
1007 state.done.notify_one();
1008 }
1009 });
1010 }
1011
1012 auto stop_workers = [&] {
1013 {
1014 std::lock_guard<std::mutex> lock(state.mutex);
1015 state.stop = true;
1016 }
1017 state.start.notify_all();
1018 for (std::thread& worker : workers) {
1019 worker.join();
1020 }
1021 };
1022
1023 try {
1024 while (true) {
1025 int64_t group = machine_handle.next_group(tc);
1026 if (group == internal::state_machine_done) {
1027 break;
1028 }
1029 if (group < 0 ||
1030 static_cast<std::size_t>(group) >= group_names.size()) {
1031 // GCOVR_EXCL_START
1032 throw std::runtime_error(
1033 "state_machine_next_group returned an unknown group "
1034 "index. Please report this as a bug.");
1035 // GCOVR_EXCL_STOP
1036 }
1037 {
1038 std::lock_guard<std::mutex> lock(state.mutex);
1039 state.num_workers_completed = 0;
1040 ++state.round_number;
1041 tc.note("---------------- Round " +
1042 std::to_string(state.round_number) + ": group \"" +
1043 group_names[static_cast<std::size_t>(group)] +
1044 "\" ----------------");
1045 }
1046 state.start.notify_all();
1047 {
1048 std::unique_lock<std::mutex> lock(state.mutex);
1049 state.done.wait(lock, [&] {
1050 return state.num_workers_completed == worker_count;
1051 });
1052 }
1053
1054 std::optional<std::size_t> control;
1055 bool saw_overrun = false;
1056 bool saw_invalid = false;
1057 std::optional<std::size_t> panic;
1058 // need to collect every event because the precedence is
1059 // control > overrun > invalid > panic > round complete
1060 for (std::size_t worker = 0; worker < worker_count; ++worker) {
1061 switch (state.events[worker].kind) {
1062 case EventKind::RoundDone:
1063 break;
1064 // GCOVR_EXCL_START
1065 case EventKind::Invalid:
1066 saw_invalid = true;
1067 break;
1068 // GCOVR_EXCL_STOP
1069 case EventKind::Control:
1070 if (!control.has_value()) {
1071 control = worker;
1072 }
1073 break;
1074 case EventKind::Overrun:
1075 saw_overrun = true;
1076 break;
1077 case EventKind::Panicked:
1078 if (!panic.has_value()) {
1079 panic = worker;
1080 }
1081 break;
1082 }
1083 }
1084 if (control.has_value()) {
1085 state.events[*control].exception->rethrow();
1086 }
1087 if (saw_overrun) {
1088 throw internal::HegelStopTest();
1089 }
1090 if (saw_invalid) {
1091 throw internal::HegelReject(); // GCOVR_EXCL_LINE
1092 }
1093 if (panic.has_value()) {
1094 state.events[*panic].exception->rethrow();
1095 }
1096 check_invariants(
1097 tc, "after round " + std::to_string(state.round_number),
1098 machine, invariants, std::ref(machine_handle));
1099 }
1100
1101 tc.note("Final invariant check.");
1102 check_invariants(tc, "in the final state", machine, invariants);
1103 } catch (...) {
1104 stop_workers();
1105 throw;
1106 }
1107 stop_workers();
1108 }
1109
1134 template <typename M>
1135 void run(M& machine, TestCase& tc, const RunParams& params = {}) {
1136 static_assert(
1137 std::is_base_of<StateMachine<M, typename M::state_type>, M>::value,
1138 "run() requires a machine deriving from "
1139 "StateMachine<M, State>.");
1140 std::vector<Rule<M>> rules = machine.rules();
1141 std::vector<Invariant<M>> invariants = machine.invariants();
1142 if (rules.empty()) {
1143 throw std::invalid_argument(
1144 "Cannot run a state machine with no rules.");
1145 }
1146 std::vector<std::string> rule_names;
1147 rule_names.reserve(rules.size());
1148 for (const Rule<M>& rule : rules)
1149 rule_names.push_back(rule.name());
1150
1151 std::vector<std::string> invariant_names;
1152 invariant_names.reserve(invariants.size());
1153 for (const Invariant<M>& invariant : invariants)
1154 invariant_names.push_back(invariant.name());
1155
1156 std::vector<int64_t> rule_groups(rule_names.size(), 0);
1157 internal::StateMachineHandle machine_handle(tc, rule_names, rule_groups,
1158 invariant_names, 1, 1);
1159 tc.note("Initial invariant check.");
1160 print_state(tc, machine, params);
1161 check_invariants(tc, "in the initial state", machine, invariants);
1162 int64_t steps_run = 0;
1163
1164 // Sequential stateful testing is modeled as a concurrent stateful test
1165 // with one group and one worker.
1166 while (machine_handle.next_group(tc) != internal::state_machine_done) {
1167 while (true) {
1168 int64_t next_rule_idx = machine_handle.next_rule(tc, 0);
1169 if (next_rule_idx == internal::state_machine_done) {
1170 break;
1171 }
1172 // GCOVR_EXCL_START
1173 if (next_rule_idx < 0 ||
1174 static_cast<size_t>(next_rule_idx) >= rules.size()) {
1175 throw std::runtime_error(
1176 "state_machine_next_rule returned out-of-range "
1177 "rule index. Please report this as a bug.");
1178 }
1179 // GCOVR_EXCL_STOP
1180 internal::start_span(tc, internal::SpanLabel::StatefulRule);
1181 steps_run++;
1182 const Rule<M>& rule = rules[static_cast<size_t>(next_rule_idx)];
1183 tc.note("Step " + std::to_string(steps_run) + ": " +
1184 rule.name());
1185
1186 try {
1187 // nest the draws the step makes under its "Step N" header.
1188 {
1189 internal::NoteIndentScope indent(tc);
1190 rule.step()(tc, machine);
1191 }
1192 print_state(tc, machine, params);
1193 internal::stop_span(tc);
1194 } catch (const internal::HegelReject&) {
1195 tc.note("Rule stopped early due to violated assumption.");
1196 machine_handle.rule_rejected(tc, 0);
1197 internal::stop_span(tc, true);
1198 } catch (...) {
1199 internal::stop_span(tc);
1200 throw;
1201 }
1202 }
1203 check_invariants(tc, "after step " + std::to_string(steps_run),
1204 machine, invariants, std::ref(machine_handle));
1205 }
1206
1207 tc.note("Final invariant check.");
1208 check_invariants(tc, "in the final state", machine, invariants);
1209 }
1210
1211
1212} // namespace hegel::stateful
Handle to the currently-executing test case.
Definition test_case.h:40
TestCase clone() const
Creates a clone of this test case with an independent choice sequence forked from this test case.
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
A pool of values that concurrent workers can share.
Definition stateful.h:502
ConcurrentPool(const TestCase &tc)
Creates an empty pool.
Definition stateful.h:509
bool empty() const
Returns true if the pool contains no values.
Definition stateful.h:530
std::size_t size() const
Returns the number of values in the pool.
Definition stateful.h:540
void add(const TestCase &tc, T value)
Adds a value to the pool.
Definition stateful.h:519
An action that a worker can apply to a concurrent state machine.
Definition stateful.h:85
std::function< void(TestCase &, M &)> Function
The type of the function that applies the rule.
Definition stateful.h:88
ConcurrentRule(std::string name, Function function)
Creates a rule in anonymous_group.
Definition stateful.h:96
const std::string & group() const
Returns the concurrency group of the rule.
Definition stateful.h:123
const Function & function() const
Returns the function that applies the rule.
Definition stateful.h:130
ConcurrentRule(std::string name, std::string group, Function function)
Creates a rule in the specified group.
Definition stateful.h:107
const std::string & name() const
Returns the name of the rule.
Definition stateful.h:116
Base class for a concurrent state machine.
Definition stateful.h:260
std::vector< Invariant< Derived > > invariants() const
Returns the invariants of the machine.
Definition stateful.h:270
An invariant is a predicate that must hold at any point in the stateful test. They are evaluated in f...
Definition stateful.h:662
Invariant(std::string name, std::function< void(const T &)> invariant)
Declare a new invariant.
Definition stateful.h:670
const std::string & name() const
Returns the name of the invariant.
Definition stateful.h:679
const std::function< void(const T &)> & invariant() const
Returns the function representing the predicate of the invariant.
Definition stateful.h:686
A pool of previously generated values. A pool lets data flow from one rule to another,...
Definition stateful.h:329
std::size_t size() const
Returns the number of variables in the pool.
Definition stateful.h:374
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:337
void add(T &&element)
Adds element to the pool. Overload for moving rvalues.
Definition stateful.h:359
void add(const T &element)
Adds element to the pool. Overload for copying lvalues.
Definition stateful.h:344
A rule is one possible action in a stateful test.
Definition stateful.h:622
Rule(std::string name, std::function< void(TestCase &, T &)> step)
Declares a new Rule.
Definition stateful.h:633
const std::function< void(TestCase &, T &)> & step() const
Returns the underlying step function of the rule.
Definition stateful.h:647
const std::string & name() const
Returns the name of the rule.
Definition stateful.h:641
Base class for a state machine. Holds the state and declares the rules and the invariants that act on...
Definition stateful.h:739
std::vector< Invariant< Derived > > invariants()
Invariants checked in full before the first step and after the final step, and sampled between steps....
Definition stateful.h:766
State state
Definition stateful.h:757
State state_type
The type of the state the rules act on.
Definition stateful.h:742
StateMachine(StateMachineParams< State > params)
Builds a machine holding the given initial state.
Definition stateful.h:752
Definition stateful.h:71
Stateful (model-based) property testing.
Definition stateful.h:66
void run_concurrent(M &machine, TestCase &tc, int64_t min_concurrency, int64_t max_concurrency)
Executes a concurrent stateful test with rules from machine.
Definition stateful.h:855
Generator< T > values_consumed(Pool< T > &p)
Returns a value from the pool and removes it.
Definition stateful.h:449
Generator< T > values_reusable(Pool< T > &p)
Returns a value from the pool without removing it.
Definition stateful.h:480
constexpr const char * anonymous_group
The group name for rules that do not specify a group.
Definition stateful.h:75
void run(M &machine, TestCase &tc, const RunParams &params={})
Executes a stateful test by repeatedly applying randomly chosen rules from machine to it....
Definition stateful.h:1135
Base interface for generators.
Definition core.h:29
Base interface for generators.
Definition core.h:29
Options for run.
Definition stateful.h:775
bool print_state
Definition stateful.h:791
Arguments for the StateMachine constructor.
Definition stateful.h:700
State initial_state
The state the first rule acts on.
Definition stateful.h:702