github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/uniter/relation/relations_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package relation_test 5 6 import ( 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "sync/atomic" 11 12 "github.com/juju/errors" 13 "github.com/juju/names" 14 "github.com/juju/testing" 15 jc "github.com/juju/testing/checkers" 16 gc "gopkg.in/check.v1" 17 "gopkg.in/juju/charm.v6-unstable" 18 "gopkg.in/juju/charm.v6-unstable/hooks" 19 20 apitesting "github.com/juju/juju/api/base/testing" 21 "github.com/juju/juju/api/uniter" 22 "github.com/juju/juju/apiserver/common" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/state/multiwatcher" 25 coretesting "github.com/juju/juju/testing" 26 "github.com/juju/juju/worker/uniter/hook" 27 "github.com/juju/juju/worker/uniter/operation" 28 "github.com/juju/juju/worker/uniter/relation" 29 "github.com/juju/juju/worker/uniter/remotestate" 30 "github.com/juju/juju/worker/uniter/resolver" 31 ) 32 33 /* 34 TODO(wallyworld) 35 DO NOT COPY THE METHODOLOGY USED IN THESE TESTS. 36 We want to write unit tests without resorting to JujuConnSuite. 37 However, the current api/uniter code uses structs instead of 38 interfaces for its component model, and it's not possible to 39 implement a stub uniter api at the model level due to the way 40 the domain objects reference each other. 41 42 The best we can do for now is to stub out the facade caller and 43 return curated values for each API call. 44 */ 45 46 type relationsSuite struct { 47 coretesting.BaseSuite 48 49 stateDir string 50 relationsDir string 51 } 52 53 var _ = gc.Suite(&relationsSuite{}) 54 55 type apiCall struct { 56 request string 57 args interface{} 58 result interface{} 59 err error 60 } 61 62 func uniterApiCall(request string, args, result interface{}, err error) apiCall { 63 return apiCall{ 64 request: request, 65 args: args, 66 result: result, 67 err: err, 68 } 69 } 70 71 func mockAPICaller(c *gc.C, callNumber *int32, apiCalls ...apiCall) apitesting.APICallerFunc { 72 apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { 73 switch objType { 74 case "NotifyWatcher": 75 return nil 76 case "Uniter": 77 index := int(atomic.AddInt32(callNumber, 1)) - 1 78 c.Check(index < len(apiCalls), jc.IsTrue) 79 call := apiCalls[index] 80 c.Logf("request %d, %s", index, request) 81 c.Check(version, gc.Equals, 3) 82 c.Check(id, gc.Equals, "") 83 c.Check(request, gc.Equals, call.request) 84 c.Check(arg, jc.DeepEquals, call.args) 85 if call.err != nil { 86 return common.ServerError(call.err) 87 } 88 testing.PatchValue(result, call.result) 89 default: 90 c.Fail() 91 } 92 return nil 93 }) 94 return apiCaller 95 } 96 97 var minimalMetadata = ` 98 name: wordpress 99 summary: "test" 100 description: "test" 101 requires: 102 mysql: db 103 `[1:] 104 105 func (s *relationsSuite) SetUpTest(c *gc.C) { 106 s.stateDir = filepath.Join(c.MkDir(), "charm") 107 err := os.MkdirAll(s.stateDir, 0755) 108 c.Assert(err, jc.ErrorIsNil) 109 err = ioutil.WriteFile(filepath.Join(s.stateDir, "metadata.yaml"), []byte(minimalMetadata), 0755) 110 c.Assert(err, jc.ErrorIsNil) 111 s.relationsDir = filepath.Join(c.MkDir(), "relations") 112 } 113 114 func assertNumCalls(c *gc.C, numCalls *int32, expected int32) { 115 v := atomic.LoadInt32(numCalls) 116 c.Assert(v, gc.Equals, expected) 117 } 118 119 func (s *relationsSuite) setupRelations(c *gc.C) relation.Relations { 120 unitTag := names.NewUnitTag("wordpress/0") 121 abort := make(chan struct{}) 122 123 var numCalls int32 124 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 125 apiCaller := mockAPICaller(c, &numCalls, 126 uniterApiCall("Life", unitEntity, params.LifeResults{Results: []params.LifeResult{{Life: params.Alive}}}, nil), 127 uniterApiCall("JoinedRelations", unitEntity, params.StringsResults{Results: []params.StringsResult{{Result: []string{}}}}, nil), 128 ) 129 st := uniter.NewState(apiCaller, unitTag) 130 r, err := relation.NewRelations(st, unitTag, s.stateDir, s.relationsDir, abort) 131 c.Assert(err, jc.ErrorIsNil) 132 assertNumCalls(c, &numCalls, 2) 133 return r 134 } 135 136 func (s *relationsSuite) TestNewRelationsNoRelations(c *gc.C) { 137 r := s.setupRelations(c) 138 //No relations created. 139 c.Assert(r.GetInfo(), gc.HasLen, 0) 140 } 141 142 func (s *relationsSuite) TestNewRelationsWithExistingRelations(c *gc.C) { 143 unitTag := names.NewUnitTag("wordpress/0") 144 abort := make(chan struct{}) 145 146 var numCalls int32 147 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 148 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 149 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 150 }} 151 relationResults := params.RelationResults{ 152 Results: []params.RelationResult{ 153 { 154 Id: 1, 155 Key: "wordpress:db mysql:db", 156 Life: params.Alive, 157 Endpoint: multiwatcher.Endpoint{ 158 ServiceName: "wordpress", 159 Relation: charm.Relation{Name: "mysql", Role: charm.RoleProvider, Interface: "db"}, 160 }}, 161 }, 162 } 163 164 apiCaller := mockAPICaller(c, &numCalls, 165 uniterApiCall("Life", unitEntity, params.LifeResults{Results: []params.LifeResult{{Life: params.Alive}}}, nil), 166 uniterApiCall("JoinedRelations", unitEntity, params.StringsResults{Results: []params.StringsResult{{Result: []string{"relation-wordpress:db mysql:db"}}}}, nil), 167 uniterApiCall("Relation", relationUnits, relationResults, nil), 168 uniterApiCall("Relation", relationUnits, relationResults, nil), 169 uniterApiCall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 170 uniterApiCall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 171 ) 172 st := uniter.NewState(apiCaller, unitTag) 173 r, err := relation.NewRelations(st, unitTag, s.stateDir, s.relationsDir, abort) 174 c.Assert(err, jc.ErrorIsNil) 175 assertNumCalls(c, &numCalls, 6) 176 177 info := r.GetInfo() 178 c.Assert(info, gc.HasLen, 1) 179 oneInfo := info[1] 180 c.Assert(oneInfo.RelationUnit.Relation().Tag(), gc.Equals, names.NewRelationTag("wordpress:db mysql:db")) 181 c.Assert(oneInfo.RelationUnit.Endpoint(), jc.DeepEquals, uniter.Endpoint{ 182 Relation: charm.Relation{Name: "mysql", Role: "provider", Interface: "db", Optional: false, Limit: 0, Scope: ""}, 183 }) 184 c.Assert(oneInfo.MemberNames, gc.HasLen, 0) 185 } 186 187 func (s *relationsSuite) TestNextOpNothing(c *gc.C) { 188 unitTag := names.NewUnitTag("wordpress/0") 189 abort := make(chan struct{}) 190 191 var numCalls int32 192 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 193 apiCaller := mockAPICaller(c, &numCalls, 194 uniterApiCall("Life", unitEntity, params.LifeResults{Results: []params.LifeResult{{Life: params.Alive}}}, nil), 195 uniterApiCall("JoinedRelations", unitEntity, params.StringsResults{Results: []params.StringsResult{{Result: []string{}}}}, nil), 196 uniterApiCall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 197 ) 198 st := uniter.NewState(apiCaller, unitTag) 199 r, err := relation.NewRelations(st, unitTag, s.stateDir, s.relationsDir, abort) 200 c.Assert(err, jc.ErrorIsNil) 201 assertNumCalls(c, &numCalls, 2) 202 203 localState := resolver.LocalState{ 204 State: operation.State{ 205 Kind: operation.Continue, 206 }, 207 } 208 remoteState := remotestate.Snapshot{} 209 relationsResolver := relation.NewRelationsResolver(r) 210 _, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 211 c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation) 212 } 213 214 func relationJoinedApiCalls() []apiCall { 215 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 216 relationResults := params.RelationResults{ 217 Results: []params.RelationResult{ 218 { 219 Id: 1, 220 Key: "wordpress:db mysql:db", 221 Life: params.Alive, 222 Endpoint: multiwatcher.Endpoint{ 223 ServiceName: "wordpress", 224 Relation: charm.Relation{Name: "mysql", Role: charm.RoleRequirer, Interface: "db", Scope: "global"}, 225 }}, 226 }, 227 } 228 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 229 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 230 }} 231 apiCalls := []apiCall{ 232 uniterApiCall("Life", unitEntity, params.LifeResults{Results: []params.LifeResult{{Life: params.Alive}}}, nil), 233 uniterApiCall("JoinedRelations", unitEntity, params.StringsResults{Results: []params.StringsResult{{Result: []string{}}}}, nil), 234 uniterApiCall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), 235 uniterApiCall("Relation", relationUnits, relationResults, nil), 236 uniterApiCall("Relation", relationUnits, relationResults, nil), 237 uniterApiCall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 238 uniterApiCall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 239 uniterApiCall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 240 } 241 return apiCalls 242 } 243 244 func (s *relationsSuite) assertHookRelationJoined(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations { 245 unitTag := names.NewUnitTag("wordpress/0") 246 abort := make(chan struct{}) 247 248 apiCaller := mockAPICaller(c, numCalls, apiCalls...) 249 st := uniter.NewState(apiCaller, unitTag) 250 r, err := relation.NewRelations(st, unitTag, s.stateDir, s.relationsDir, abort) 251 c.Assert(err, jc.ErrorIsNil) 252 assertNumCalls(c, numCalls, 2) 253 254 localState := resolver.LocalState{ 255 State: operation.State{ 256 Kind: operation.Continue, 257 }, 258 } 259 remoteState := remotestate.Snapshot{ 260 Relations: map[int]remotestate.RelationSnapshot{ 261 1: remotestate.RelationSnapshot{ 262 Life: params.Alive, 263 Members: map[string]int64{ 264 "wordpress": 1, 265 }, 266 }, 267 }, 268 } 269 relationsResolver := relation.NewRelationsResolver(r) 270 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 271 c.Assert(err, jc.ErrorIsNil) 272 assertNumCalls(c, numCalls, 8) 273 c.Assert(op.String(), gc.Equals, "run hook relation-joined on unit with relation 1") 274 275 // Commit the operation so we save local state for any next operation. 276 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 277 c.Assert(err, jc.ErrorIsNil) 278 err = r.CommitHook(op.(*mockOperation).hookInfo) 279 c.Assert(err, jc.ErrorIsNil) 280 return r 281 } 282 283 func (s *relationsSuite) TestHookRelationJoined(c *gc.C) { 284 var numCalls int32 285 s.assertHookRelationJoined(c, &numCalls, relationJoinedApiCalls()...) 286 } 287 288 func (s *relationsSuite) assertHookRelationChanged( 289 c *gc.C, r relation.Relations, 290 remoteRelationSnapshot remotestate.RelationSnapshot, 291 numCalls *int32, 292 ) { 293 numCallsBefore := *numCalls 294 localState := resolver.LocalState{ 295 State: operation.State{ 296 Kind: operation.Continue, 297 }, 298 } 299 remoteState := remotestate.Snapshot{ 300 Relations: map[int]remotestate.RelationSnapshot{ 301 1: remoteRelationSnapshot, 302 }, 303 } 304 relationsResolver := relation.NewRelationsResolver(r) 305 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 306 c.Assert(err, jc.ErrorIsNil) 307 assertNumCalls(c, numCalls, numCallsBefore+1) 308 c.Assert(op.String(), gc.Equals, "run hook relation-changed on unit with relation 1") 309 310 // Commit the operation so we save local state for any next operation. 311 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 312 c.Assert(err, jc.ErrorIsNil) 313 err = r.CommitHook(op.(*mockOperation).hookInfo) 314 c.Assert(err, jc.ErrorIsNil) 315 } 316 317 func getPrincipalApiCalls(numCalls int32) []apiCall { 318 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 319 result := make([]apiCall, numCalls) 320 for i := int32(0); i < numCalls; i++ { 321 result[i] = uniterApiCall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil) 322 } 323 return result 324 } 325 326 func (s *relationsSuite) TestHookRelationChanged(c *gc.C) { 327 var numCalls int32 328 apiCalls := relationJoinedApiCalls() 329 apiCalls = append(apiCalls, getPrincipalApiCalls(3)...) 330 r := s.assertHookRelationJoined(c, &numCalls, apiCalls...) 331 332 // There will be an initial relation-changed regardless of 333 // members, due to the "changed pending" local persistent 334 // state. 335 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 336 Life: params.Alive, 337 }, &numCalls) 338 339 // wordpress starts at 1, changing to 2 should trigger a 340 // relation-changed hook. 341 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 342 Life: params.Alive, 343 Members: map[string]int64{ 344 "wordpress": 2, 345 }, 346 }, &numCalls) 347 348 // NOTE(axw) this is a test for the temporary to fix lp:1495542. 349 // 350 // wordpress is at 2, changing to 1 should trigger a 351 // relation-changed hook. This is to cater for the scenario 352 // where the relation settings document is removed and 353 // recreated, thus resetting the txn-revno. 354 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 355 Life: params.Alive, 356 Members: map[string]int64{ 357 "wordpress": 1, 358 }, 359 }, &numCalls) 360 } 361 362 func (s *relationsSuite) assertHookRelationDeparted(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations { 363 r := s.assertHookRelationJoined(c, numCalls, apiCalls...) 364 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 365 Life: params.Alive, 366 }, numCalls) 367 numCallsBefore := *numCalls 368 369 localState := resolver.LocalState{ 370 State: operation.State{ 371 Kind: operation.Continue, 372 }, 373 } 374 remoteState := remotestate.Snapshot{ 375 Relations: map[int]remotestate.RelationSnapshot{ 376 1: remotestate.RelationSnapshot{ 377 Life: params.Dying, 378 Members: map[string]int64{ 379 "wordpress": 1, 380 }, 381 }, 382 }, 383 } 384 relationsResolver := relation.NewRelationsResolver(r) 385 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 386 c.Assert(err, jc.ErrorIsNil) 387 assertNumCalls(c, numCalls, numCallsBefore+1) 388 c.Assert(op.String(), gc.Equals, "run hook relation-departed on unit with relation 1") 389 390 // Commit the operation so we save local state for any next operation. 391 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 392 c.Assert(err, jc.ErrorIsNil) 393 err = r.CommitHook(op.(*mockOperation).hookInfo) 394 c.Assert(err, jc.ErrorIsNil) 395 return r 396 } 397 398 func (s *relationsSuite) TestHookRelationDeparted(c *gc.C) { 399 var numCalls int32 400 apiCalls := relationJoinedApiCalls() 401 402 apiCalls = append(apiCalls, getPrincipalApiCalls(2)...) 403 s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 404 } 405 406 func (s *relationsSuite) TestHookRelationBroken(c *gc.C) { 407 var numCalls int32 408 apiCalls := relationJoinedApiCalls() 409 410 apiCalls = append(apiCalls, getPrincipalApiCalls(3)...) 411 r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 412 413 localState := resolver.LocalState{ 414 State: operation.State{ 415 Kind: operation.Continue, 416 }, 417 } 418 remoteState := remotestate.Snapshot{ 419 Relations: map[int]remotestate.RelationSnapshot{ 420 1: remotestate.RelationSnapshot{ 421 Life: params.Dying, 422 }, 423 }, 424 } 425 relationsResolver := relation.NewRelationsResolver(r) 426 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 427 c.Assert(err, jc.ErrorIsNil) 428 assertNumCalls(c, &numCalls, 11) 429 c.Assert(op.String(), gc.Equals, "run hook relation-broken on unit with relation 1") 430 } 431 432 func (s *relationsSuite) TestCommitHook(c *gc.C) { 433 var numCalls int32 434 apiCalls := relationJoinedApiCalls() 435 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 436 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 437 }} 438 apiCalls = append(apiCalls, 439 uniterApiCall("LeaveScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 440 ) 441 stateFile := filepath.Join(s.relationsDir, "1", "wordpress") 442 c.Assert(stateFile, jc.DoesNotExist) 443 r := s.assertHookRelationJoined(c, &numCalls, apiCalls...) 444 445 data, err := ioutil.ReadFile(stateFile) 446 c.Assert(err, jc.ErrorIsNil) 447 c.Assert(string(data), gc.Equals, "change-version: 1\nchanged-pending: true\n") 448 449 err = r.CommitHook(hook.Info{ 450 Kind: hooks.RelationChanged, 451 RemoteUnit: "wordpress", 452 RelationId: 1, 453 ChangeVersion: 2, 454 }) 455 c.Assert(err, jc.ErrorIsNil) 456 data, err = ioutil.ReadFile(stateFile) 457 c.Assert(err, jc.ErrorIsNil) 458 c.Assert(string(data), gc.Equals, "change-version: 2\n") 459 460 err = r.CommitHook(hook.Info{ 461 Kind: hooks.RelationDeparted, 462 RemoteUnit: "wordpress", 463 RelationId: 1, 464 }) 465 c.Assert(err, jc.ErrorIsNil) 466 c.Assert(stateFile, jc.DoesNotExist) 467 } 468 469 func (s *relationsSuite) TestImplicitRelationNoHooks(c *gc.C) { 470 unitTag := names.NewUnitTag("wordpress/0") 471 abort := make(chan struct{}) 472 473 unitEntity := params.Entities{Entities: []params.Entity{params.Entity{Tag: "unit-wordpress-0"}}} 474 relationResults := params.RelationResults{ 475 Results: []params.RelationResult{ 476 { 477 Id: 1, 478 Key: "wordpress:juju-info juju-info:juju-info", 479 Life: params.Alive, 480 Endpoint: multiwatcher.Endpoint{ 481 ServiceName: "wordpress", 482 Relation: charm.Relation{Name: "juju-info", Role: charm.RoleProvider, Interface: "juju-info", Scope: "global"}, 483 }}, 484 }, 485 } 486 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 487 {Relation: "relation-wordpress.juju-info#juju-info.juju-info", Unit: "unit-wordpress-0"}, 488 }} 489 apiCalls := []apiCall{ 490 uniterApiCall("Life", unitEntity, params.LifeResults{Results: []params.LifeResult{{Life: params.Alive}}}, nil), 491 uniterApiCall("JoinedRelations", unitEntity, params.StringsResults{Results: []params.StringsResult{{Result: []string{}}}}, nil), 492 uniterApiCall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), 493 uniterApiCall("Relation", relationUnits, relationResults, nil), 494 uniterApiCall("Relation", relationUnits, relationResults, nil), 495 uniterApiCall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 496 uniterApiCall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 497 uniterApiCall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 498 } 499 500 var numCalls int32 501 apiCaller := mockAPICaller(c, &numCalls, apiCalls...) 502 st := uniter.NewState(apiCaller, unitTag) 503 r, err := relation.NewRelations(st, unitTag, s.stateDir, s.relationsDir, abort) 504 c.Assert(err, jc.ErrorIsNil) 505 506 localState := resolver.LocalState{ 507 State: operation.State{ 508 Kind: operation.Continue, 509 }, 510 } 511 remoteState := remotestate.Snapshot{ 512 Relations: map[int]remotestate.RelationSnapshot{ 513 1: remotestate.RelationSnapshot{ 514 Life: params.Alive, 515 Members: map[string]int64{ 516 "wordpress": 1, 517 }, 518 }, 519 }, 520 } 521 relationsResolver := relation.NewRelationsResolver(r) 522 _, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 523 c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation) 524 }