github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "gopkg.in/check.v1"
    16  	"gopkg.in/juju/charm.v6"
    17  	"gopkg.in/juju/charm.v6/hooks"
    18  	"gopkg.in/juju/names.v2"
    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/core/leadership"
    25  	"github.com/juju/juju/state/multiwatcher"
    26  	coretesting "github.com/juju/juju/testing"
    27  	"github.com/juju/juju/worker/uniter/hook"
    28  	"github.com/juju/juju/worker/uniter/operation"
    29  	"github.com/juju/juju/worker/uniter/relation"
    30  	"github.com/juju/juju/worker/uniter/remotestate"
    31  	"github.com/juju/juju/worker/uniter/resolver"
    32  	"github.com/juju/juju/worker/uniter/runner/context"
    33  )
    34  
    35  /*
    36  TODO(wallyworld)
    37  DO NOT COPY THE METHODOLOGY USED IN THESE TESTS.
    38  We want to write unit tests without resorting to JujuConnSuite.
    39  However, the current api/uniter code uses structs instead of
    40  interfaces for its component model, and it's not possible to
    41  implement a stub uniter api at the model level due to the way
    42  the domain objects reference each other.
    43  
    44  The best we can do for now is to stub out the facade caller and
    45  return curated values for each API call.
    46  */
    47  
    48  type relationsSuite struct {
    49  	coretesting.BaseSuite
    50  
    51  	stateDir              string
    52  	relationsDir          string
    53  	leadershipContextFunc relation.LeadershipContextFunc
    54  }
    55  
    56  var _ = gc.Suite(&relationsSuite{})
    57  
    58  type apiCall struct {
    59  	request string
    60  	args    interface{}
    61  	result  interface{}
    62  	err     error
    63  }
    64  
    65  func uniterAPICall(request string, args, result interface{}, err error) apiCall {
    66  	return apiCall{
    67  		request: request,
    68  		args:    args,
    69  		result:  result,
    70  		err:     err,
    71  	}
    72  }
    73  
    74  func mockAPICaller(c *gc.C, callNumber *int32, apiCalls ...apiCall) apitesting.APICallerFunc {
    75  	apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
    76  		switch objType {
    77  		case "NotifyWatcher":
    78  			return nil
    79  		case "Uniter":
    80  			index := int(atomic.AddInt32(callNumber, 1)) - 1
    81  			c.Check(index < len(apiCalls), jc.IsTrue)
    82  			call := apiCalls[index]
    83  			c.Logf("request %d, %s", index, request)
    84  			c.Check(version, gc.Equals, 9)
    85  			c.Check(id, gc.Equals, "")
    86  			c.Check(request, gc.Equals, call.request)
    87  			c.Check(arg, jc.DeepEquals, call.args)
    88  			if call.err != nil {
    89  				return common.ServerError(call.err)
    90  			}
    91  			testing.PatchValue(result, call.result)
    92  		default:
    93  			c.Fail()
    94  		}
    95  		return nil
    96  	})
    97  	return apiCaller
    98  }
    99  
   100  type stubLeadershipContext struct {
   101  	context.LeadershipContext
   102  	isLeader bool
   103  }
   104  
   105  func (stub *stubLeadershipContext) IsLeader() (bool, error) {
   106  	return stub.isLeader, nil
   107  }
   108  
   109  var minimalMetadata = `
   110  name: wordpress
   111  summary: "test"
   112  description: "test"
   113  requires:
   114    mysql: db
   115  `[1:]
   116  
   117  func (s *relationsSuite) SetUpTest(c *gc.C) {
   118  	s.stateDir = filepath.Join(c.MkDir(), "charm")
   119  	err := os.MkdirAll(s.stateDir, 0755)
   120  	c.Assert(err, jc.ErrorIsNil)
   121  	err = ioutil.WriteFile(filepath.Join(s.stateDir, "metadata.yaml"), []byte(minimalMetadata), 0755)
   122  	c.Assert(err, jc.ErrorIsNil)
   123  	s.relationsDir = filepath.Join(c.MkDir(), "relations")
   124  	s.leadershipContextFunc = func(accessor context.LeadershipSettingsAccessor, tracker leadership.Tracker, unitName string) context.LeadershipContext {
   125  		return &stubLeadershipContext{isLeader: true}
   126  	}
   127  }
   128  
   129  func assertNumCalls(c *gc.C, numCalls *int32, expected int32) {
   130  	v := atomic.LoadInt32(numCalls)
   131  	c.Assert(v, gc.Equals, expected)
   132  }
   133  
   134  func (s *relationsSuite) setupRelations(c *gc.C) relation.Relations {
   135  	unitTag := names.NewUnitTag("wordpress/0")
   136  	abort := make(chan struct{})
   137  
   138  	var numCalls int32
   139  	unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}}
   140  	apiCaller := mockAPICaller(c, &numCalls,
   141  		uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   142  		uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil),
   143  		uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil),
   144  	)
   145  	st := uniter.NewState(apiCaller, unitTag)
   146  	r, err := relation.NewRelations(
   147  		relation.RelationsConfig{
   148  			State:                st,
   149  			UnitTag:              unitTag,
   150  			CharmDir:             s.stateDir,
   151  			RelationsDir:         s.relationsDir,
   152  			NewLeadershipContext: s.leadershipContextFunc,
   153  			Abort:                abort,
   154  		})
   155  	c.Assert(err, jc.ErrorIsNil)
   156  	assertNumCalls(c, &numCalls, 3)
   157  	return r
   158  }
   159  
   160  func (s *relationsSuite) TestNewRelationsNoRelations(c *gc.C) {
   161  	r := s.setupRelations(c)
   162  	//No relations created.
   163  	c.Assert(r.GetInfo(), gc.HasLen, 0)
   164  }
   165  
   166  func (s *relationsSuite) assertNewRelationsWithExistingRelations(c *gc.C, isLeader bool) {
   167  	unitTag := names.NewUnitTag("wordpress/0")
   168  	abort := make(chan struct{})
   169  	s.leadershipContextFunc = func(accessor context.LeadershipSettingsAccessor, tracker leadership.Tracker, unitName string) context.LeadershipContext {
   170  		return &stubLeadershipContext{isLeader: isLeader}
   171  	}
   172  
   173  	var numCalls int32
   174  	unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}}
   175  	relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{
   176  		{Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"},
   177  	}}
   178  	relationResults := params.RelationResults{
   179  		Results: []params.RelationResult{
   180  			{
   181  				Id:   1,
   182  				Key:  "wordpress:db mysql:db",
   183  				Life: params.Alive,
   184  				Endpoint: multiwatcher.Endpoint{
   185  					ApplicationName: "wordpress",
   186  					Relation:        multiwatcher.CharmRelation{Name: "mysql", Role: string(charm.RoleProvider), Interface: "db"},
   187  				}},
   188  		},
   189  	}
   190  	relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{
   191  		UnitTag:    "unit-wordpress-0",
   192  		RelationId: 1,
   193  		Status:     params.Joined,
   194  	}}}
   195  
   196  	apiCalls := []apiCall{
   197  		uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   198  		uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil),
   199  		uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{
   200  			{RelationResults: []params.RelationUnitStatus{{RelationTag: "relation-wordpress:db mysql:db", InScope: true}}}}}, nil),
   201  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   202  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   203  		uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil),
   204  		uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil),
   205  	}
   206  	if isLeader {
   207  		apiCalls = append(apiCalls,
   208  			uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil),
   209  		)
   210  	}
   211  	apiCaller := mockAPICaller(c, &numCalls, apiCalls...)
   212  	st := uniter.NewState(apiCaller, unitTag)
   213  	r, err := relation.NewRelations(
   214  		relation.RelationsConfig{
   215  			State:                st,
   216  			UnitTag:              unitTag,
   217  			CharmDir:             s.stateDir,
   218  			RelationsDir:         s.relationsDir,
   219  			NewLeadershipContext: s.leadershipContextFunc,
   220  			Abort:                abort,
   221  		})
   222  	c.Assert(err, jc.ErrorIsNil)
   223  	assertNumCalls(c, &numCalls, int32(len(apiCalls)))
   224  
   225  	info := r.GetInfo()
   226  	c.Assert(info, gc.HasLen, 1)
   227  	oneInfo := info[1]
   228  	c.Assert(oneInfo.RelationUnit.Relation().Tag(), gc.Equals, names.NewRelationTag("wordpress:db mysql:db"))
   229  	c.Assert(oneInfo.RelationUnit.Endpoint(), jc.DeepEquals, uniter.Endpoint{
   230  		Relation: charm.Relation{Name: "mysql", Role: "provider", Interface: "db", Optional: false, Limit: 0, Scope: ""},
   231  	})
   232  	c.Assert(oneInfo.MemberNames, gc.HasLen, 0)
   233  }
   234  
   235  func (s *relationsSuite) TestNewRelationsWithExistingRelationsLeader(c *gc.C) {
   236  	s.assertNewRelationsWithExistingRelations(c, true)
   237  }
   238  
   239  func (s *relationsSuite) TestNewRelationsWithExistingRelationsNotLeader(c *gc.C) {
   240  	s.assertNewRelationsWithExistingRelations(c, false)
   241  }
   242  
   243  func (s *relationsSuite) TestNextOpNothing(c *gc.C) {
   244  	unitTag := names.NewUnitTag("wordpress/0")
   245  	abort := make(chan struct{})
   246  
   247  	var numCalls int32
   248  	unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}}
   249  	apiCaller := mockAPICaller(c, &numCalls,
   250  		uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   251  		uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil),
   252  		uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil),
   253  	)
   254  	st := uniter.NewState(apiCaller, unitTag)
   255  	r, err := relation.NewRelations(
   256  		relation.RelationsConfig{
   257  			State:                st,
   258  			UnitTag:              unitTag,
   259  			CharmDir:             s.stateDir,
   260  			RelationsDir:         s.relationsDir,
   261  			NewLeadershipContext: s.leadershipContextFunc,
   262  			Abort:                abort,
   263  		})
   264  	c.Assert(err, jc.ErrorIsNil)
   265  	assertNumCalls(c, &numCalls, 3)
   266  
   267  	localState := resolver.LocalState{
   268  		State: operation.State{
   269  			Kind: operation.Continue,
   270  		},
   271  	}
   272  	remoteState := remotestate.Snapshot{}
   273  	relationsResolver := relation.NewRelationsResolver(r)
   274  	_, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   275  	c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation)
   276  }
   277  
   278  func relationJoinedAPICalls() []apiCall {
   279  	unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}}
   280  	relationResults := params.RelationResults{
   281  		Results: []params.RelationResult{
   282  			{
   283  				Id:   1,
   284  				Key:  "wordpress:db mysql:db",
   285  				Life: params.Alive,
   286  				Endpoint: multiwatcher.Endpoint{
   287  					ApplicationName: "wordpress",
   288  					Relation:        multiwatcher.CharmRelation{Name: "mysql", Role: string(charm.RoleRequirer), Interface: "db", Scope: "global"},
   289  				}},
   290  		},
   291  	}
   292  	relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{
   293  		{Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"},
   294  	}}
   295  	relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{
   296  		UnitTag:    "unit-wordpress-0",
   297  		RelationId: 1,
   298  		Status:     params.Joined,
   299  	}}}
   300  	apiCalls := []apiCall{
   301  		uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   302  		uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil),
   303  		uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil),
   304  		uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil),
   305  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   306  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   307  		uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil),
   308  		uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil),
   309  		uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil),
   310  	}
   311  	return apiCalls
   312  }
   313  
   314  func (s *relationsSuite) assertHookRelationJoined(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations {
   315  	unitTag := names.NewUnitTag("wordpress/0")
   316  	abort := make(chan struct{})
   317  
   318  	apiCaller := mockAPICaller(c, numCalls, apiCalls...)
   319  	st := uniter.NewState(apiCaller, unitTag)
   320  	r, err := relation.NewRelations(
   321  		relation.RelationsConfig{
   322  			State:                st,
   323  			UnitTag:              unitTag,
   324  			CharmDir:             s.stateDir,
   325  			RelationsDir:         s.relationsDir,
   326  			NewLeadershipContext: s.leadershipContextFunc,
   327  			Abort:                abort,
   328  		})
   329  	c.Assert(err, jc.ErrorIsNil)
   330  	assertNumCalls(c, numCalls, 3)
   331  
   332  	localState := resolver.LocalState{
   333  		State: operation.State{
   334  			Kind: operation.Continue,
   335  		},
   336  	}
   337  	remoteState := remotestate.Snapshot{
   338  		Relations: map[int]remotestate.RelationSnapshot{
   339  			1: {
   340  				Life:      params.Alive,
   341  				Suspended: false,
   342  				Members: map[string]int64{
   343  					"wordpress": 1,
   344  				},
   345  			},
   346  		},
   347  	}
   348  	relationsResolver := relation.NewRelationsResolver(r)
   349  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   350  	c.Assert(err, jc.ErrorIsNil)
   351  	assertNumCalls(c, numCalls, 9)
   352  	c.Assert(op.String(), gc.Equals, "run hook relation-joined on unit with relation 1")
   353  
   354  	// Commit the operation so we save local state for any next operation.
   355  	_, err = r.PrepareHook(op.(*mockOperation).hookInfo)
   356  	c.Assert(err, jc.ErrorIsNil)
   357  	err = r.CommitHook(op.(*mockOperation).hookInfo)
   358  	c.Assert(err, jc.ErrorIsNil)
   359  	return r
   360  }
   361  
   362  func (s *relationsSuite) TestHookRelationJoined(c *gc.C) {
   363  	var numCalls int32
   364  	s.assertHookRelationJoined(c, &numCalls, relationJoinedAPICalls()...)
   365  }
   366  
   367  func (s *relationsSuite) assertHookRelationChanged(
   368  	c *gc.C, r relation.Relations,
   369  	remoteRelationSnapshot remotestate.RelationSnapshot,
   370  	numCalls *int32,
   371  ) {
   372  	numCallsBefore := *numCalls
   373  	localState := resolver.LocalState{
   374  		State: operation.State{
   375  			Kind: operation.Continue,
   376  		},
   377  	}
   378  	remoteState := remotestate.Snapshot{
   379  		Relations: map[int]remotestate.RelationSnapshot{
   380  			1: remoteRelationSnapshot,
   381  		},
   382  	}
   383  	relationsResolver := relation.NewRelationsResolver(r)
   384  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   385  	c.Assert(err, jc.ErrorIsNil)
   386  	assertNumCalls(c, numCalls, numCallsBefore)
   387  	c.Assert(op.String(), gc.Equals, "run hook relation-changed on unit with relation 1")
   388  
   389  	// Commit the operation so we save local state for any next operation.
   390  	_, err = r.PrepareHook(op.(*mockOperation).hookInfo)
   391  	c.Assert(err, jc.ErrorIsNil)
   392  	err = r.CommitHook(op.(*mockOperation).hookInfo)
   393  	c.Assert(err, jc.ErrorIsNil)
   394  }
   395  
   396  func (s *relationsSuite) TestHookRelationChanged(c *gc.C) {
   397  	var numCalls int32
   398  	apiCalls := relationJoinedAPICalls()
   399  	r := s.assertHookRelationJoined(c, &numCalls, apiCalls...)
   400  
   401  	// There will be an initial relation-changed regardless of
   402  	// members, due to the "changed pending" local persistent
   403  	// state.
   404  	s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{
   405  		Life:      params.Alive,
   406  		Suspended: false,
   407  	}, &numCalls)
   408  
   409  	// wordpress starts at 1, changing to 2 should trigger a
   410  	// relation-changed hook.
   411  	s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{
   412  		Life:      params.Alive,
   413  		Suspended: false,
   414  		Members: map[string]int64{
   415  			"wordpress": 2,
   416  		},
   417  	}, &numCalls)
   418  
   419  	// NOTE(axw) this is a test for the temporary to fix lp:1495542.
   420  	//
   421  	// wordpress is at 2, changing to 1 should trigger a
   422  	// relation-changed hook. This is to cater for the scenario
   423  	// where the relation settings document is removed and
   424  	// recreated, thus resetting the txn-revno.
   425  	s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{
   426  		Life: params.Alive,
   427  		Members: map[string]int64{
   428  			"wordpress": 1,
   429  		},
   430  	}, &numCalls)
   431  }
   432  
   433  func (s *relationsSuite) TestHookRelationChangedSuspended(c *gc.C) {
   434  	var numCalls int32
   435  	apiCalls := relationJoinedAPICalls()
   436  	r := s.assertHookRelationJoined(c, &numCalls, apiCalls...)
   437  
   438  	// There will be an initial relation-changed regardless of
   439  	// members, due to the "changed pending" local persistent
   440  	// state.
   441  	s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{
   442  		Life:      params.Alive,
   443  		Suspended: true,
   444  	}, &numCalls)
   445  	c.Assert(r.GetInfo()[1].RelationUnit.Relation().Suspended(), jc.IsTrue)
   446  
   447  	numCallsBefore := numCalls
   448  
   449  	localState := resolver.LocalState{
   450  		State: operation.State{
   451  			Kind: operation.Continue,
   452  		},
   453  	}
   454  	remoteState := remotestate.Snapshot{
   455  		Relations: map[int]remotestate.RelationSnapshot{
   456  			1: {
   457  				Life:      params.Alive,
   458  				Suspended: true,
   459  			},
   460  		},
   461  	}
   462  
   463  	relationsResolver := relation.NewRelationsResolver(r)
   464  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   465  	c.Assert(err, jc.ErrorIsNil)
   466  	assertNumCalls(c, &numCalls, numCallsBefore)
   467  	c.Assert(op.String(), gc.Equals, "run hook relation-departed on unit with relation 1")
   468  }
   469  
   470  func (s *relationsSuite) assertHookRelationDeparted(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations {
   471  	r := s.assertHookRelationJoined(c, numCalls, apiCalls...)
   472  	s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{
   473  		Life:      params.Alive,
   474  		Suspended: false,
   475  	}, numCalls)
   476  	numCallsBefore := *numCalls
   477  
   478  	localState := resolver.LocalState{
   479  		State: operation.State{
   480  			Kind: operation.Continue,
   481  		},
   482  	}
   483  	remoteState := remotestate.Snapshot{
   484  		Relations: map[int]remotestate.RelationSnapshot{
   485  			1: {
   486  				Life: params.Dying,
   487  				Members: map[string]int64{
   488  					"wordpress": 1,
   489  				},
   490  			},
   491  		},
   492  	}
   493  	relationsResolver := relation.NewRelationsResolver(r)
   494  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   495  	c.Assert(err, jc.ErrorIsNil)
   496  	assertNumCalls(c, numCalls, numCallsBefore)
   497  	c.Assert(op.String(), gc.Equals, "run hook relation-departed on unit with relation 1")
   498  
   499  	// Commit the operation so we save local state for any next operation.
   500  	_, err = r.PrepareHook(op.(*mockOperation).hookInfo)
   501  	c.Assert(err, jc.ErrorIsNil)
   502  	err = r.CommitHook(op.(*mockOperation).hookInfo)
   503  	c.Assert(err, jc.ErrorIsNil)
   504  	return r
   505  }
   506  
   507  func (s *relationsSuite) TestHookRelationDeparted(c *gc.C) {
   508  	var numCalls int32
   509  	apiCalls := relationJoinedAPICalls()
   510  
   511  	s.assertHookRelationDeparted(c, &numCalls, apiCalls...)
   512  }
   513  
   514  func (s *relationsSuite) TestHookRelationBroken(c *gc.C) {
   515  	var numCalls int32
   516  	apiCalls := relationJoinedAPICalls()
   517  
   518  	r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...)
   519  
   520  	localState := resolver.LocalState{
   521  		State: operation.State{
   522  			Kind: operation.Continue,
   523  		},
   524  	}
   525  	remoteState := remotestate.Snapshot{
   526  		Relations: map[int]remotestate.RelationSnapshot{
   527  			1: {
   528  				Life: params.Dying,
   529  			},
   530  		},
   531  	}
   532  	relationsResolver := relation.NewRelationsResolver(r)
   533  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   534  	c.Assert(err, jc.ErrorIsNil)
   535  	assertNumCalls(c, &numCalls, 9)
   536  	c.Assert(op.String(), gc.Equals, "run hook relation-broken on unit with relation 1")
   537  }
   538  
   539  func (s *relationsSuite) TestHookRelationBrokenWhenSuspended(c *gc.C) {
   540  	var numCalls int32
   541  	apiCalls := relationJoinedAPICalls()
   542  
   543  	r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...)
   544  
   545  	localState := resolver.LocalState{
   546  		State: operation.State{
   547  			Kind: operation.Continue,
   548  		},
   549  	}
   550  	remoteState := remotestate.Snapshot{
   551  		Relations: map[int]remotestate.RelationSnapshot{
   552  			1: {
   553  				Life:      params.Alive,
   554  				Suspended: true,
   555  			},
   556  		},
   557  	}
   558  	relationsResolver := relation.NewRelationsResolver(r)
   559  	op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   560  	c.Assert(err, jc.ErrorIsNil)
   561  	assertNumCalls(c, &numCalls, 9)
   562  	c.Assert(op.String(), gc.Equals, "run hook relation-broken on unit with relation 1")
   563  }
   564  
   565  func (s *relationsSuite) TestHookRelationBrokenOnlyOnce(c *gc.C) {
   566  	var numCalls int32
   567  	apiCalls := relationJoinedAPICalls()
   568  	relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{
   569  		{Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"},
   570  	}}
   571  	apiCalls = append(apiCalls,
   572  		uniterAPICall("LeaveScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil),
   573  	)
   574  
   575  	r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...)
   576  
   577  	localState := resolver.LocalState{
   578  		State: operation.State{
   579  			Kind: operation.Continue,
   580  		},
   581  	}
   582  	remoteState := remotestate.Snapshot{
   583  		Relations: map[int]remotestate.RelationSnapshot{
   584  			1: {
   585  				Life:      params.Alive,
   586  				Suspended: true,
   587  			},
   588  		},
   589  	}
   590  	relationsResolver := relation.NewRelationsResolver(r)
   591  
   592  	// Remove the state directory to check that the hook is not run again.
   593  	err := os.RemoveAll(s.relationsDir)
   594  	c.Assert(err, jc.ErrorIsNil)
   595  	_, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   596  	c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation)
   597  }
   598  
   599  func (s *relationsSuite) TestCommitHook(c *gc.C) {
   600  	var numCalls int32
   601  	apiCalls := relationJoinedAPICalls()
   602  	relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{
   603  		{Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"},
   604  	}}
   605  	apiCalls = append(apiCalls,
   606  		uniterAPICall("LeaveScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil),
   607  	)
   608  	stateFile := filepath.Join(s.relationsDir, "1", "wordpress")
   609  	c.Assert(stateFile, jc.DoesNotExist)
   610  	r := s.assertHookRelationJoined(c, &numCalls, apiCalls...)
   611  
   612  	data, err := ioutil.ReadFile(stateFile)
   613  	c.Assert(err, jc.ErrorIsNil)
   614  	c.Assert(string(data), gc.Equals, "change-version: 1\nchanged-pending: true\n")
   615  
   616  	err = r.CommitHook(hook.Info{
   617  		Kind:          hooks.RelationChanged,
   618  		RemoteUnit:    "wordpress",
   619  		RelationId:    1,
   620  		ChangeVersion: 2,
   621  	})
   622  	c.Assert(err, jc.ErrorIsNil)
   623  	data, err = ioutil.ReadFile(stateFile)
   624  	c.Assert(err, jc.ErrorIsNil)
   625  	c.Assert(string(data), gc.Equals, "change-version: 2\n")
   626  
   627  	err = r.CommitHook(hook.Info{
   628  		Kind:       hooks.RelationDeparted,
   629  		RemoteUnit: "wordpress",
   630  		RelationId: 1,
   631  	})
   632  	c.Assert(err, jc.ErrorIsNil)
   633  	c.Assert(stateFile, jc.DoesNotExist)
   634  }
   635  
   636  func (s *relationsSuite) TestImplicitRelationNoHooks(c *gc.C) {
   637  	unitTag := names.NewUnitTag("wordpress/0")
   638  	abort := make(chan struct{})
   639  
   640  	unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}}
   641  	relationResults := params.RelationResults{
   642  		Results: []params.RelationResult{
   643  			{
   644  				Id:   1,
   645  				Key:  "wordpress:juju-info juju-info:juju-info",
   646  				Life: params.Alive,
   647  				Endpoint: multiwatcher.Endpoint{
   648  					ApplicationName: "wordpress",
   649  					Relation:        multiwatcher.CharmRelation{Name: "juju-info", Role: string(charm.RoleProvider), Interface: "juju-info", Scope: "global"},
   650  				}},
   651  		},
   652  	}
   653  	relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{
   654  		{Relation: "relation-wordpress.juju-info#juju-info.juju-info", Unit: "unit-wordpress-0"},
   655  	}}
   656  	relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{
   657  		UnitTag:    "unit-wordpress-0",
   658  		RelationId: 1,
   659  		Status:     params.Joined,
   660  	}}}
   661  	apiCalls := []apiCall{
   662  		uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   663  		uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil),
   664  		uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil),
   665  		uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil),
   666  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   667  		uniterAPICall("Relation", relationUnits, relationResults, nil),
   668  		uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil),
   669  		uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil),
   670  		uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil),
   671  	}
   672  
   673  	var numCalls int32
   674  	apiCaller := mockAPICaller(c, &numCalls, apiCalls...)
   675  	st := uniter.NewState(apiCaller, unitTag)
   676  	r, err := relation.NewRelations(
   677  		relation.RelationsConfig{
   678  			State:                st,
   679  			UnitTag:              unitTag,
   680  			CharmDir:             s.stateDir,
   681  			RelationsDir:         s.relationsDir,
   682  			NewLeadershipContext: s.leadershipContextFunc,
   683  			Abort:                abort,
   684  		})
   685  	c.Assert(err, jc.ErrorIsNil)
   686  
   687  	localState := resolver.LocalState{
   688  		State: operation.State{
   689  			Kind: operation.Continue,
   690  		},
   691  	}
   692  	remoteState := remotestate.Snapshot{
   693  		Relations: map[int]remotestate.RelationSnapshot{
   694  			1: {
   695  				Life: params.Alive,
   696  				Members: map[string]int64{
   697  					"wordpress": 1,
   698  				},
   699  			},
   700  		},
   701  	}
   702  	relationsResolver := relation.NewRelationsResolver(r)
   703  	_, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{})
   704  	c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation)
   705  }
   706  
   707  var (
   708  	noErrorResult  = params.ErrorResults{Results: []params.ErrorResult{{}}}
   709  	nrpeUnitTag    = names.NewUnitTag("nrpe/0")
   710  	nrpeUnitEntity = params.Entities{Entities: []params.Entity{{Tag: nrpeUnitTag.String()}}}
   711  )
   712  
   713  func subSubRelationAPICalls() []apiCall {
   714  	relationStatusResults := params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{
   715  		RelationResults: []params.RelationUnitStatus{{
   716  			RelationTag: "relation-wordpress:juju-info nrpe:general-info",
   717  			InScope:     true,
   718  		}, {
   719  			RelationTag: "relation-ntp:nrpe-external-master nrpe:external-master",
   720  			InScope:     true,
   721  		},
   722  		}}}}
   723  	relationUnits1 := params.RelationUnits{RelationUnits: []params.RelationUnit{
   724  		{Relation: "relation-wordpress.juju-info#nrpe.general-info", Unit: "unit-nrpe-0"},
   725  	}}
   726  	relationResults1 := params.RelationResults{
   727  		Results: []params.RelationResult{{
   728  			Id:               1,
   729  			Key:              "wordpress:juju-info nrpe:general-info",
   730  			Life:             params.Alive,
   731  			OtherApplication: "wordpress",
   732  			Endpoint: multiwatcher.Endpoint{
   733  				ApplicationName: "nrpe",
   734  				Relation: multiwatcher.CharmRelation{
   735  					Name:      "general-info",
   736  					Role:      string(charm.RoleRequirer),
   737  					Interface: "juju-info",
   738  					Scope:     "container",
   739  				},
   740  			},
   741  		}},
   742  	}
   743  	relationUnits2 := params.RelationUnits{RelationUnits: []params.RelationUnit{
   744  		{Relation: "relation-ntp.nrpe-external-master#nrpe.external-master", Unit: "unit-nrpe-0"},
   745  	}}
   746  	relationResults2 := params.RelationResults{
   747  		Results: []params.RelationResult{{
   748  			Id:               2,
   749  			Key:              "ntp:nrpe-external-master nrpe:external-master",
   750  			Life:             params.Alive,
   751  			OtherApplication: "ntp",
   752  			Endpoint: multiwatcher.Endpoint{
   753  				ApplicationName: "nrpe",
   754  				Relation: multiwatcher.CharmRelation{
   755  					Name:      "external-master",
   756  					Role:      string(charm.RoleRequirer),
   757  					Interface: "nrpe-external-master",
   758  					Scope:     "container",
   759  				},
   760  			},
   761  		}},
   762  	}
   763  	relationStatus1 := params.RelationStatusArgs{Args: []params.RelationStatusArg{{
   764  		UnitTag:    "unit-nrpe-0",
   765  		RelationId: 1,
   766  		Status:     params.Joined,
   767  	}}}
   768  	relationStatus2 := params.RelationStatusArgs{Args: []params.RelationStatusArg{{
   769  		UnitTag:    "unit-nrpe-0",
   770  		RelationId: 2,
   771  		Status:     params.Joined,
   772  	}}}
   773  
   774  	return []apiCall{
   775  		uniterAPICall("Refresh", nrpeUnitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil),
   776  		uniterAPICall("GetPrincipal", nrpeUnitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil),
   777  		uniterAPICall("RelationsStatus", nrpeUnitEntity, relationStatusResults, nil),
   778  		uniterAPICall("Relation", relationUnits1, relationResults1, nil),
   779  		uniterAPICall("Relation", relationUnits2, relationResults2, nil),
   780  		uniterAPICall("Relation", relationUnits1, relationResults1, nil),
   781  		uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil),
   782  		uniterAPICall("EnterScope", relationUnits1, noErrorResult, nil),
   783  		uniterAPICall("SetRelationStatus", relationStatus1, noErrorResult, nil),
   784  		uniterAPICall("Relation", relationUnits2, relationResults2, nil),
   785  		uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "2"}}}, nil),
   786  		uniterAPICall("EnterScope", relationUnits2, noErrorResult, nil),
   787  		uniterAPICall("SetRelationStatus", relationStatus2, noErrorResult, nil),
   788  	}
   789  }
   790  
   791  func (s *relationsSuite) TestSubSubPrincipalRelationDyingDestroysUnit(c *gc.C) {
   792  	// When two subordinate units are related on a principal unit's
   793  	// machine, the sub-sub relation shouldn't keep them alive if the
   794  	// relation to the principal dies.
   795  	var numCalls int32
   796  	apiCalls := subSubRelationAPICalls()
   797  	callsBeforeDestroy := int32(len(apiCalls))
   798  	callsAfterDestroy := callsBeforeDestroy + 1
   799  	// This should only be called once the relation to the
   800  	// principal app is destroyed.
   801  	apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntity, noErrorResult, nil))
   802  	apiCaller := mockAPICaller(c, &numCalls, apiCalls...)
   803  
   804  	st := uniter.NewState(apiCaller, nrpeUnitTag)
   805  	r, err := relation.NewRelations(
   806  		relation.RelationsConfig{
   807  			State:                st,
   808  			UnitTag:              nrpeUnitTag,
   809  			CharmDir:             s.stateDir,
   810  			RelationsDir:         s.relationsDir,
   811  			NewLeadershipContext: s.leadershipContextFunc,
   812  			Abort:                make(chan struct{}),
   813  		})
   814  	c.Assert(err, jc.ErrorIsNil)
   815  	assertNumCalls(c, &numCalls, callsBeforeDestroy)
   816  
   817  	// So now we have a relations object with two relations, one to
   818  	// wordpress and one to ntp. We want to ensure that if the
   819  	// relation to wordpress changes to Dying, the unit is destroyed,
   820  	// even if the ntp relation is still going strong.
   821  	localState := resolver.LocalState{
   822  		State: operation.State{
   823  			Kind: operation.Continue,
   824  		},
   825  	}
   826  
   827  	remoteState := remotestate.Snapshot{
   828  		Relations: map[int]remotestate.RelationSnapshot{
   829  			1: {
   830  				Life: params.Dying,
   831  				Members: map[string]int64{
   832  					"wordpress/0": 1,
   833  				},
   834  			},
   835  			2: {
   836  				Life: params.Alive,
   837  				Members: map[string]int64{
   838  					"ntp/0": 1,
   839  				},
   840  			},
   841  		},
   842  	}
   843  
   844  	rr := relation.NewRelationsResolver(r)
   845  	_, err = rr.NextOp(localState, remoteState, &mockOperations{})
   846  	c.Assert(err, jc.ErrorIsNil)
   847  
   848  	// Check that we've made the destroy unit call.
   849  	assertNumCalls(c, &numCalls, callsAfterDestroy)
   850  }
   851  
   852  func (s *relationsSuite) TestSubSubOtherRelationDyingNotDestroyed(c *gc.C) {
   853  	var numCalls int32
   854  	apiCalls := subSubRelationAPICalls()
   855  	// Sanity check: there shouldn't be a destroy at the end.
   856  	c.Assert(apiCalls[len(apiCalls)-1].request, gc.Not(gc.Equals), "Destroy")
   857  
   858  	expectedCalls := int32(len(apiCalls))
   859  	apiCaller := mockAPICaller(c, &numCalls, apiCalls...)
   860  
   861  	st := uniter.NewState(apiCaller, nrpeUnitTag)
   862  	r, err := relation.NewRelations(
   863  		relation.RelationsConfig{
   864  			State:                st,
   865  			UnitTag:              nrpeUnitTag,
   866  			CharmDir:             s.stateDir,
   867  			RelationsDir:         s.relationsDir,
   868  			NewLeadershipContext: s.leadershipContextFunc,
   869  			Abort:                make(chan struct{}),
   870  		})
   871  	c.Assert(err, jc.ErrorIsNil)
   872  	assertNumCalls(c, &numCalls, expectedCalls)
   873  
   874  	// So now we have a relations object with two relations, one to
   875  	// wordpress and one to ntp. We want to ensure that if the
   876  	// relation to ntp changes to Dying, the unit isn't destroyed,
   877  	// since it's kept alive by the principal relation.
   878  	localState := resolver.LocalState{
   879  		State: operation.State{
   880  			Kind: operation.Continue,
   881  		},
   882  	}
   883  
   884  	remoteState := remotestate.Snapshot{
   885  		Relations: map[int]remotestate.RelationSnapshot{
   886  			1: {
   887  				Life: params.Alive,
   888  				Members: map[string]int64{
   889  					"wordpress/0": 1,
   890  				},
   891  			},
   892  			2: {
   893  				Life: params.Dying,
   894  				Members: map[string]int64{
   895  					"ntp/0": 1,
   896  				},
   897  			},
   898  		},
   899  	}
   900  
   901  	rr := relation.NewRelationsResolver(r)
   902  	_, err = rr.NextOp(localState, remoteState, &mockOperations{})
   903  	c.Assert(err, jc.ErrorIsNil)
   904  
   905  	// Check that we didn't try to make a destroy call (the apiCaller
   906  	// should panic in that case anyway).
   907  	assertNumCalls(c, &numCalls, expectedCalls)
   908  }