• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 946895122

pending completion
946895122

push

gitlab-ci

kacf
test: Add the big `StateTransitionsTest`.

This test was almost entirely copied from the Golang client, and then
adapted to C++. Some key points:

* No new tests have been added.

* The internal state list has been removed. The usefulness of testing
  internal states is questionable, even in the original test. It's the
  external behavior that matters.

* `SupportsRollback` and `NeedsArtifactReboot` have been removed from
  checking because they are called at slightly different points in the
  C++ client, and they are not really important to the flow.

* All tests involving missing features have been disabled, with ticket
  numbers attached.

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>

4932 of 6272 relevant lines covered (78.64%)

196.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/common/state_machine.hpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#ifndef MENDER_COMMON_STATE_MACHINE_HPP
16
#define MENDER_COMMON_STATE_MACHINE_HPP
17

18
#include <queue>
19
#include <unordered_map>
20
#include <unordered_set>
21

22
#include <common/common.hpp>
23
#include <common/events.hpp>
24
#include <common/log.hpp>
25

26
namespace mender {
27
namespace common {
28
namespace state_machine {
29

30
using namespace std;
31

32
namespace common = mender::common;
33
namespace events = mender::common::events;
34
namespace log = mender::common::log;
35

36
template <typename ContextType, typename EventType>
37
class StateMachineRunner;
38

39
template <typename EventType>
40
class EventPoster {
41
public:
42
        virtual ~EventPoster() {
×
43
        }
×
44

45
        virtual void PostEvent(EventType event) = 0;
46
};
47

48
template <typename ContextType, typename EventType>
49
class State {
50
public:
51
        virtual ~State() {
×
52
        }
×
53

54
        virtual void OnEnter(ContextType &ctx, EventPoster<EventType> &poster) = 0;
55
};
56

57
enum class TransitionFlag {
58
        Immediate,
59
        Deferred,
60
};
61

62
template <typename ContextType, typename EventType>
63
class StateMachine {
64
public:
65
        StateMachine(State<ContextType, EventType> &start_state) :
66
                current_state_(&start_state) {
67
        }
68
        StateMachine(StateMachine &) = delete;
69

70
        void SetState(State<ContextType, EventType> &state) {
71
                current_state_ = &state;
72
                state_entered_ = false;
73
        }
74

75
private:
76
        struct TransitionCondition {
77
                // Note: Comparing address-of states. We don't want to rely on comparison operators
78
                // in the states themselves, and we just want to know if they are different
79
                // instances.
80
                State<ContextType, EventType> *state;
81
                EventType event;
82

83
                bool operator==(const TransitionCondition &t) const {
×
84
                        return state == t.state && event == t.event;
×
85
                }
86
        };
87

88
        class Hasher {
89
        public:
90
                size_t operator()(const TransitionCondition &obj) const {
×
91
                        return std::hash<State<ContextType, EventType> *>()(obj.state)
×
92
                                   ^ std::hash<int>()(static_cast<int>(obj.event));
×
93
                }
94
        };
95

96
        State<ContextType, EventType> *current_state_;
97
        bool state_entered_ {false};
98

99
        unordered_map<TransitionCondition, State<ContextType, EventType> *, Hasher> transitions_;
100
        unordered_set<EventType> deferred_events_;
101

102
        friend class StateMachineRunner<ContextType, EventType>;
103

104
public:
105
        void AddTransition(
106
                State<ContextType, EventType> &source_state,
107
                EventType event,
108
                State<ContextType, EventType> &target_state,
109
                TransitionFlag flag) {
110
                transitions_[TransitionCondition {&source_state, event}] = &target_state;
111
                if (flag == TransitionFlag::Deferred) {
112
                        // Event is involved in at least one deferred transition, so mark that.
113
                        deferred_events_.insert(event);
114
                }
115
        }
116
};
117

118
template <typename ContextType, typename EventType>
119
class StateMachineRunner : virtual public EventPoster<EventType> {
120
public:
121
        StateMachineRunner(ContextType &ctx) :
122
                ctx_(ctx) {
123
        }
124
        StateMachineRunner(StateMachineRunner &) = delete;
125

126
        void PostEvent(EventType event) override {
×
127
                event_queue_.push(event);
×
128
                PostToEventLoop();
×
129
        }
×
130

131
        // Continously run state machinery on event loop.
132
        void AttachToEventLoop(events::EventLoop &event_loop) {
133
                DetachFromEventLoop();
134

135
                cancelled_ = make_shared<bool>(false);
136

137
                // We don't actually own the object, we are just keeping a pointer to it. Use null
138
                // deleter.
139
                event_loop_.reset(&event_loop, [](events::EventLoop *loop) {});
140
        }
141

142
        void DetachFromEventLoop() {
143
                if (cancelled_) {
144
                        *cancelled_ = true;
145
                        cancelled_.reset();
146
                }
147
                event_loop_.reset();
148
        }
149

150
        void AddStateMachine(StateMachine<ContextType, EventType> &machine) {
151
                machines_.push_back(&machine);
152
        }
153

154
private:
155
        void RunOne() {
×
156
                vector<State<ContextType, EventType> *> to_run;
×
157

158
                for (auto machine : machines_) {
×
159
                        if (!machine->state_entered_) {
×
160
                                to_run.push_back(machine->current_state_);
×
161
                                machine->state_entered_ = true;
×
162
                        }
163
                }
164

165
                const size_t size = event_queue_.size();
×
166

167
                for (size_t count = 0; to_run.empty() && count < size; count++) {
×
168
                        bool deferred = false;
×
169
                        auto event = event_queue_.front();
×
170
                        event_queue_.pop();
×
171

172
                        for (auto machine : machines_) {
×
173
                                typename StateMachine<ContextType, EventType>::TransitionCondition cond {
×
174
                                        machine->current_state_, event};
×
175
                                if (machine->deferred_events_.find(event) != machine->deferred_events_.end()) {
×
176
                                        deferred = true;
×
177
                                }
178

179
                                auto match = machine->transitions_.find(cond);
×
180
                                if (match == machine->transitions_.end()) {
×
181
                                        // No match in this machine, continue.
182
                                        continue;
×
183
                                }
184

185
                                auto &target = match->second;
×
186
                                to_run.push_back(target);
×
187
                                machine->current_state_ = target;
×
188
                        }
189

190
                        if (to_run.empty()) {
×
191
                                if (deferred) {
×
192
                                        // Put back in the queue to try later. This won't be tried
193
                                        // again during this run, due to only making `size`
194
                                        // attempts in the for loop.
195
                                        event_queue_.push(event);
×
196
                                } else {
197
                                        log::Warning(
×
198
                                                "State machine event " + to_string(static_cast<int>(event))
×
199
                                                + " was not handled by any state. This is a bug and could hang the state machine.");
×
200
                                        assert(!to_run.empty());
×
201
                                }
202
                        }
203
                }
204

205
                if (!to_run.empty()) {
×
206
                        for (auto &state : to_run) {
×
207
                                log::Trace("Entering state " + common::BestAvailableTypeName(*state));
×
208
                                state->OnEnter(ctx_, *this);
×
209
                        }
210
                        // Since we ran something, there may be more events waiting to
211
                        // execute. OTOH, if we didn't, it either means that there are no events, or
212
                        // it means that all events currently in the queue are deferred, and not
213
                        // actionable until at least one state machine reaches a different state.
214
                        PostToEventLoop();
×
215
                }
216
        }
×
217

218
        void PostToEventLoop() {
×
219
                if (!event_loop_) {
×
220
                        return;
×
221
                }
222

223
                auto cancelled = cancelled_;
×
224
                event_loop_->Post([cancelled, this]() {
×
225
                        if (!*cancelled_) {
×
226
                                RunOne();
×
227
                        }
228
                });
229
        }
230

231
        ContextType &ctx_;
232

233
        shared_ptr<bool> cancelled_;
234
        vector<StateMachine<ContextType, EventType> *> machines_;
235

236
        queue<EventType> event_queue_;
237

238
        // Would be nice with optional<EventLoop &> reference here, but optional doesn't support
239
        // references. Use a pointer with a null deleter instead.
240
        shared_ptr<events::EventLoop> event_loop_;
241
};
242

243
} // namespace state_machine
244
} // namespace common
245
} // namespace mender
246

247
#endif // MENDER_COMMON_STATE_MACHINE_HPP
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc