launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/state/megawatcher_internal_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "launchpad.net/errgo/errors" 9 "reflect" 10 "sort" 11 "time" 12 13 "labix.org/v2/mgo" 14 gc "launchpad.net/gocheck" 15 16 "launchpad.net/juju-core/charm" 17 "launchpad.net/juju-core/constraints" 18 "launchpad.net/juju-core/instance" 19 "launchpad.net/juju-core/state/api/params" 20 "launchpad.net/juju-core/state/multiwatcher" 21 "launchpad.net/juju-core/state/watcher" 22 "launchpad.net/juju-core/testing" 23 "launchpad.net/juju-core/testing/testbase" 24 ) 25 26 var dottedConfig = ` 27 options: 28 key.dotted: {default: My Key, description: Desc, type: string} 29 ` 30 31 type storeManagerStateSuite struct { 32 testbase.LoggingSuite 33 testing.MgoSuite 34 State *State 35 } 36 37 func (s *storeManagerStateSuite) SetUpSuite(c *gc.C) { 38 s.LoggingSuite.SetUpSuite(c) 39 s.MgoSuite.SetUpSuite(c) 40 } 41 42 func (s *storeManagerStateSuite) TearDownSuite(c *gc.C) { 43 s.MgoSuite.TearDownSuite(c) 44 s.LoggingSuite.TearDownSuite(c) 45 } 46 47 func (s *storeManagerStateSuite) SetUpTest(c *gc.C) { 48 s.LoggingSuite.SetUpTest(c) 49 s.MgoSuite.SetUpTest(c) 50 s.State = TestingInitialize(c, nil) 51 s.State.AddUser("admin", "pass") 52 } 53 54 func (s *storeManagerStateSuite) TearDownTest(c *gc.C) { 55 s.State.Close() 56 s.MgoSuite.TearDownTest(c) 57 s.LoggingSuite.TearDownTest(c) 58 } 59 60 func (s *storeManagerStateSuite) Reset(c *gc.C) { 61 s.TearDownTest(c) 62 s.SetUpTest(c) 63 } 64 65 var _ = gc.Suite(&storeManagerStateSuite{}) 66 67 // setUpScenario adds some entities to the state so that 68 // we can check that they all get pulled in by 69 // allWatcherStateBacking.getAll. 70 func (s *storeManagerStateSuite) setUpScenario(c *gc.C) (entities entityInfoSlice) { 71 add := func(e params.EntityInfo) { 72 entities = append(entities, e) 73 } 74 m, err := s.State.AddMachine("quantal", JobManageEnviron) 75 c.Assert(err, gc.IsNil) 76 c.Assert(m.Tag(), gc.Equals, "machine-0") 77 err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil) 78 c.Assert(err, gc.IsNil) 79 add(¶ms.MachineInfo{ 80 Id: "0", 81 InstanceId: "i-machine-0", 82 Status: params.StatusPending, 83 }) 84 85 wordpress := AddTestingService(c, s.State, "wordpress", AddTestingCharm(c, s.State, "wordpress")) 86 err = wordpress.SetExposed() 87 c.Assert(err, gc.IsNil) 88 err = wordpress.SetMinUnits(3) 89 c.Assert(err, gc.IsNil) 90 err = wordpress.SetConstraints(constraints.MustParse("mem=100M")) 91 c.Assert(err, gc.IsNil) 92 setServiceConfigAttr(c, wordpress, "blog-title", "boring") 93 add(¶ms.ServiceInfo{ 94 Name: "wordpress", 95 Exposed: true, 96 CharmURL: serviceCharmURL(wordpress).String(), 97 OwnerTag: "user-admin", 98 Life: params.Life(Alive.String()), 99 MinUnits: 3, 100 Constraints: constraints.MustParse("mem=100M"), 101 Config: charm.Settings{"blog-title": "boring"}, 102 }) 103 pairs := map[string]string{"x": "12", "y": "99"} 104 err = wordpress.SetAnnotations(pairs) 105 c.Assert(err, gc.IsNil) 106 add(¶ms.AnnotationInfo{ 107 Tag: "service-wordpress", 108 Annotations: pairs, 109 }) 110 111 logging := AddTestingService(c, s.State, "logging", AddTestingCharm(c, s.State, "logging")) 112 add(¶ms.ServiceInfo{ 113 Name: "logging", 114 CharmURL: serviceCharmURL(logging).String(), 115 OwnerTag: "user-admin", 116 Life: params.Life(Alive.String()), 117 Config: charm.Settings{}, 118 }) 119 120 eps, err := s.State.InferEndpoints([]string{"logging", "wordpress"}) 121 c.Assert(err, gc.IsNil) 122 rel, err := s.State.AddRelation(eps...) 123 c.Assert(err, gc.IsNil) 124 add(¶ms.RelationInfo{ 125 Key: "logging:logging-directory wordpress:logging-dir", 126 Id: rel.Id(), 127 Endpoints: []params.Endpoint{ 128 {ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}}, 129 {ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, 130 }) 131 132 for i := 0; i < 2; i++ { 133 wu, err := wordpress.AddUnit() 134 c.Assert(err, gc.IsNil) 135 c.Assert(wu.Tag(), gc.Equals, fmt.Sprintf("unit-wordpress-%d", i)) 136 137 m, err := s.State.AddMachine("quantal", JobHostUnits) 138 c.Assert(err, gc.IsNil) 139 c.Assert(m.Tag(), gc.Equals, fmt.Sprintf("machine-%d", i+1)) 140 141 add(¶ms.UnitInfo{ 142 Name: fmt.Sprintf("wordpress/%d", i), 143 Service: wordpress.Name(), 144 Series: m.Series(), 145 MachineId: m.Id(), 146 Ports: []instance.Port{}, 147 Status: params.StatusPending, 148 }) 149 pairs := map[string]string{"name": fmt.Sprintf("bar %d", i)} 150 err = wu.SetAnnotations(pairs) 151 c.Assert(err, gc.IsNil) 152 add(¶ms.AnnotationInfo{ 153 Tag: fmt.Sprintf("unit-wordpress-%d", i), 154 Annotations: pairs, 155 }) 156 157 err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil) 158 c.Assert(err, gc.IsNil) 159 err = m.SetStatus(params.StatusError, m.Tag(), nil) 160 c.Assert(err, gc.IsNil) 161 add(¶ms.MachineInfo{ 162 Id: fmt.Sprint(i + 1), 163 InstanceId: "i-" + m.Tag(), 164 Status: params.StatusError, 165 StatusInfo: m.Tag(), 166 }) 167 err = wu.AssignToMachine(m) 168 c.Assert(err, gc.IsNil) 169 170 deployer, ok := wu.DeployerTag() 171 c.Assert(ok, gc.Equals, true) 172 c.Assert(deployer, gc.Equals, fmt.Sprintf("machine-%d", i+1)) 173 174 wru, err := rel.Unit(wu) 175 c.Assert(err, gc.IsNil) 176 177 // Create the subordinate unit as a side-effect of entering 178 // scope in the principal's relation-unit. 179 err = wru.EnterScope(nil) 180 c.Assert(err, gc.IsNil) 181 182 lu, err := s.State.Unit(fmt.Sprintf("logging/%d", i)) 183 c.Assert(err, gc.IsNil) 184 c.Assert(lu.IsPrincipal(), gc.Equals, false) 185 deployer, ok = lu.DeployerTag() 186 c.Assert(ok, gc.Equals, true) 187 c.Assert(deployer, gc.Equals, fmt.Sprintf("unit-wordpress-%d", i)) 188 add(¶ms.UnitInfo{ 189 Name: fmt.Sprintf("logging/%d", i), 190 Service: "logging", 191 Series: "quantal", 192 Ports: []instance.Port{}, 193 Status: params.StatusPending, 194 }) 195 } 196 return 197 } 198 199 func serviceCharmURL(svc *Service) *charm.URL { 200 url, _ := svc.CharmURL() 201 return url 202 } 203 204 func assertEntitiesEqual(c *gc.C, got, want []params.EntityInfo) { 205 if len(got) == 0 { 206 got = nil 207 } 208 if len(want) == 0 { 209 want = nil 210 } 211 if reflect.DeepEqual(got, want) { 212 return 213 } 214 c.Errorf("entity mismatch; got len %d; want %d", len(got), len(want)) 215 c.Logf("got:") 216 for _, e := range got { 217 c.Logf("\t%T %#v", e, e) 218 } 219 c.Logf("expected:") 220 for _, e := range want { 221 c.Logf("\t%T %#v", e, e) 222 } 223 c.FailNow() 224 } 225 226 func (s *storeManagerStateSuite) TestStateBackingGetAll(c *gc.C) { 227 expectEntities := s.setUpScenario(c) 228 b := newAllWatcherStateBacking(s.State) 229 all := multiwatcher.NewStore() 230 err := b.GetAll(all) 231 c.Assert(err, gc.IsNil) 232 var gotEntities entityInfoSlice = all.All() 233 sort.Sort(gotEntities) 234 sort.Sort(expectEntities) 235 assertEntitiesEqual(c, gotEntities, expectEntities) 236 } 237 238 var allWatcherChangedTests = []struct { 239 about string 240 add []params.EntityInfo 241 setUp func(c *gc.C, st *State) 242 change watcher.Change 243 expectContents []params.EntityInfo 244 }{ 245 // Machine changes 246 { 247 about: "no machine in state, no machine in store -> do nothing", 248 setUp: func(*gc.C, *State) {}, 249 change: watcher.Change{ 250 C: "machines", 251 Id: "1", 252 }, 253 }, { 254 about: "machine is removed if it's not in backing", 255 add: []params.EntityInfo{¶ms.MachineInfo{Id: "1"}}, 256 setUp: func(*gc.C, *State) {}, 257 change: watcher.Change{ 258 C: "machines", 259 Id: "1", 260 }, 261 }, { 262 about: "machine is added if it's in backing but not in Store", 263 setUp: func(c *gc.C, st *State) { 264 m, err := st.AddMachine("quantal", JobHostUnits) 265 c.Assert(err, gc.IsNil) 266 err = m.SetStatus(params.StatusError, "failure", nil) 267 c.Assert(err, gc.IsNil) 268 }, 269 change: watcher.Change{ 270 C: "machines", 271 Id: "0", 272 }, 273 expectContents: []params.EntityInfo{ 274 ¶ms.MachineInfo{ 275 Id: "0", 276 Status: params.StatusError, 277 StatusInfo: "failure", 278 }, 279 }, 280 }, 281 // Machine status changes 282 { 283 about: "machine is updated if it's in backing and in Store", 284 add: []params.EntityInfo{ 285 ¶ms.MachineInfo{ 286 Id: "0", 287 Status: params.StatusError, 288 StatusInfo: "another failure", 289 }, 290 }, 291 setUp: func(c *gc.C, st *State) { 292 m, err := st.AddMachine("quantal", JobManageEnviron) 293 c.Assert(err, gc.IsNil) 294 err = m.SetProvisioned("i-0", "bootstrap_nonce", nil) 295 c.Assert(err, gc.IsNil) 296 }, 297 change: watcher.Change{ 298 C: "machines", 299 Id: "0", 300 }, 301 expectContents: []params.EntityInfo{ 302 ¶ms.MachineInfo{ 303 Id: "0", 304 InstanceId: "i-0", 305 Status: params.StatusError, 306 StatusInfo: "another failure", 307 }, 308 }, 309 }, 310 // Unit changes 311 { 312 about: "no unit in state, no unit in store -> do nothing", 313 setUp: func(c *gc.C, st *State) {}, 314 change: watcher.Change{ 315 C: "units", 316 Id: "1", 317 }, 318 }, { 319 about: "unit is removed if it's not in backing", 320 add: []params.EntityInfo{¶ms.UnitInfo{Name: "wordpress/1"}}, 321 setUp: func(*gc.C, *State) {}, 322 change: watcher.Change{ 323 C: "units", 324 Id: "wordpress/1", 325 }, 326 }, { 327 about: "unit is added if it's in backing but not in Store", 328 setUp: func(c *gc.C, st *State) { 329 wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 330 u, err := wordpress.AddUnit() 331 c.Assert(err, gc.IsNil) 332 err = u.SetPublicAddress("public") 333 c.Assert(err, gc.IsNil) 334 err = u.SetPrivateAddress("private") 335 c.Assert(err, gc.IsNil) 336 err = u.OpenPort("tcp", 12345) 337 c.Assert(err, gc.IsNil) 338 m, err := st.AddMachine("quantal", JobHostUnits) 339 c.Assert(err, gc.IsNil) 340 err = u.AssignToMachine(m) 341 c.Assert(err, gc.IsNil) 342 err = u.SetStatus(params.StatusError, "failure", nil) 343 c.Assert(err, gc.IsNil) 344 }, 345 change: watcher.Change{ 346 C: "units", 347 Id: "wordpress/0", 348 }, 349 expectContents: []params.EntityInfo{ 350 ¶ms.UnitInfo{ 351 Name: "wordpress/0", 352 Service: "wordpress", 353 Series: "quantal", 354 PublicAddress: "public", 355 PrivateAddress: "private", 356 MachineId: "0", 357 Ports: []instance.Port{{"tcp", 12345}}, 358 Status: params.StatusError, 359 StatusInfo: "failure", 360 }, 361 }, 362 }, { 363 about: "unit is updated if it's in backing and in multiwatcher.Store", 364 add: []params.EntityInfo{¶ms.UnitInfo{ 365 Name: "wordpress/0", 366 Status: params.StatusError, 367 StatusInfo: "another failure", 368 }}, 369 setUp: func(c *gc.C, st *State) { 370 wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 371 u, err := wordpress.AddUnit() 372 c.Assert(err, gc.IsNil) 373 err = u.SetPublicAddress("public") 374 c.Assert(err, gc.IsNil) 375 err = u.OpenPort("udp", 17070) 376 c.Assert(err, gc.IsNil) 377 }, 378 change: watcher.Change{ 379 C: "units", 380 Id: "wordpress/0", 381 }, 382 expectContents: []params.EntityInfo{ 383 ¶ms.UnitInfo{ 384 Name: "wordpress/0", 385 Service: "wordpress", 386 Series: "quantal", 387 PublicAddress: "public", 388 Ports: []instance.Port{{"udp", 17070}}, 389 Status: params.StatusError, 390 StatusInfo: "another failure", 391 }, 392 }, 393 }, 394 // Service changes 395 { 396 about: "no service in state, no service in store -> do nothing", 397 setUp: func(c *gc.C, st *State) {}, 398 change: watcher.Change{ 399 C: "services", 400 Id: "wordpress", 401 }, 402 }, { 403 about: "service is removed if it's not in backing", 404 add: []params.EntityInfo{¶ms.ServiceInfo{Name: "wordpress"}}, 405 setUp: func(*gc.C, *State) {}, 406 change: watcher.Change{ 407 C: "services", 408 Id: "wordpress", 409 }, 410 }, { 411 about: "service is added if it's in backing but not in Store", 412 setUp: func(c *gc.C, st *State) { 413 wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 414 err := wordpress.SetExposed() 415 c.Assert(err, gc.IsNil) 416 err = wordpress.SetMinUnits(42) 417 c.Assert(err, gc.IsNil) 418 }, 419 change: watcher.Change{ 420 C: "services", 421 Id: "wordpress", 422 }, 423 expectContents: []params.EntityInfo{ 424 ¶ms.ServiceInfo{ 425 Name: "wordpress", 426 Exposed: true, 427 CharmURL: "local:quantal/quantal-wordpress-3", 428 OwnerTag: "user-admin", 429 Life: params.Life(Alive.String()), 430 MinUnits: 42, 431 Config: charm.Settings{}, 432 }, 433 }, 434 }, { 435 about: "service is updated if it's in backing and in multiwatcher.Store", 436 add: []params.EntityInfo{¶ms.ServiceInfo{ 437 Name: "wordpress", 438 Exposed: true, 439 CharmURL: "local:quantal/quantal-wordpress-3", 440 MinUnits: 47, 441 Constraints: constraints.MustParse("mem=99M"), 442 Config: charm.Settings{"blog-title": "boring"}, 443 }}, 444 setUp: func(c *gc.C, st *State) { 445 svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 446 setServiceConfigAttr(c, svc, "blog-title", "boring") 447 }, 448 change: watcher.Change{ 449 C: "services", 450 Id: "wordpress", 451 }, 452 expectContents: []params.EntityInfo{ 453 ¶ms.ServiceInfo{ 454 Name: "wordpress", 455 CharmURL: "local:quantal/quantal-wordpress-3", 456 OwnerTag: "user-admin", 457 Life: params.Life(Alive.String()), 458 Constraints: constraints.MustParse("mem=99M"), 459 Config: charm.Settings{"blog-title": "boring"}, 460 }, 461 }, 462 }, { 463 about: "service re-reads config when charm URL changes", 464 add: []params.EntityInfo{¶ms.ServiceInfo{ 465 Name: "wordpress", 466 // Note: CharmURL has a different revision number from 467 // the wordpress revision in the testing repo. 468 CharmURL: "local:quantal/quantal-wordpress-2", 469 Config: charm.Settings{"foo": "bar"}, 470 }}, 471 setUp: func(c *gc.C, st *State) { 472 svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 473 setServiceConfigAttr(c, svc, "blog-title", "boring") 474 }, 475 change: watcher.Change{ 476 C: "services", 477 Id: "wordpress", 478 }, 479 expectContents: []params.EntityInfo{ 480 ¶ms.ServiceInfo{ 481 Name: "wordpress", 482 CharmURL: "local:quantal/quantal-wordpress-3", 483 OwnerTag: "user-admin", 484 Life: params.Life(Alive.String()), 485 Config: charm.Settings{"blog-title": "boring"}, 486 }, 487 }, 488 }, 489 // Relation changes 490 { 491 about: "no relation in state, no service in store -> do nothing", 492 setUp: func(c *gc.C, st *State) {}, 493 change: watcher.Change{ 494 C: "relations", 495 Id: "logging:logging-directory wordpress:logging-dir", 496 }, 497 }, { 498 about: "relation is removed if it's not in backing", 499 add: []params.EntityInfo{¶ms.RelationInfo{Key: "logging:logging-directory wordpress:logging-dir"}}, 500 setUp: func(*gc.C, *State) {}, 501 change: watcher.Change{ 502 C: "relations", 503 Id: "logging:logging-directory wordpress:logging-dir", 504 }, 505 }, { 506 about: "relation is added if it's in backing but not in Store", 507 setUp: func(c *gc.C, st *State) { 508 AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 509 510 AddTestingService(c, st, "logging", AddTestingCharm(c, st, "logging")) 511 eps, err := st.InferEndpoints([]string{"logging", "wordpress"}) 512 c.Assert(err, gc.IsNil) 513 _, err = st.AddRelation(eps...) 514 c.Assert(err, gc.IsNil) 515 }, 516 change: watcher.Change{ 517 C: "relations", 518 Id: "logging:logging-directory wordpress:logging-dir", 519 }, 520 expectContents: []params.EntityInfo{ 521 ¶ms.RelationInfo{ 522 Key: "logging:logging-directory wordpress:logging-dir", 523 Endpoints: []params.Endpoint{ 524 {ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}}, 525 {ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, 526 }, 527 }, 528 }, 529 // Annotation changes 530 { 531 about: "no annotation in state, no annotation in store -> do nothing", 532 setUp: func(c *gc.C, st *State) {}, 533 change: watcher.Change{ 534 C: "relations", 535 Id: "m#0", 536 }, 537 }, { 538 about: "annotation is removed if it's not in backing", 539 add: []params.EntityInfo{¶ms.AnnotationInfo{Tag: "machine-0"}}, 540 setUp: func(*gc.C, *State) {}, 541 change: watcher.Change{ 542 C: "annotations", 543 Id: "m#0", 544 }, 545 }, { 546 about: "annotation is added if it's in backing but not in Store", 547 setUp: func(c *gc.C, st *State) { 548 m, err := st.AddMachine("quantal", JobHostUnits) 549 c.Assert(err, gc.IsNil) 550 err = m.SetAnnotations(map[string]string{"foo": "bar", "arble": "baz"}) 551 c.Assert(err, gc.IsNil) 552 }, 553 change: watcher.Change{ 554 C: "annotations", 555 Id: "m#0", 556 }, 557 expectContents: []params.EntityInfo{ 558 ¶ms.AnnotationInfo{ 559 Tag: "machine-0", 560 Annotations: map[string]string{"foo": "bar", "arble": "baz"}, 561 }, 562 }, 563 }, { 564 about: "annotation is updated if it's in backing and in multiwatcher.Store", 565 add: []params.EntityInfo{¶ms.AnnotationInfo{ 566 Tag: "machine-0", 567 Annotations: map[string]string{ 568 "arble": "baz", 569 "foo": "bar", 570 "pretty": "polly", 571 }, 572 }}, 573 setUp: func(c *gc.C, st *State) { 574 m, err := st.AddMachine("quantal", JobHostUnits) 575 c.Assert(err, gc.IsNil) 576 err = m.SetAnnotations(map[string]string{ 577 "arble": "khroomph", 578 "pretty": "", 579 "new": "attr", 580 }) 581 c.Assert(err, gc.IsNil) 582 }, 583 change: watcher.Change{ 584 C: "annotations", 585 Id: "m#0", 586 }, 587 expectContents: []params.EntityInfo{ 588 ¶ms.AnnotationInfo{ 589 Tag: "machine-0", 590 Annotations: map[string]string{ 591 "arble": "khroomph", 592 "new": "attr", 593 }, 594 }, 595 }, 596 }, 597 // Unit status changes 598 { 599 about: "no unit in state -> do nothing", 600 setUp: func(c *gc.C, st *State) {}, 601 change: watcher.Change{ 602 C: "statuses", 603 Id: "u#wordpress/0", 604 }, 605 }, { 606 about: "no change if status is not in backing", 607 add: []params.EntityInfo{¶ms.UnitInfo{ 608 Name: "wordpress/0", 609 Status: params.StatusError, 610 StatusInfo: "failure", 611 }}, 612 setUp: func(*gc.C, *State) {}, 613 change: watcher.Change{ 614 C: "statuses", 615 Id: "u#wordpress/0", 616 }, 617 expectContents: []params.EntityInfo{ 618 ¶ms.UnitInfo{ 619 Name: "wordpress/0", 620 Status: params.StatusError, 621 StatusInfo: "failure", 622 }, 623 }, 624 }, { 625 about: "status is changed if the unit exists in the store", 626 add: []params.EntityInfo{¶ms.UnitInfo{ 627 Name: "wordpress/0", 628 Status: params.StatusError, 629 StatusInfo: "failure", 630 }}, 631 setUp: func(c *gc.C, st *State) { 632 wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 633 u, err := wordpress.AddUnit() 634 c.Assert(err, gc.IsNil) 635 err = u.SetStatus(params.StatusStarted, "", nil) 636 c.Assert(err, gc.IsNil) 637 }, 638 change: watcher.Change{ 639 C: "statuses", 640 Id: "u#wordpress/0", 641 }, 642 expectContents: []params.EntityInfo{ 643 ¶ms.UnitInfo{ 644 Name: "wordpress/0", 645 Status: params.StatusStarted, 646 StatusData: params.StatusData{}, 647 }, 648 }, 649 }, { 650 about: "status is changed with additional status data", 651 add: []params.EntityInfo{¶ms.UnitInfo{ 652 Name: "wordpress/0", 653 Status: params.StatusStarted, 654 }}, 655 setUp: func(c *gc.C, st *State) { 656 wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 657 u, err := wordpress.AddUnit() 658 c.Assert(err, gc.IsNil) 659 err = u.SetStatus(params.StatusError, "hook error", params.StatusData{ 660 "1st-key": "one", 661 "2nd-key": 2, 662 "3rd-key": true, 663 }) 664 c.Assert(err, gc.IsNil) 665 }, 666 change: watcher.Change{ 667 C: "statuses", 668 Id: "u#wordpress/0", 669 }, 670 expectContents: []params.EntityInfo{ 671 ¶ms.UnitInfo{ 672 Name: "wordpress/0", 673 Status: params.StatusError, 674 StatusInfo: "hook error", 675 StatusData: params.StatusData{ 676 "1st-key": "one", 677 "2nd-key": 2, 678 "3rd-key": true, 679 }, 680 }, 681 }, 682 }, 683 // Machine status changes 684 { 685 about: "no machine in state -> do nothing", 686 setUp: func(c *gc.C, st *State) {}, 687 change: watcher.Change{ 688 C: "statuses", 689 Id: "m#0", 690 }, 691 }, { 692 about: "no change if status is not in backing", 693 add: []params.EntityInfo{¶ms.MachineInfo{ 694 Id: "0", 695 Status: params.StatusError, 696 StatusInfo: "failure", 697 }}, 698 setUp: func(*gc.C, *State) {}, 699 change: watcher.Change{ 700 C: "statuses", 701 Id: "m#0", 702 }, 703 expectContents: []params.EntityInfo{¶ms.MachineInfo{ 704 Id: "0", 705 Status: params.StatusError, 706 StatusInfo: "failure", 707 }}, 708 }, { 709 about: "status is changed if the machine exists in the store", 710 add: []params.EntityInfo{¶ms.MachineInfo{ 711 Id: "0", 712 Status: params.StatusError, 713 StatusInfo: "failure", 714 }}, 715 setUp: func(c *gc.C, st *State) { 716 m, err := st.AddMachine("quantal", JobHostUnits) 717 c.Assert(err, gc.IsNil) 718 err = m.SetStatus(params.StatusStarted, "", nil) 719 c.Assert(err, gc.IsNil) 720 }, 721 change: watcher.Change{ 722 C: "statuses", 723 Id: "m#0", 724 }, 725 expectContents: []params.EntityInfo{ 726 ¶ms.MachineInfo{ 727 Id: "0", 728 Status: params.StatusStarted, 729 StatusData: params.StatusData{}, 730 }, 731 }, 732 }, 733 // Service constraints changes 734 { 735 about: "no service in state -> do nothing", 736 setUp: func(c *gc.C, st *State) {}, 737 change: watcher.Change{ 738 C: "constraints", 739 Id: "s#wordpress", 740 }, 741 }, { 742 about: "no change if service is not in backing", 743 add: []params.EntityInfo{¶ms.ServiceInfo{ 744 Name: "wordpress", 745 Constraints: constraints.MustParse("mem=99M"), 746 }}, 747 setUp: func(*gc.C, *State) {}, 748 change: watcher.Change{ 749 C: "constraints", 750 Id: "s#wordpress", 751 }, 752 expectContents: []params.EntityInfo{¶ms.ServiceInfo{ 753 Name: "wordpress", 754 Constraints: constraints.MustParse("mem=99M"), 755 }}, 756 }, { 757 about: "status is changed if the service exists in the store", 758 add: []params.EntityInfo{¶ms.ServiceInfo{ 759 Name: "wordpress", 760 Constraints: constraints.MustParse("mem=99M cpu-cores=2 cpu-power=4"), 761 }}, 762 setUp: func(c *gc.C, st *State) { 763 svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 764 err := svc.SetConstraints(constraints.MustParse("mem=4G cpu-cores= arch=amd64")) 765 c.Assert(err, gc.IsNil) 766 }, 767 change: watcher.Change{ 768 C: "constraints", 769 Id: "s#wordpress", 770 }, 771 expectContents: []params.EntityInfo{ 772 ¶ms.ServiceInfo{ 773 Name: "wordpress", 774 Constraints: constraints.MustParse("mem=4G cpu-cores= arch=amd64"), 775 }, 776 }, 777 }, 778 // Service config changes. 779 { 780 about: "no service in state -> do nothing", 781 setUp: func(c *gc.C, st *State) {}, 782 change: watcher.Change{ 783 C: "settings", 784 Id: "s#wordpress#local:quantal/quantal-wordpress-3", 785 }, 786 }, { 787 about: "no change if service is not in backing", 788 add: []params.EntityInfo{¶ms.ServiceInfo{ 789 Name: "wordpress", 790 CharmURL: "local:quantal/quantal-wordpress-3", 791 }}, 792 setUp: func(*gc.C, *State) {}, 793 change: watcher.Change{ 794 C: "settings", 795 Id: "s#wordpress#local:quantal/quantal-wordpress-3", 796 }, 797 expectContents: []params.EntityInfo{¶ms.ServiceInfo{ 798 Name: "wordpress", 799 CharmURL: "local:quantal/quantal-wordpress-3", 800 }}, 801 }, { 802 about: "service config is changed if service exists in the store with the same URL", 803 add: []params.EntityInfo{¶ms.ServiceInfo{ 804 Name: "wordpress", 805 CharmURL: "local:quantal/quantal-wordpress-3", 806 Config: charm.Settings{"foo": "bar"}, 807 }}, 808 setUp: func(c *gc.C, st *State) { 809 svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 810 setServiceConfigAttr(c, svc, "blog-title", "foo") 811 }, 812 change: watcher.Change{ 813 C: "settings", 814 Id: "s#wordpress#local:quantal/quantal-wordpress-3", 815 }, 816 expectContents: []params.EntityInfo{ 817 ¶ms.ServiceInfo{ 818 Name: "wordpress", 819 CharmURL: "local:quantal/quantal-wordpress-3", 820 Config: charm.Settings{"blog-title": "foo"}, 821 }, 822 }, 823 }, { 824 about: "service config is unescaped when reading from the backing store", 825 add: []params.EntityInfo{¶ms.ServiceInfo{ 826 Name: "wordpress", 827 CharmURL: "local:quantal/quantal-wordpress-3", 828 Config: charm.Settings{"key.dotted": "bar"}, 829 }}, 830 setUp: func(c *gc.C, st *State) { 831 testCharm := AddCustomCharm( 832 c, st, "wordpress", 833 "config.yaml", dottedConfig, 834 "quantal", 3) 835 svc := AddTestingService(c, st, "wordpress", testCharm) 836 setServiceConfigAttr(c, svc, "key.dotted", "foo") 837 }, 838 change: watcher.Change{ 839 C: "settings", 840 Id: "s#wordpress#local:quantal/quantal-wordpress-3", 841 }, 842 expectContents: []params.EntityInfo{ 843 ¶ms.ServiceInfo{ 844 Name: "wordpress", 845 CharmURL: "local:quantal/quantal-wordpress-3", 846 Config: charm.Settings{"key.dotted": "foo"}, 847 }, 848 }, 849 }, { 850 about: "service config is unchanged if service exists in the store with a different URL", 851 add: []params.EntityInfo{¶ms.ServiceInfo{ 852 Name: "wordpress", 853 CharmURL: "local:quantal/quantal-wordpress-2", // Note different revno. 854 Config: charm.Settings{"foo": "bar"}, 855 }}, 856 setUp: func(c *gc.C, st *State) { 857 svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 858 setServiceConfigAttr(c, svc, "blog-title", "foo") 859 }, 860 change: watcher.Change{ 861 C: "settings", 862 Id: "s#wordpress#local:quantal/quantal-wordpress-3", 863 }, 864 expectContents: []params.EntityInfo{ 865 ¶ms.ServiceInfo{ 866 Name: "wordpress", 867 CharmURL: "local:quantal/quantal-wordpress-2", 868 Config: charm.Settings{"foo": "bar"}, 869 }, 870 }, 871 }, { 872 about: "non-service config change is ignored", 873 setUp: func(*gc.C, *State) {}, 874 change: watcher.Change{ 875 C: "settings", 876 Id: "m#0", 877 }, 878 }, { 879 about: "service config change with no charm url is ignored", 880 setUp: func(*gc.C, *State) {}, 881 change: watcher.Change{ 882 C: "settings", 883 Id: "s#foo", 884 }, 885 }, 886 } 887 888 func setServiceConfigAttr(c *gc.C, svc *Service, attr string, val interface{}) { 889 err := svc.UpdateConfigSettings(charm.Settings{attr: val}) 890 c.Assert(err, gc.IsNil) 891 } 892 893 func (s *storeManagerStateSuite) TestChanged(c *gc.C) { 894 collections := map[string]*mgo.Collection{ 895 "machines": s.State.machines, 896 "units": s.State.units, 897 "services": s.State.services, 898 "relations": s.State.relations, 899 "annotations": s.State.annotations, 900 "statuses": s.State.statuses, 901 "constraints": s.State.constraints, 902 "settings": s.State.settings, 903 } 904 for i, test := range allWatcherChangedTests { 905 c.Logf("test %d. %s", i, test.about) 906 b := newAllWatcherStateBacking(s.State) 907 all := multiwatcher.NewStore() 908 for _, info := range test.add { 909 all.Update(info) 910 } 911 test.setUp(c, s.State) 912 c.Logf("done set up") 913 ch := test.change 914 ch.C = collections[ch.C].Name 915 err := b.Changed(all, test.change) 916 c.Assert(err, gc.IsNil) 917 assertEntitiesEqual(c, all.All(), test.expectContents) 918 s.Reset(c) 919 } 920 } 921 922 // StateWatcher tests the integration of the state watcher 923 // with the state-based backing. Most of the logic is tested elsewhere - 924 // this just tests end-to-end. 925 func (s *storeManagerStateSuite) TestStateWatcher(c *gc.C) { 926 m0, err := s.State.AddMachine("quantal", JobManageEnviron) 927 c.Assert(err, gc.IsNil) 928 c.Assert(m0.Id(), gc.Equals, "0") 929 930 m1, err := s.State.AddMachine("quantal", JobHostUnits) 931 c.Assert(err, gc.IsNil) 932 c.Assert(m1.Id(), gc.Equals, "1") 933 934 b := newAllWatcherStateBacking(s.State) 935 aw := multiwatcher.NewStoreManager(b) 936 defer aw.Stop() 937 w := multiwatcher.NewWatcher(aw) 938 s.State.StartSync() 939 checkNext(c, w, b, []params.Delta{{ 940 Entity: ¶ms.MachineInfo{ 941 Id: "0", 942 Status: params.StatusPending, 943 }, 944 }, { 945 Entity: ¶ms.MachineInfo{ 946 Id: "1", 947 Status: params.StatusPending, 948 }, 949 }}, "") 950 951 // Make some changes to the state. 952 err = m0.SetProvisioned("i-0", "bootstrap_nonce", nil) 953 c.Assert(err, gc.IsNil) 954 err = m1.Destroy() 955 c.Assert(err, gc.IsNil) 956 err = m1.EnsureDead() 957 c.Assert(err, gc.IsNil) 958 err = m1.Remove() 959 c.Assert(err, gc.IsNil) 960 m2, err := s.State.AddMachine("quantal", JobHostUnits) 961 c.Assert(err, gc.IsNil) 962 c.Assert(m2.Id(), gc.Equals, "2") 963 s.State.StartSync() 964 965 // Check that we see the changes happen within a 966 // reasonable time. 967 var deltas []params.Delta 968 for { 969 d, err := getNext(c, w, 100*time.Millisecond) 970 if errors.Cause(err) == errTimeout { 971 break 972 } 973 c.Assert(err, gc.IsNil) 974 deltas = append(deltas, d...) 975 } 976 checkDeltasEqual(c, b, deltas, []params.Delta{{ 977 Removed: true, 978 Entity: ¶ms.MachineInfo{ 979 Id: "1", 980 Status: params.StatusPending, 981 }, 982 }, { 983 Entity: ¶ms.MachineInfo{ 984 Id: "2", 985 Status: params.StatusPending, 986 }, 987 }, { 988 Entity: ¶ms.MachineInfo{ 989 Id: "0", 990 InstanceId: "i-0", 991 Status: params.StatusPending, 992 }, 993 }}) 994 995 err = w.Stop() 996 c.Assert(err, gc.IsNil) 997 998 _, err = w.Next() 999 c.Assert(errors.Cause(err), gc.Equals, multiwatcher.ErrWatcherStopped) 1000 } 1001 1002 type entityInfoSlice []params.EntityInfo 1003 1004 func (s entityInfoSlice) Len() int { return len(s) } 1005 func (s entityInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 1006 func (s entityInfoSlice) Less(i, j int) bool { 1007 id0, id1 := s[i].EntityId(), s[j].EntityId() 1008 if id0.Kind != id1.Kind { 1009 return id0.Kind < id1.Kind 1010 } 1011 switch id := id0.Id.(type) { 1012 case string: 1013 return id < id1.Id.(string) 1014 default: 1015 } 1016 panic("unexpected entity id type") 1017 } 1018 1019 var errTimeout = errors.New("no change received in sufficient time") 1020 1021 func getNext(c *gc.C, w *multiwatcher.Watcher, timeout time.Duration) ([]params.Delta, error) { 1022 var deltas []params.Delta 1023 var err error 1024 ch := make(chan struct{}, 1) 1025 go func() { 1026 deltas, err = w.Next() 1027 ch <- struct{}{} 1028 }() 1029 select { 1030 case <-ch: 1031 return deltas, err 1032 case <-time.After(1 * time.Second): 1033 } 1034 return nil, errTimeout 1035 } 1036 1037 func checkNext(c *gc.C, w *multiwatcher.Watcher, b multiwatcher.Backing, deltas []params.Delta, expectErr string) { 1038 d, err := getNext(c, w, 1*time.Second) 1039 if expectErr != "" { 1040 c.Check(err, gc.ErrorMatches, expectErr) 1041 return 1042 } 1043 checkDeltasEqual(c, b, d, deltas) 1044 } 1045 1046 // deltas are returns in arbitrary order, so we compare 1047 // them as sets. 1048 func checkDeltasEqual(c *gc.C, b multiwatcher.Backing, d0, d1 []params.Delta) { 1049 c.Check(deltaMap(d0, b), gc.DeepEquals, deltaMap(d1, b)) 1050 } 1051 1052 func deltaMap(deltas []params.Delta, b multiwatcher.Backing) map[multiwatcher.InfoId]params.EntityInfo { 1053 m := make(map[multiwatcher.InfoId]params.EntityInfo) 1054 for _, d := range deltas { 1055 id := d.Entity.EntityId() 1056 if _, ok := m[id]; ok { 1057 panic(errors.Newf("%v mentioned twice in delta set", id)) 1058 } 1059 if d.Removed { 1060 m[id] = nil 1061 } else { 1062 m[id] = d.Entity 1063 } 1064 } 1065 return m 1066 }