summaryrefslogtreecommitdiff
path: root/services/surfaceflinger/CompositionEngine/tests/CallOrderStateMachineHelper.h
blob: 2675dcf06958b6f5eb47747120bf94b28788a137 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

/**
 * CallOrderStateMachineHelper is a helper class for setting up a compile-time
 * checked state machine that a sequence of calls is correct for completely
 * setting up the state for some other type.
 *
 * Two examples where this could be used are with setting up a "Builder" flow
 * for initializing an instance of some type, and writing tests where the state
 * machine sets up expectations and preconditions, calls the function under
 * test, and then evaluations postconditions.
 *
 * The purpose of this helper is to offload some of the boilerplate code to
 * simplify the actual state classes, and is also a place to document how to
 * go about setting up the state classes.
 *
 * To work at compile time, the idea is that each state is a unique C++ type,
 * and the valid transitions between states are given by member functions on
 * those types, with those functions returning a simple value type expressing
 * the new state to use. Illegal state transitions become a compile error because
 * a named member function does not exist.
 *
 * Example usage in a test:
 *
 *    A two step (+ terminator step) setup process can defined using:
 *
 *        class Step1 : public CallOrderStateMachineHelper<TestFixtureType, Step1> {
 *           [[nodiscard]] auto firstMockCalledWith(int value1) {
 *              // Set up an expectation or initial state using the fixture
 *              EXPECT_CALL(getInstance->firstMock, FirstCall(value1));
 *              return nextState<Step2>();
 *           }
 *        };
 *
 *        class Step2 : public CallOrderStateMachineHelper<TestFixtureType, Step2> {
 *           [[nodiscard]] auto secondMockCalledWith(int value2) {
 *              // Set up an expectation or initial state using the fixture
 *              EXPECT_CALL(getInstance()->secondMock, SecondCall(value2));
 *              return nextState<StepExecute>();
 *           }
 *        };
 *
 *        class StepExecute : public CallOrderStateMachineHelper<TestFixtureType, Step3> {
 *           void execute() {
 *              invokeFunctionUnderTest();
 *           }
 *        };
 *
 *    Note how the non-terminator steps return by value and use [[nodiscard]] to
 *    enforce the setup flow. Only the terminator step returns void.
 *
 *    This can then be used in the tests with:
 *
 *        Step1::make(this).firstMockCalledWith(value1)
 *                .secondMockCalledWith(value2)
 *                .execute);
 *
 *    If the test fixture defines a `verify()` helper function which returns
 *    `Step1::make(this)`, this can be simplified to:
 *
 *        verify().firstMockCalledWith(value1)
 *                .secondMockCalledWith(value2)
 *                .execute();
 *
 *    This is equivalent to the following calls made by the text function:
 *
 *        EXPECT_CALL(firstMock, FirstCall(value1));
 *        EXPECT_CALL(secondMock, SecondCall(value2));
 *        invokeFunctionUnderTest();
 */
template <typename InstanceType, typename CurrentStateType>
class CallOrderStateMachineHelper {
public:
    CallOrderStateMachineHelper() = default;

    // Disallow copying
    CallOrderStateMachineHelper(const CallOrderStateMachineHelper&) = delete;
    CallOrderStateMachineHelper& operator=(const CallOrderStateMachineHelper&) = delete;

    // Moving is intended use case.
    CallOrderStateMachineHelper(CallOrderStateMachineHelper&&) = default;
    CallOrderStateMachineHelper& operator=(CallOrderStateMachineHelper&&) = default;

    // Using a static "Make" function means the CurrentStateType classes do not
    // need anything other than a default no-argument constructor.
    static CurrentStateType make(InstanceType* instance) {
        auto helper = CurrentStateType();
        helper.mInstance = instance;
        return helper;
    }

    // Each non-terminal state function
    template <typename NextStateType>
    auto nextState() {
        // Note: Further operations on the current state become undefined
        // operations as the instance pointer is moved to the next state type.
        // But that doesn't stop someone from storing an intermediate state
        // instance as a local and possibly calling one than one member function
        // on it. By swapping with nullptr, we at least can try to catch this
        // this at runtime.
        InstanceType* instance = nullptr;
        std::swap(instance, mInstance);
        return NextStateType::make(instance);
    }

    InstanceType* getInstance() const { return mInstance; }

private:
    InstanceType* mInstance;
};