3#include <condition_variable>
19#include "hegel/core.h"
108 : name_(std::move(
name)), group_(std::move(
group)),
116 const std::string&
name()
const {
return name_; }
123 const std::string&
group()
const {
return group_; }
270 std::vector<Invariant<Derived>>
invariants()
const {
return {}; }
329 template <
typename T>
class Pool {
344 void add(
const T& element) {
345 int64_t var_id = pool_handle_.add(tc_);
347 if (pool_.find(var_id) != pool_.end()) {
348 throw std::runtime_error(
"unexpected variable id in map");
351 pool_.emplace(var_id, element);
360 int64_t var_id = pool_handle_.add(tc_);
362 if (pool_.find(var_id) != pool_.end()) {
363 throw std::runtime_error(
"unexpected variable id in map");
366 pool_.emplace(var_id, std::move(element));
374 std::size_t
size()
const {
return pool_.size(); }
380 hegel::internal::PoolHandle pool_handle_;
381 std::map<int64_t, T> pool_;
388 template <
typename T>
class VariablesGenerator :
public IGenerator<T> {
390 explicit VariablesGenerator(Pool<T>& p,
bool consume)
391 : p_(p), consume_(consume) {}
393 T do_draw(
const TestCase& tc)
const override {
394 int64_t variable = p_.pool_handle_.draw_variable(tc, consume_);
396 auto it = p_.pool_.find(variable);
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.");
405 auto val = std::move(it->second);
485 template <
typename T>
class ConcurrentVariablesGenerator;
520 std::lock_guard<std::mutex> lock(mutex_);
521 int64_t variable = pool_handle_.add(tc);
522 values_.emplace(variable, std::move(value));
531 std::lock_guard<std::mutex> lock(mutex_);
532 return values_.empty();
541 std::lock_guard<std::mutex> lock(mutex_);
542 return values_.size();
549 internal::PoolHandle pool_handle_;
550 mutable std::mutex mutex_;
551 std::map<int64_t, T> values_;
553 friend class ConcurrentVariablesGenerator<T>;
557 template <
typename T>
558 class ConcurrentVariablesGenerator :
public IGenerator<T> {
560 ConcurrentVariablesGenerator(ConcurrentPool<T>& pool,
bool consume)
561 : pool_(pool), consume_(consume) {}
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_);
567 T result = std::move(pool_.values_.at(variable));
568 pool_.values_.erase(variable);
571 return pool_.values_.at(variable);
576 ConcurrentPool<T>& pool_;
590 template <
typename T>
592 return Generator<T>(
new ConcurrentVariablesGenerator<T>(pool,
true));
605 template <
typename T>
607 return Generator<T>(
new ConcurrentVariablesGenerator<T>(pool,
false));
622 template <
typename T>
class Rule {
634 : name_(std::move(
name)), step_(std::move(
step)) {}
641 const std::string&
name()
const {
return name_; }
647 const std::function<void(
TestCase&, T&)>&
step()
const {
return step_; }
651 std::function<void(
TestCase&, T&)> step_;
672 : name_(std::move(
name)), invariant_(std::move(
invariant)) {}
679 const std::string&
name()
const {
return name_; }
686 const std::function<void(
const T&)>&
invariant()
const {
692 std::function<void(
const T&)> invariant_;
753 :
state(std::move(params.initial_state)) {}
796 template <
typename M>
799 tc.
note(
"state = " + internal::repr(machine.state));
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))) {
817 inv.invariant()(state);
819 tc.note(
"Invariant " + inv.name() +
" violated " + origin);
854 template <
typename M>
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");
865 std::vector<ConcurrentRule<M>> rules = machine.rules();
866 std::vector<Invariant<M>> invariants = machine.invariants();
868 throw std::invalid_argument(
869 "Cannot run a concurrent state machine with no rules.");
872 std::vector<std::string> rule_names;
873 std::vector<std::string> invariant_names;
874 std::vector<std::string> group_names;
875 std::map<std::string, int64_t> group_ids;
879 rule_names.reserve(rules.size());
880 rule_groups.reserve(rules.size());
882 rule_names.push_back(rule.name());
883 auto [group, inserted] = group_ids.emplace(
884 rule.group(),
static_cast<int64_t
>(group_ids.size()));
886 group_names.push_back(rule.group());
888 rule_groups.push_back(group->second);
890 invariant_names.reserve(invariants.size());
892 invariant_names.push_back(invariant.name());
895 internal::StateMachineHandle machine_handle(
896 tc, rule_names, rule_groups, invariant_names, min_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);
903 enum class EventKind { RoundDone, Invalid, Overrun, Control, Panicked };
905 EventKind kind = EventKind::RoundDone;
906 std::optional<internal::CapturedException> exception;
910 std::condition_variable start;
911 std::condition_variable done;
912 uint64_t round_number = 0;
914 std::size_t num_workers_completed = 0;
915 std::vector<Event> events;
920 event.exception = internal::capture_current_exception();
922 std::rethrow_exception(event.exception->exception);
923 }
catch (
const internal::HegelStopTest&) {
924 event.kind = EventKind::Overrun;
925 }
catch (
const internal::HegelReject&) {
929 event.kind = EventKind::Invalid;
930 }
catch (
const std::invalid_argument&) {
931 event.kind = EventKind::Control;
933 event.kind = EventKind::Panicked;
938 auto run_round = [&](std::size_t worker,
TestCase& worker_tc) {
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) {
946 if (rule_index < 0 ||
947 static_cast<std::size_t
>(rule_index) >= rules.size()) {
949 throw std::runtime_error(
950 "state_machine_next_rule returned out-of-range "
951 "rule index. Please report this as a bug.");
956 rules[
static_cast<std::size_t
>(rule_index)];
957 worker_tc.note(
"Rule: " + rule.
name());
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));
965 "Rule stopped early due to violated assumption.");
973 std::size_t worker_count =
974 static_cast<std::size_t
>(machine_handle.concurrency());
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) {
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;
987 std::unique_lock<std::mutex> lock(state.mutex);
988 state.start.wait(lock, [&] {
990 state.round_number !=
991 observed_round_number;
998 observed_round_number = state.round_number;
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;
1007 state.done.notify_one();
1012 auto stop_workers = [&] {
1014 std::lock_guard<std::mutex> lock(state.mutex);
1017 state.start.notify_all();
1018 for (std::thread& worker : workers) {
1025 int64_t group = machine_handle.next_group(tc);
1026 if (group == internal::state_machine_done) {
1030 static_cast<std::size_t
>(group) >= group_names.size()) {
1032 throw std::runtime_error(
1033 "state_machine_next_group returned an unknown group "
1034 "index. Please report this as a bug.");
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 "\" ----------------");
1046 state.start.notify_all();
1048 std::unique_lock<std::mutex> lock(state.mutex);
1049 state.done.wait(lock, [&] {
1050 return state.num_workers_completed == worker_count;
1054 std::optional<std::size_t> control;
1055 bool saw_overrun =
false;
1056 bool saw_invalid =
false;
1057 std::optional<std::size_t> panic;
1060 for (std::size_t worker = 0; worker < worker_count; ++worker) {
1061 switch (state.events[worker].kind) {
1062 case EventKind::RoundDone:
1065 case EventKind::Invalid:
1069 case EventKind::Control:
1070 if (!control.has_value()) {
1074 case EventKind::Overrun:
1077 case EventKind::Panicked:
1078 if (!panic.has_value()) {
1084 if (control.has_value()) {
1085 state.events[*control].exception->rethrow();
1088 throw internal::HegelStopTest();
1091 throw internal::HegelReject();
1093 if (panic.has_value()) {
1094 state.events[*panic].exception->rethrow();
1097 tc,
"after round " + std::to_string(state.round_number),
1098 machine, invariants, std::ref(machine_handle));
1101 tc.
note(
"Final invariant check.");
1102 check_invariants(tc,
"in the final state", machine, invariants);
1134 template <
typename M>
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.");
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());
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());
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;
1166 while (machine_handle.next_group(tc) != internal::state_machine_done) {
1168 int64_t next_rule_idx = machine_handle.next_rule(tc, 0);
1169 if (next_rule_idx == internal::state_machine_done) {
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.");
1180 internal::start_span(tc, internal::SpanLabel::StatefulRule);
1182 const Rule<M>& rule = rules[
static_cast<size_t>(next_rule_idx)];
1183 tc.
note(
"Step " + std::to_string(steps_run) +
": " +
1189 internal::NoteIndentScope indent(tc);
1190 rule.step()(tc, machine);
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);
1199 internal::stop_span(tc);
1203 check_invariants(tc,
"after step " + std::to_string(steps_run),
1204 machine, invariants, std::ref(machine_handle));
1207 tc.
note(
"Final invariant check.");
1208 check_invariants(tc,
"in the final state", machine, invariants);
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
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 ¶ms={})
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