1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #pragma once
18 
19 /**
20  * CallOrderStateMachineHelper is a helper class for setting up a compile-time
21  * checked state machine that a sequence of calls is correct for completely
22  * setting up the state for some other type.
23  *
24  * Two examples where this could be used are with setting up a "Builder" flow
25  * for initializing an instance of some type, and writing tests where the state
26  * machine sets up expectations and preconditions, calls the function under
27  * test, and then evaluations postconditions.
28  *
29  * The purpose of this helper is to offload some of the boilerplate code to
30  * simplify the actual state classes, and is also a place to document how to
31  * go about setting up the state classes.
32  *
33  * To work at compile time, the idea is that each state is a unique C++ type,
34  * and the valid transitions between states are given by member functions on
35  * those types, with those functions returning a simple value type expressing
36  * the new state to use. Illegal state transitions become a compile error because
37  * a named member function does not exist.
38  *
39  * Example usage in a test:
40  *
41  *    A two step (+ terminator step) setup process can defined using:
42  *
43  *        class Step1 : public CallOrderStateMachineHelper<TestFixtureType, Step1> {
44  *           [[nodiscard]] auto firstMockCalledWith(int value1) {
45  *              // Set up an expectation or initial state using the fixture
46  *              EXPECT_CALL(getInstance->firstMock, FirstCall(value1));
47  *              return nextState<Step2>();
48  *           }
49  *        };
50  *
51  *        class Step2 : public CallOrderStateMachineHelper<TestFixtureType, Step2> {
52  *           [[nodiscard]] auto secondMockCalledWith(int value2) {
53  *              // Set up an expectation or initial state using the fixture
54  *              EXPECT_CALL(getInstance()->secondMock, SecondCall(value2));
55  *              return nextState<StepExecute>();
56  *           }
57  *        };
58  *
59  *        class StepExecute : public CallOrderStateMachineHelper<TestFixtureType, Step3> {
60  *           void execute() {
61  *              invokeFunctionUnderTest();
62  *           }
63  *        };
64  *
65  *    Note how the non-terminator steps return by value and use [[nodiscard]] to
66  *    enforce the setup flow. Only the terminator step returns void.
67  *
68  *    This can then be used in the tests with:
69  *
70  *        Step1::make(this).firstMockCalledWith(value1)
71  *                .secondMockCalledWith(value2)
72  *                .execute);
73  *
74  *    If the test fixture defines a `verify()` helper function which returns
75  *    `Step1::make(this)`, this can be simplified to:
76  *
77  *        verify().firstMockCalledWith(value1)
78  *                .secondMockCalledWith(value2)
79  *                .execute();
80  *
81  *    This is equivalent to the following calls made by the text function:
82  *
83  *        EXPECT_CALL(firstMock, FirstCall(value1));
84  *        EXPECT_CALL(secondMock, SecondCall(value2));
85  *        invokeFunctionUnderTest();
86  */
87 template <typename InstanceType, typename CurrentStateType>
88 class CallOrderStateMachineHelper {
89 public:
90     CallOrderStateMachineHelper() = default;
91 
92     // Disallow copying
93     CallOrderStateMachineHelper(const CallOrderStateMachineHelper&) = delete;
94     CallOrderStateMachineHelper& operator=(const CallOrderStateMachineHelper&) = delete;
95 
96     // Moving is intended use case.
97     CallOrderStateMachineHelper(CallOrderStateMachineHelper&&) = default;
98     CallOrderStateMachineHelper& operator=(CallOrderStateMachineHelper&&) = default;
99 
100     // Using a static "Make" function means the CurrentStateType classes do not
101     // need anything other than a default no-argument constructor.
make(InstanceType * instance)102     static CurrentStateType make(InstanceType* instance) {
103         auto helper = CurrentStateType();
104         helper.mInstance = instance;
105         return helper;
106     }
107 
108     // Each non-terminal state function
109     template <typename NextStateType>
nextState()110     auto nextState() {
111         // Note: Further operations on the current state become undefined
112         // operations as the instance pointer is moved to the next state type.
113         // But that doesn't stop someone from storing an intermediate state
114         // instance as a local and possibly calling one than one member function
115         // on it. By swapping with nullptr, we at least can try to catch this
116         // this at runtime.
117         InstanceType* instance = nullptr;
118         std::swap(instance, mInstance);
119         return NextStateType::make(instance);
120     }
121 
getInstance()122     InstanceType* getInstance() const { return mInstance; }
123 
124 private:
125     InstanceType* mInstance;
126 };
127