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