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  }