github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/uniter/resolver/loop_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package resolver_test 5 6 import ( 7 "errors" 8 "time" 9 10 "github.com/juju/charm/v12/hooks" 11 "github.com/juju/loggo" 12 "github.com/juju/mutex/v2" 13 envtesting "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 gc "gopkg.in/check.v1" 16 17 "github.com/juju/juju/testcharms" 18 "github.com/juju/juju/testing" 19 coretesting "github.com/juju/juju/testing" 20 "github.com/juju/juju/worker/uniter/hook" 21 "github.com/juju/juju/worker/uniter/operation" 22 "github.com/juju/juju/worker/uniter/remotestate" 23 "github.com/juju/juju/worker/uniter/resolver" 24 ) 25 26 type LoopSuite struct { 27 testing.BaseSuite 28 29 resolver resolver.Resolver 30 watcher *mockRemoteStateWatcher 31 opFactory *mockOpFactory 32 executor *mockOpExecutor 33 charmURL string 34 charmDir string 35 abort chan struct{} 36 onIdle func() error 37 } 38 39 var _ = gc.Suite(&LoopSuite{}) 40 41 func (s *LoopSuite) SetUpTest(c *gc.C) { 42 s.BaseSuite.SetUpTest(c) 43 s.resolver = resolver.ResolverFunc(func(resolver.LocalState, remotestate.Snapshot, operation.Factory) (operation.Operation, error) { 44 return nil, resolver.ErrNoOperation 45 }) 46 s.watcher = &mockRemoteStateWatcher{ 47 changes: make(chan struct{}, 1), 48 } 49 s.opFactory = &mockOpFactory{} 50 s.executor = &mockOpExecutor{} 51 s.charmURL = "ch:trusty/mysql-1" 52 s.abort = make(chan struct{}) 53 } 54 55 func (s *LoopSuite) loop() (resolver.LocalState, error) { 56 localState := resolver.LocalState{ 57 CharmURL: s.charmURL, 58 } 59 err := resolver.Loop(resolver.LoopConfig{ 60 Resolver: s.resolver, 61 Factory: s.opFactory, 62 Watcher: s.watcher, 63 Executor: s.executor, 64 Abort: s.abort, 65 OnIdle: s.onIdle, 66 CharmDir: s.charmDir, 67 CharmDirGuard: &mockCharmDirGuard{}, 68 Logger: loggo.GetLogger("test"), 69 }, &localState) 70 return localState, err 71 } 72 73 func (s *LoopSuite) TestAbort(c *gc.C) { 74 close(s.abort) 75 _, err := s.loop() 76 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 77 } 78 79 func (s *LoopSuite) TestOnIdle(c *gc.C) { 80 onIdleCh := make(chan interface{}, 1) 81 s.onIdle = func() error { 82 onIdleCh <- nil 83 return nil 84 } 85 86 done := make(chan interface{}, 1) 87 go func() { 88 _, err := s.loop() 89 done <- err 90 }() 91 92 waitChannel(c, onIdleCh, "waiting for onIdle") 93 s.watcher.changes <- struct{}{} 94 waitChannel(c, onIdleCh, "waiting for onIdle") 95 close(s.abort) 96 97 err := waitChannel(c, done, "waiting for loop to exit") 98 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 99 100 select { 101 case <-onIdleCh: 102 c.Fatal("unexpected onIdle call") 103 default: 104 } 105 } 106 107 func (s *LoopSuite) TestOnIdleError(c *gc.C) { 108 s.onIdle = func() error { 109 return errors.New("onIdle failed") 110 } 111 close(s.abort) 112 _, err := s.loop() 113 c.Assert(err, gc.ErrorMatches, "onIdle failed") 114 } 115 116 func (s *LoopSuite) TestErrWaitingNoOnIdle(c *gc.C) { 117 var onIdleCalled bool 118 s.onIdle = func() error { 119 onIdleCalled = true 120 return nil 121 } 122 s.resolver = resolver.ResolverFunc(func( 123 _ resolver.LocalState, 124 _ remotestate.Snapshot, 125 _ operation.Factory, 126 ) (operation.Operation, error) { 127 return nil, resolver.ErrWaiting 128 }) 129 close(s.abort) 130 _, err := s.loop() 131 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 132 c.Assert(onIdleCalled, jc.IsFalse) 133 } 134 135 func (s *LoopSuite) TestInitialFinalLocalState(c *gc.C) { 136 var local resolver.LocalState 137 s.resolver = resolver.ResolverFunc(func( 138 l resolver.LocalState, 139 _ remotestate.Snapshot, 140 _ operation.Factory, 141 ) (operation.Operation, error) { 142 local = l 143 return nil, resolver.ErrNoOperation 144 }) 145 146 close(s.abort) 147 lastLocal, err := s.loop() 148 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 149 c.Assert(local, jc.DeepEquals, resolver.LocalState{ 150 CharmURL: s.charmURL, 151 }) 152 c.Assert(lastLocal, jc.DeepEquals, local) 153 } 154 155 func (s *LoopSuite) TestLoop(c *gc.C) { 156 var resolverCalls int 157 theOp := &mockOp{} 158 s.resolver = resolver.ResolverFunc(func( 159 _ resolver.LocalState, 160 _ remotestate.Snapshot, 161 _ operation.Factory, 162 ) (operation.Operation, error) { 163 resolverCalls++ 164 switch resolverCalls { 165 // On the first call, return an operation. 166 case 1: 167 return theOp, nil 168 // On the second call, simulate having 169 // no operations to perform, at which 170 // point we'll wait for a remote state 171 // change. 172 case 2: 173 s.watcher.changes <- struct{}{} 174 break 175 // On the third call, kill the loop. 176 case 3: 177 close(s.abort) 178 break 179 } 180 return nil, resolver.ErrNoOperation 181 }) 182 183 _, err := s.loop() 184 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 185 c.Assert(resolverCalls, gc.Equals, 3) 186 s.executor.CheckCallNames(c, "State", "State", "State", "Run", "State", "State") 187 188 runArgs := s.executor.Calls()[3].Args 189 c.Assert(runArgs, gc.HasLen, 2) 190 c.Assert(runArgs[0], gc.DeepEquals, theOp) 191 c.Assert(runArgs[1], gc.NotNil) 192 } 193 194 func (s *LoopSuite) TestLoopWithChange(c *gc.C) { 195 var resolverCalls int 196 theOp := &mockOp{} 197 s.resolver = resolver.ResolverFunc(func( 198 _ resolver.LocalState, 199 _ remotestate.Snapshot, 200 _ operation.Factory, 201 ) (operation.Operation, error) { 202 resolverCalls++ 203 switch resolverCalls { 204 // On the first call, return an operation. 205 case 1: 206 return theOp, nil 207 // On the second call, simulate having 208 // no operations to perform, at which 209 // point we'll wait for a remote state 210 // change. 211 case 2: 212 s.watcher.changes <- struct{}{} 213 break 214 case 3: 215 break 216 // On the fourth call, kill the loop. 217 case 4: 218 close(s.abort) 219 break 220 } 221 return nil, resolver.ErrNoOperation 222 }) 223 224 var remoteStateSnapshotChan <-chan remotestate.Snapshot 225 remoteStateSnapshotCount := 0 226 s.executor.run = func(op operation.Operation, rs <-chan remotestate.Snapshot) error { 227 remoteStateSnapshotChan = rs 228 for i := 0; i < 5; i++ { 229 // queue up a change to trigger snapshot channel. 230 s.watcher.changes <- struct{}{} 231 // wait for changes to propagate 232 select { 233 case _, ok := <-rs: 234 c.Assert(ok, jc.IsTrue) 235 remoteStateSnapshotCount++ 236 case <-time.After(testing.ShortWait): 237 c.Fatalf("timed out waiting for remote state snapshot") 238 panic("unreachable") 239 } 240 } 241 return nil 242 } 243 244 _, err := s.loop() 245 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 246 c.Assert(resolverCalls, gc.Equals, 4) 247 s.executor.CheckCallNames(c, "State", "State", "State", "Run", "State", "State", "State") 248 249 c.Assert(remoteStateSnapshotCount, gc.Equals, 5) 250 select { 251 case _, ok := <-remoteStateSnapshotChan: 252 c.Assert(ok, jc.IsTrue) 253 c.Fatalf("remote state snapshot channel fired more than once") 254 default: 255 } 256 257 runArgs := s.executor.Calls()[3].Args 258 c.Assert(runArgs, gc.HasLen, 2) 259 c.Assert(runArgs[0], gc.DeepEquals, theOp) 260 c.Assert(runArgs[1], gc.NotNil) 261 } 262 263 func (s *LoopSuite) TestRunFails(c *gc.C) { 264 s.executor.SetErrors(errors.New("run fails")) 265 s.resolver = resolver.ResolverFunc(func( 266 _ resolver.LocalState, 267 _ remotestate.Snapshot, 268 _ operation.Factory, 269 ) (operation.Operation, error) { 270 return mockOp{}, nil 271 }) 272 _, err := s.loop() 273 c.Assert(err, gc.ErrorMatches, "run fails") 274 } 275 276 func (s *LoopSuite) TestNextOpFails(c *gc.C) { 277 s.resolver = resolver.ResolverFunc(func( 278 _ resolver.LocalState, 279 _ remotestate.Snapshot, 280 _ operation.Factory, 281 ) (operation.Operation, error) { 282 return nil, errors.New("NextOp fails") 283 }) 284 _, err := s.loop() 285 c.Assert(err, gc.ErrorMatches, "NextOp fails") 286 } 287 288 func (s *LoopSuite) TestCheckCharmUpgradeUpgradeCharmHook(c *gc.C) { 289 s.executor = &mockOpExecutor{ 290 Executor: nil, 291 Stub: envtesting.Stub{}, 292 st: operation.State{ 293 Installed: true, 294 Kind: operation.Continue, 295 Hook: &hook.Info{Kind: hooks.UpgradeCharm}, 296 }, 297 run: nil, 298 } 299 s.testCheckCharmUpgradeDoesNothing(c) 300 } 301 302 func (s *LoopSuite) TestCheckCharmUpgradeSameURL(c *gc.C) { 303 s.executor = &mockOpExecutor{ 304 Executor: nil, 305 Stub: envtesting.Stub{}, 306 st: operation.State{ 307 Installed: true, 308 Kind: operation.Continue, 309 }, 310 run: nil, 311 } 312 s.watcher = &mockRemoteStateWatcher{ 313 snapshot: remotestate.Snapshot{ 314 CharmURL: s.charmURL, 315 }, 316 } 317 s.charmDir = testcharms.Repo.CharmDirPath("mysql") 318 s.testCheckCharmUpgradeDoesNothing(c) 319 } 320 321 func (s *LoopSuite) TestCheckCharmUpgradeNotInstalled(c *gc.C) { 322 s.executor = &mockOpExecutor{ 323 Executor: nil, 324 Stub: envtesting.Stub{}, 325 st: operation.State{ 326 Kind: operation.Continue, 327 }, 328 run: nil, 329 } 330 s.watcher = &mockRemoteStateWatcher{ 331 snapshot: remotestate.Snapshot{ 332 CharmURL: "ch:trusty/mysql-2", 333 }, 334 } 335 s.charmDir = testcharms.Repo.CharmDirPath("mysql") 336 s.testCheckCharmUpgradeDoesNothing(c) 337 } 338 339 func (s *LoopSuite) TestCheckCharmUpgradeIncorrectLXDProfile(c *gc.C) { 340 s.executor = &mockOpExecutor{ 341 Executor: nil, 342 Stub: envtesting.Stub{}, 343 st: operation.State{ 344 Installed: true, 345 Started: true, 346 Kind: operation.Continue, 347 }, 348 run: nil, 349 } 350 s.watcher = &mockRemoteStateWatcher{ 351 snapshot: remotestate.Snapshot{ 352 CharmURL: "ch:trusty/mysql-2", 353 CharmProfileRequired: true, 354 LXDProfileName: "juju-test-mysql-1", 355 }, 356 } 357 s.testCheckCharmUpgradeDoesNothing(c) 358 } 359 360 func (s *LoopSuite) testCheckCharmUpgradeDoesNothing(c *gc.C) { 361 s.resolver = resolver.ResolverFunc(func( 362 _ resolver.LocalState, 363 _ remotestate.Snapshot, 364 _ operation.Factory, 365 ) (operation.Operation, error) { 366 return nil, resolver.ErrWaiting 367 }) 368 close(s.abort) 369 _, err := s.loop() 370 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 371 372 // Run not called 373 c.Assert(s.executor.Calls(), gc.HasLen, 3) 374 s.executor.CheckCallNames(c, "State", "State", "State") 375 } 376 377 func (s *LoopSuite) TestCheckCharmUpgrade(c *gc.C) { 378 s.executor = &mockOpExecutor{ 379 Executor: nil, 380 Stub: envtesting.Stub{}, 381 st: operation.State{ 382 Installed: true, 383 Kind: operation.Continue, 384 }, 385 run: nil, 386 } 387 s.watcher = &mockRemoteStateWatcher{ 388 snapshot: remotestate.Snapshot{ 389 CharmURL: "ch:trusty/mysql-2", 390 }, 391 } 392 s.testCheckCharmUpgradeCallsRun(c, "Upgrade") 393 } 394 395 func (s *LoopSuite) TestCheckCharmUpgradeMissingCharmDir(c *gc.C) { 396 s.executor = &mockOpExecutor{ 397 Executor: nil, 398 Stub: envtesting.Stub{}, 399 st: operation.State{ 400 Installed: true, 401 Kind: operation.Continue, 402 }, 403 run: nil, 404 } 405 s.watcher = &mockRemoteStateWatcher{ 406 snapshot: remotestate.Snapshot{ 407 CharmURL: s.charmURL, 408 }, 409 } 410 s.testCheckCharmUpgradeCallsRun(c, "Upgrade") 411 } 412 413 func (s *LoopSuite) TestCheckCharmInstallMissingCharmDirInstallHookFail(c *gc.C) { 414 s.executor = &mockOpExecutor{ 415 Executor: nil, 416 Stub: envtesting.Stub{}, 417 st: operation.State{ 418 Installed: false, 419 Kind: operation.RunHook, 420 Step: operation.Pending, 421 Hook: &hook.Info{Kind: hooks.Install}, 422 }, 423 run: nil, 424 } 425 s.watcher = &mockRemoteStateWatcher{ 426 snapshot: remotestate.Snapshot{ 427 CharmURL: s.charmURL, 428 }, 429 } 430 s.testCheckCharmUpgradeCallsRun(c, "Install") 431 } 432 433 func (s *LoopSuite) TestCheckCharmUpgradeLXDProfile(c *gc.C) { 434 s.executor = &mockOpExecutor{ 435 Executor: nil, 436 Stub: envtesting.Stub{}, 437 st: operation.State{ 438 Installed: true, 439 Started: true, 440 Kind: operation.Continue, 441 }, 442 run: nil, 443 } 444 s.watcher = &mockRemoteStateWatcher{ 445 snapshot: remotestate.Snapshot{ 446 CharmURL: "ch:trusty/mysql-2", 447 CharmProfileRequired: true, 448 LXDProfileName: "juju-test-mysql-2", 449 }, 450 } 451 s.testCheckCharmUpgradeCallsRun(c, "Upgrade") 452 } 453 454 func (s *LoopSuite) testCheckCharmUpgradeCallsRun(c *gc.C, op string) { 455 s.opFactory = &mockOpFactory{ 456 Factory: nil, 457 Stub: envtesting.Stub{}, 458 op: mockOp{}, 459 } 460 s.resolver = resolver.ResolverFunc(func( 461 _ resolver.LocalState, 462 _ remotestate.Snapshot, 463 _ operation.Factory, 464 ) (operation.Operation, error) { 465 return nil, resolver.ErrWaiting 466 }) 467 close(s.abort) 468 _, err := s.loop() 469 c.Assert(err, gc.Equals, resolver.ErrLoopAborted) 470 471 // Run not called 472 c.Assert(s.executor.Calls(), gc.HasLen, 4) 473 s.executor.CheckCallNames(c, "State", "State", "Run", "State") 474 475 c.Assert(s.opFactory.Calls(), gc.HasLen, 1) 476 s.opFactory.CheckCallNames(c, "New"+op) 477 } 478 479 func (s *LoopSuite) TestCancelledLockAcquisitionCausesRestart(c *gc.C) { 480 s.executor = &mockOpExecutor{ 481 Executor: nil, 482 Stub: envtesting.Stub{}, 483 st: operation.State{ 484 Started: true, 485 Kind: operation.Continue, 486 }, 487 run: func(operation.Operation, <-chan remotestate.Snapshot) error { 488 return mutex.ErrCancelled 489 }, 490 } 491 492 s.resolver = resolver.ResolverFunc(func( 493 _ resolver.LocalState, 494 _ remotestate.Snapshot, 495 _ operation.Factory, 496 ) (operation.Operation, error) { 497 return &mockOp{}, nil 498 }) 499 500 _, err := s.loop() 501 c.Assert(err, gc.Equals, resolver.ErrRestart) 502 } 503 504 func waitChannel(c *gc.C, ch <-chan interface{}, activity string) interface{} { 505 select { 506 case v := <-ch: 507 return v 508 case <-time.After(coretesting.LongWait): 509 c.Fatalf("timed out " + activity) 510 panic("unreachable") 511 } 512 }