blob: 2675dcf06958b6f5eb47747120bf94b28788a137 [file] [log] [blame]
Lloyd Pique17ca7422019-11-14 14:24:10 -08001/*
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 */
87template <typename InstanceType, typename CurrentStateType>
88class CallOrderStateMachineHelper {
89public:
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.
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>
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
122 InstanceType* getInstance() const { return mInstance; }
123
124private:
125 InstanceType* mInstance;
126};