github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/lease/util_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lease_test 5 6 import ( 7 "context" 8 "sync" 9 "time" 10 11 "github.com/juju/clock/testclock" 12 "github.com/juju/errors" 13 "github.com/juju/names/v5" 14 jc "github.com/juju/testing/checkers" 15 gc "gopkg.in/check.v1" 16 17 "github.com/juju/juju/core/lease" 18 coretesting "github.com/juju/juju/testing" 19 ) 20 21 // Secretary implements lease.Secretary for testing purposes. 22 type Secretary struct{} 23 24 // CheckLease is part of the lease.Secretary interface. 25 func (Secretary) CheckLease(key lease.Key) error { 26 return checkName(key.Lease) 27 } 28 29 // CheckHolder is part of the lease.Secretary interface. 30 func (Secretary) CheckHolder(name string) error { 31 return checkName(name) 32 } 33 34 func checkName(name string) error { 35 if name == "INVALID" { 36 return errors.NotValidf("name") 37 } 38 return nil 39 } 40 41 // CheckDuration is part of the lease.Secretary interface. 42 func (Secretary) CheckDuration(duration time.Duration) error { 43 if duration != time.Minute { 44 return errors.NotValidf("time") 45 } 46 return nil 47 } 48 49 // Store implements corelease.Store for testing purposes. 50 type Store struct { 51 mu sync.Mutex 52 clock *testclock.Clock 53 leases map[lease.Key]lease.Info 54 expect []call 55 failed chan error 56 runningCalls int 57 done chan struct{} 58 } 59 60 // NewStore initializes and returns a new store configured to report 61 // the supplied leases and expect the supplied calls. 62 func NewStore(leases map[lease.Key]lease.Info, expect []call, clock *testclock.Clock) *Store { 63 if leases == nil { 64 leases = make(map[lease.Key]lease.Info) 65 } 66 done := make(chan struct{}) 67 if len(expect) == 0 { 68 close(done) 69 } 70 return &Store{ 71 leases: leases, 72 expect: expect, 73 done: done, 74 failed: make(chan error, 1000), 75 clock: clock, 76 } 77 } 78 79 // Wait will return when all expected calls have been made, or fail the test 80 // if they don't happen within a second. (You control the clock; your tests 81 // should pass in *way* less than 10 seconds of wall-clock time.) 82 func (store *Store) Wait(c *gc.C) { 83 select { 84 case <-store.done: 85 select { 86 case err := <-store.failed: 87 c.Errorf(err.Error()) 88 default: 89 } 90 case <-time.After(coretesting.LongWait): 91 store.mu.Lock() 92 remaining := make([]string, len(store.expect)) 93 for i := range store.expect { 94 remaining[i] = store.expect[i].method 95 } 96 store.mu.Unlock() 97 c.Errorf("Store test took way too long, still expecting %v", remaining) 98 } 99 } 100 101 func (store *Store) expireLeases() { 102 store.mu.Lock() 103 defer store.mu.Unlock() 104 for k, v := range store.leases { 105 if store.clock.Now().Before(v.Expiry) { 106 continue 107 } 108 delete(store.leases, k) 109 } 110 } 111 112 // Leases is part of the lease.Store interface. 113 func (store *Store) Leases(_ context.Context, keys ...lease.Key) (map[lease.Key]lease.Info, error) { 114 filter := make(map[lease.Key]bool) 115 filtering := len(keys) > 0 116 if filtering { 117 for _, key := range keys { 118 filter[key] = true 119 } 120 } 121 122 store.mu.Lock() 123 defer store.mu.Unlock() 124 result := make(map[lease.Key]lease.Info) 125 for k, v := range store.leases { 126 if filtering && !filter[k] { 127 continue 128 } 129 result[k] = v 130 } 131 return result, nil 132 } 133 134 // LeaseGroup is part of the lease.Store interface. 135 func (store *Store) LeaseGroup(ctx context.Context, namespace, modelUUID string) (map[lease.Key]lease.Info, error) { 136 leases, err := store.Leases(ctx) 137 if err != nil { 138 return nil, err 139 } 140 141 results := make(map[lease.Key]lease.Info) 142 for key, info := range leases { 143 if key.Namespace == namespace && key.ModelUUID == modelUUID { 144 results[key] = info 145 } 146 } 147 return results, nil 148 } 149 150 func (store *Store) closeIfEmpty() { 151 // This must be called with the lock held. 152 if store.runningCalls > 1 { 153 // The last one to leave should turn out the lights. 154 return 155 } 156 if len(store.expect) == 0 || len(store.failed) > 0 { 157 close(store.done) 158 } 159 } 160 161 // call implements the bulk of the lease.Store interface. 162 func (store *Store) call(method string, args []interface{}) error { 163 store.mu.Lock() 164 defer store.mu.Unlock() 165 166 store.runningCalls++ 167 defer func() { 168 store.runningCalls-- 169 }() 170 171 select { 172 case <-store.done: 173 err := errors.Errorf("Store method called after test complete: %s %v", method, args) 174 store.failed <- err 175 return err 176 default: 177 } 178 defer store.closeIfEmpty() 179 180 if len(store.expect) < 1 { 181 err := errors.Errorf("store.%s called but was not expected", method) 182 store.failed <- err 183 return err 184 } 185 expect := store.expect[0] 186 store.expect = store.expect[1:] 187 if expect.parallelCallback != nil { 188 store.mu.Unlock() 189 expect.parallelCallback(&store.mu, store.leases) 190 store.mu.Lock() 191 } 192 if expect.callback != nil { 193 expect.callback(store.leases) 194 } 195 196 if method == expect.method { 197 if ok, _ := jc.DeepEqual(args, expect.args); ok { 198 return expect.err 199 } 200 } 201 err := errors.Errorf("unexpected Store call:\n actual: %s %v\n expect: %s %v", 202 method, args, expect.method, expect.args, 203 ) 204 store.failed <- err 205 return err 206 } 207 208 // ClaimLease is part of the corelease.Store interface. 209 func (store *Store) ClaimLease(_ context.Context, key lease.Key, request lease.Request) error { 210 return store.call("ClaimLease", []interface{}{key, request}) 211 } 212 213 // ExtendLease is part of the corelease.Store interface. 214 func (store *Store) ExtendLease(_ context.Context, key lease.Key, request lease.Request) error { 215 return store.call("ExtendLease", []interface{}{key, request}) 216 } 217 218 func (store *Store) RevokeLease(_ context.Context, lease lease.Key, holder string) error { 219 return store.call("RevokeLease", []interface{}{lease, holder}) 220 } 221 222 // PinLease is part of the corelease.Store interface. 223 func (store *Store) PinLease(_ context.Context, key lease.Key, entity string) error { 224 return store.call("PinLease", []interface{}{key, entity}) 225 } 226 227 // UnpinLease is part of the corelease.Store interface. 228 func (store *Store) UnpinLease(_ context.Context, key lease.Key, entity string) error { 229 return store.call("UnpinLease", []interface{}{key, entity}) 230 } 231 232 func (store *Store) Pinned(_ context.Context) (map[lease.Key][]string, error) { 233 _ = store.call("Pinned", nil) 234 return map[lease.Key][]string{ 235 { 236 Namespace: "namespace", 237 ModelUUID: "modelUUID", 238 Lease: "redis", 239 }: {names.NewMachineTag("0").String()}, 240 { 241 Namespace: "ignored-namespace", 242 ModelUUID: "ignored modelUUID", 243 Lease: "lolwut", 244 }: {names.NewMachineTag("666").String()}, 245 }, nil 246 } 247 248 // call defines a expected method call on a Store; it encodes: 249 type call struct { 250 251 // method is the name of the method. 252 method string 253 254 // args is the expected arguments. 255 args []interface{} 256 257 // err is the error to return. 258 err error 259 260 // callback, if non-nil, will be passed the internal leases dict; for 261 // modification, if desired. Otherwise you can use it to, e.g., assert 262 // clock time. 263 callback func(leases map[lease.Key]lease.Info) 264 265 // parallelCallback is like callback, but is also passed the 266 // lock. It's for testing calls that happen in parallel, where one 267 // might take longer than another. Any update to the leases dict 268 // must only happen while the lock is held. 269 parallelCallback func(mu *sync.Mutex, leases map[lease.Key]lease.Info) 270 } 271 272 func key(args ...string) lease.Key { 273 result := lease.Key{ 274 Namespace: "namespace", 275 ModelUUID: "modelUUID", 276 Lease: "lease", 277 } 278 if len(args) == 1 { 279 result.Lease = args[0] 280 } else if len(args) == 3 { 281 result.Namespace = args[0] 282 result.ModelUUID = args[1] 283 result.Lease = args[2] 284 } 285 return result 286 }