go.uber.org/yarpc@v1.72.1/api/peer/peertest/peerlistaction.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package peertest 22 23 import ( 24 "context" 25 "fmt" 26 "sync" 27 "testing" 28 "time" 29 30 "github.com/stretchr/testify/assert" 31 "go.uber.org/yarpc/api/peer" 32 "go.uber.org/yarpc/api/transport" 33 "go.uber.org/yarpc/internal/testtime" 34 ) 35 36 // ListActionDeps are passed through PeerListActions' Apply methods in order 37 // to allow the PeerListAction to modify state other than just the PeerList 38 type ListActionDeps struct { 39 Peers map[string]*LightMockPeer 40 } 41 42 // PeerListAction defines actions that can be applied to a PeerList 43 type PeerListAction interface { 44 // Apply runs a function on the PeerList and asserts the result 45 Apply(*testing.T, peer.Chooser, ListActionDeps) 46 } 47 48 // StartAction is an action for testing PeerList.Start 49 type StartAction struct { 50 ExpectedErr error 51 } 52 53 // Apply runs "Start" on the peerList and validates the error 54 func (a StartAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 55 err := pl.Start() 56 assert.Equal(t, a.ExpectedErr, err) 57 } 58 59 // StopAction is an action for testing PeerList.Stop 60 type StopAction struct { 61 ExpectedErr error 62 } 63 64 // Apply runs "Stop" on the peerList and validates the error 65 func (a StopAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 66 err := pl.Stop() 67 assert.Equal(t, a.ExpectedErr, err, "Stop action expected error %v, got %v", a.ExpectedErr, err) 68 } 69 70 // ChooseMultiAction will run Choose multiple times on the PeerList 71 // It will assert if there are ANY failures 72 type ChooseMultiAction struct { 73 ExpectedPeers []string 74 } 75 76 // Apply runs "Choose" on the peerList for every ExpectedPeer 77 func (a ChooseMultiAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 78 for _, expectedPeer := range a.ExpectedPeers { 79 action := ChooseAction{ 80 ExpectedPeer: expectedPeer, 81 InputContextTimeout: 50 * testtime.Millisecond, 82 } 83 action.Apply(t, pl, deps) 84 } 85 } 86 87 // ChooseAction is an action for choosing a peer from the peerlist 88 type ChooseAction struct { 89 InputContext context.Context 90 InputContextTimeout time.Duration 91 InputRequest *transport.Request 92 ExpectedPeer string 93 ExpectedErr error 94 ExpectedErrMsg string 95 } 96 97 // Apply runs "Choose" on the peerList and validates the peer && error 98 func (a ChooseAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 99 ctx := a.InputContext 100 custom := ctx != nil 101 if ctx == nil { 102 ctx = context.Background() 103 } 104 if !custom || a.InputContextTimeout > 0 { 105 timeout := a.InputContextTimeout 106 if timeout == 0 { 107 timeout = testtime.Second 108 } 109 110 var cancel context.CancelFunc 111 ctx, cancel = context.WithTimeout(ctx, timeout) 112 defer cancel() 113 } 114 115 p, finish, err := pl.Choose(ctx, a.InputRequest) 116 if err == nil { 117 finish(nil) 118 } 119 120 if a.ExpectedErrMsg != "" { 121 assert.Nil(t, p) 122 assert.Contains(t, err.Error(), a.ExpectedErrMsg) 123 return 124 } 125 126 if a.ExpectedErr != nil { 127 // Note that we're not verifying anything about ExpectedPeer here because 128 // it being non-empty means that the test itself was invalid. If anything, 129 // that should cause a panic, not a test failure. But that validation can 130 // be done before you start asserting expectations. 131 assert.Nil(t, p) 132 assert.Equal(t, a.ExpectedErr, err) 133 return 134 } 135 136 if assert.NoError(t, err) && assert.NotNil(t, p) { 137 assert.Equal(t, a.ExpectedPeer, p.Identifier()) 138 } 139 } 140 141 // UpdateAction is an action for adding/removing multiple peers on the PeerList 142 type UpdateAction struct { 143 AddedPeerIDs []string 144 RemovedPeerIDs []string 145 ExpectedErr error 146 } 147 148 // Apply runs "Update" on the peer.Chooser after casting it to a peer.List 149 // and validates the error 150 func (a UpdateAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 151 list := pl.(peer.List) 152 153 added := make([]peer.Identifier, 0, len(a.AddedPeerIDs)) 154 for _, peerID := range a.AddedPeerIDs { 155 added = append(added, MockPeerIdentifier(peerID)) 156 } 157 158 removed := make([]peer.Identifier, 0, len(a.RemovedPeerIDs)) 159 for _, peerID := range a.RemovedPeerIDs { 160 removed = append(removed, MockPeerIdentifier(peerID)) 161 } 162 163 err := list.Update( 164 peer.ListUpdates{ 165 Additions: added, 166 Removals: removed, 167 }, 168 ) 169 assert.Equal(t, a.ExpectedErr, err) 170 } 171 172 // ConcurrentAction will run a series of actions in parallel 173 type ConcurrentAction struct { 174 Actions []PeerListAction 175 Wait time.Duration 176 } 177 178 // Apply runs all the ConcurrentAction's actions in goroutines with a delay of `Wait` 179 // between each action. Returns when all actions have finished executing 180 func (a ConcurrentAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 181 var wg sync.WaitGroup 182 183 wg.Add(len(a.Actions)) 184 for _, action := range a.Actions { 185 go func(ac PeerListAction) { 186 defer wg.Done() 187 ac.Apply(t, pl, deps) 188 }(action) 189 190 if a.Wait > 0 { 191 testtime.Sleep(a.Wait) 192 } 193 } 194 195 wg.Wait() 196 } 197 198 // NotifyStatusChangeAction will run the NotifyStatusChange function on a PeerList 199 // with a specified Peer after changing the peer's ConnectionStatus 200 type NotifyStatusChangeAction struct { 201 // PeerID is a unique identifier to the Peer we want use in the notification 202 PeerID string 203 204 // NewConnectionStatus is the new ConnectionStatus of the Peer 205 NewConnectionStatus peer.ConnectionStatus 206 207 // Unretained indicates that this notify occurs to a peer that has never been 208 // retained on the transport (to test edge cases). 209 Unretained bool 210 } 211 212 // Apply will run the NotifyStatusChanged function on the PeerList with the provided Peer 213 func (a NotifyStatusChangeAction) Apply(t *testing.T, pl peer.Chooser, deps ListActionDeps) { 214 plSub := pl.(peer.Subscriber) 215 216 if a.Unretained { 217 plSub.NotifyStatusChanged(MockPeerIdentifier(a.PeerID)) 218 return 219 } 220 221 deps.Peers[a.PeerID].PeerStatus.ConnectionStatus = a.NewConnectionStatus 222 223 plSub.NotifyStatusChanged(deps.Peers[a.PeerID]) 224 } 225 226 // ApplyPeerListActions runs all the PeerListActions on the PeerList 227 func ApplyPeerListActions(t *testing.T, pl peer.Chooser, actions []PeerListAction, deps ListActionDeps) { 228 for i, action := range actions { 229 t.Run(fmt.Sprintf("action #%d: %T", i, action), func(t *testing.T) { 230 action.Apply(t, pl, deps) 231 }) 232 } 233 }