Hegel 0.13.3
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<TestCase> worker_cases;
978 worker_cases.reserve(worker_count);
979 for (std::size_t worker = 0; worker < worker_count; ++worker) {
980 worker_cases.push_back(tc.clone());
981 internal::set_concurrent_worker(worker_cases.back(), worker);
982 }
983 std::vector<std::thread> workers;
984 workers.reserve(worker_count);
985 for (std::size_t worker = 0; worker < worker_count; ++worker) {
986 workers.emplace_back(
987 [&, worker,
988 worker_tc = std::move(worker_cases[worker])]() mutable {
989 uint64_t observed_round_number = 0;
990 while (true) {
991 {
992 std::unique_lock<std::mutex> lock(state.mutex);
993 state.start.wait(lock, [&] {
994 return state.stop ||
995 state.round_number !=
996 observed_round_number; // main thread
997 // started a
998 // new round
999 });
1000 if (state.stop) {
1001 return;
1002 }
1003 observed_round_number = state.round_number;
1004 }
1005
1006 {
1007 Event event = run_round(worker, worker_tc);
1008 std::lock_guard<std::mutex> lock(state.mutex);
1009 state.events[worker] = std::move(event);
1010 ++state.num_workers_completed;
1011 }
1012 state.done.notify_one();
1013 }
1014 });
1015 }
1016
1017 auto stop_workers = [&] {
1018 {
1019 std::lock_guard<std::mutex> lock(state.mutex);
1020 state.stop = true;
1021 }
1022 state.start.notify_all();
1023 for (std::thread& worker : workers) {
1024 worker.join();
1025 }
1026 };
1027
1028 try {
1029 while (true) {
1030 int64_t group = machine_handle.next_group(tc);
1031 if (group == internal::state_machine_done) {
1032 break;
1033 }
1034 if (group < 0 ||
1035 static_cast<std::size_t>(group) >= group_names.size()) {
1036 // GCOVR_EXCL_START
1037 throw std::runtime_error(
1038 "state_machine_next_group returned an unknown group "
1039 "index. Please report this as a bug.");
1040 // GCOVR_EXCL_STOP
1041 }
1042 {
1043 std::lock_guard<std::mutex> lock(state.mutex);
1044 state.num_workers_completed = 0;
1045 ++state.round_number;
1046 tc.note("---------------- Round " +
1047 std::to_string(state.round_number) + ": group \"" +
1048 group_names[static_cast<std::size_t>(group)] +
1049 "\" ----------------");
1050 }
1051 state.start.notify_all();
1052 {
1053 std::unique_lock<std::mutex> lock(state.mutex);
1054 state.done.wait(lock, [&] {
1055 return state.num_workers_completed == worker_count;
1056 });
1057 }
1058
1059 std::optional<std::size_t> control;
1060 bool saw_overrun = false;
1061 bool saw_invalid = false;
1062 std::optional<std::size_t> panic;
1063 // need to collect every event because the precedence is
1064 // control > overrun > invalid > panic > round complete
1065 for (std::size_t worker = 0; worker < worker_count; ++worker) {
1066 switch (state.events[worker].kind) {
1067 case EventKind::RoundDone:
1068 break;
1069 // GCOVR_EXCL_START
1070 case EventKind::Invalid:
1071 saw_invalid = true;
1072 break;
1073 // GCOVR_EXCL_STOP
1074 case EventKind::Control:
1075 if (!control.has_value()) {
1076 control = worker;
1077 }
1078 break;
1079 case EventKind::Overrun:
1080 saw_overrun = true;
1081 break;
1082 case EventKind::Panicked:
1083 if (!panic.has_value()) {
1084 panic = worker;
1085 }
1086 break;
1087 }
1088 }
1089 if (control.has_value()) {
1090 state.events[*control].exception->rethrow();
1091 }
1092 if (saw_overrun) {
1093 throw internal::HegelStopTest();
1094 }
1095 if (saw_invalid) {
1096 throw internal::HegelReject(); // GCOVR_EXCL_LINE
1097 }
1098 if (panic.has_value()) {
1099 state.events[*panic].exception->rethrow();
1100 }
1101 check_invariants(
1102 tc, "after round " + std::to_string(state.round_number),
1103 machine, invariants, std::ref(machine_handle));
1104 }
1105
1106 tc.note("Final invariant check.");
1107 check_invariants(tc, "in the final state", machine, invariants);
1108 } catch (...) {
1109 stop_workers();
1110 throw;
1111 }
1112 stop_workers();
1113 }
1114
1139 template <typename M>
1140 void run(M& machine, TestCase& tc, const RunParams& params = {}) {
1141 static_assert(
1142 std::is_base_of<StateMachine<M, typename M::state_type>, M>::value,
1143 "run() requires a machine deriving from "
1144 "StateMachine<M, State>.");
1145 std::vector<Rule<M>> rules = machine.rules();
1146 std::vector<Invariant<M>> invariants = machine.invariants();
1147 if (rules.empty()) {
1148 throw std::invalid_argument(
1149 "Cannot run a state machine with no rules.");
1150 }
1151 std::vector<std::string> rule_names;
1152 rule_names.reserve(rules.size());
1153 for (const Rule<M>& rule : rules)
1154 rule_names.push_back(rule.name());
1155
1156 std::vector<std::string> invariant_names;
1157 invariant_names.reserve(invariants.size());
1158 for (const Invariant<M>& invariant : invariants)
1159 invariant_names.push_back(invariant.name());
1160
1161 std::vector<int64_t> rule_groups(rule_names.size(), 0);
1162 internal::StateMachineHandle machine_handle(tc, rule_names, rule_groups,
1163 invariant_names, 1, 1);
1164 tc.note("Initial invariant check.");
1165 print_state(tc, machine, params);
1166 check_invariants(tc, "in the initial state", machine, invariants);
1167 int64_t steps_run = 0;
1168
1169 // Sequential stateful testing is modeled as a concurrent stateful test
1170 // with one group and one worker.
1171 while (machine_handle.next_group(tc) != internal::state_machine_done) {
1172 while (true) {
1173 int64_t next_rule_idx = machine_handle.next_rule(tc, 0);
1174 if (next_rule_idx == internal::state_machine_done) {
1175 break;
1176 }
1177 // GCOVR_EXCL_START
1178 if (next_rule_idx < 0 ||
1179 static_cast<size_t>(next_rule_idx) >= rules.size()) {
1180 throw std::runtime_error(
1181 "state_machine_next_rule returned out-of-range "
1182 "rule index. Please report this as a bug.");
1183 }
1184 // GCOVR_EXCL_STOP
1185 internal::start_span(tc, internal::SpanLabel::StatefulRule);
1186 steps_run++;
1187 const Rule<M>& rule = rules[static_cast<size_t>(next_rule_idx)];
1188 tc.note("Step " + std::to_string(steps_run) + ": " +
1189 rule.name());
1190
1191 try {
1192 // nest the draws the step makes under its "Step N" header.
1193 {
1194 internal::NoteIndentScope indent(tc);
1195 rule.step()(tc, machine);
1196 }
1197 print_state(tc, machine, params);
1198 internal::stop_span(tc);
1199 } catch (const internal::HegelReject&) {
1200 tc.note("Rule stopped early due to violated assumption.");
1201 machine_handle.rule_rejected(tc, 0);
1202 internal::stop_span(tc, true);
1203 } catch (...) {
1204 internal::stop_span(tc);
1205 throw;
1206 }
1207 }
1208 check_invariants(tc, "after step " + std::to_string(steps_run),
1209 machine, invariants, std::ref(machine_handle));
1210 }
1211
1212 tc.note("Final invariant check.");
1213 check_invariants(tc, "in the final state", machine, invariants);
1214 }
1215
1216
1217} // 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:1140
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