Skip to content

Commit

Permalink
api/base/testing: simplify API call checkers
Browse files Browse the repository at this point in the history
With the upcoming changes to command mocking, we're going to be
using API call checkers quite a bit more, so simplify the API
and make it a little more general. Specifically, we

- integrate CheckingAPICallerMultiArgs and CheckingAPICaller into one
- make NotifyingCheckingAPICaller just do notification
on an APICaller implementation without mixing that with the checking
logic
- make the error return a part of an individual call rather
than separate.
- include the call count in the returned APICaller implementation
rather than a separate argument.
  • Loading branch information
rogpeppe committed Apr 19, 2017
1 parent 80780c9 commit 2d5b67c
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 271 deletions.
194 changes: 102 additions & 92 deletions api/base/testing/apicaller.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,107 +53,117 @@ func (APICallerFunc) ConnectControllerStream(path string, attrs url.Values, head
return nil, errors.NotImplementedf("controller stream connection")
}

// CheckArgs holds the possible arguments to CheckingAPICaller(). Any
// fields non empty fields will be checked to match the arguments
// recieved by the APICall() method of the returned APICallerFunc. If
// Id is empty, but IdIsEmpty is true, the id argument is checked to
// be empty. The same applies to Version being empty, but if
// VersionIsZero set to true the version is checked to be 0.
type CheckArgs struct {
Facade string
// CallChecker is an APICaller implementation that checks
// calls as they are made.
type CallChecker struct {
APICallerFunc

// CallCount records the current call count.
CallCount int
}

// APICall describes an expected API call.
type APICall struct {
// If Check is non-nil, all other fields will be ignored and Check
// will be called to check the call.
Check func(objType string, version int, id, request string, params, response interface{}) error

// Facade holds the expected call facade. If it's empty,
// any facade will be accepted.
Facade string

// Version holds the expected call version. If it's zero,
// any version will be accepted unless VersionIsZero is true.
Version int
Id string
Method string
Args interface{}
Results interface{}

IdIsEmpty bool
// VersionIsZero holds whether the version is expected to be zero.
VersionIsZero bool
}

func checkArgs(c *gc.C, args *CheckArgs, facade string, version int, id, method string, inArgs, outResults interface{}) {
if args == nil {
c.Logf("checkArgs: args is nil!")
return
} else {
if args.Facade != "" {
c.Check(facade, gc.Equals, args.Facade)
}
if args.Version != 0 {
c.Check(version, gc.Equals, args.Version)
} else if args.VersionIsZero {
c.Check(version, gc.Equals, 0)
}
if args.Id != "" {
c.Check(id, gc.Equals, args.Id)
} else if args.IdIsEmpty {
c.Check(id, gc.Equals, "")
}
if args.Method != "" {
c.Check(method, gc.Equals, args.Method)
}
if args.Args != nil {
c.Check(inArgs, jc.DeepEquals, args.Args)
}
if args.Results != nil {
c.Check(outResults, gc.NotNil)
testing.PatchValue(outResults, args.Results)
// Id holds the expected call id. If it's empty, any id will be
// accepted unless IdIsEmpty is true.
Id string

// IdIsEmpty holds whether the call id is expected to be empty.
IdIsEmpty bool

// Method holds the expected method.
Method string

// Args holds the expected value of the call's argument.
Args interface{}

// Results is assigned to the result parameter of the call on return.
Results interface{}

// Error is returned from the call.
Error error
}

// APICallChecker returns an APICaller implementation that checks
// API calls. Each element of calls corresponds to an expected
// API call. If more calls are made than there are elements, they
// will not be checked - check the value of the Count field
// to ensure that the expected number of calls have been made.
//
// Note that the returned value is not thread-safe - do not
// use it if the client is making concurrent calls.
func APICallChecker(c *gc.C, calls ...APICall) *CallChecker {
var checker CallChecker
checker.APICallerFunc = func(facade string, version int, id, method string, inArgs, outResults interface{}) error {
call := checker.CallCount
checker.CallCount++
if call >= len(calls) {
return nil
}
return checkArgs(c, calls[call], facade, version, id, method, inArgs, outResults)
}
return &checker
}

func checkArgs(c *gc.C, args APICall, facade string, version int, id, method string, inArgs, outResults interface{}) error {
if args.Facade != "" {
c.Check(facade, gc.Equals, args.Facade)
}
if args.Version != 0 {
c.Check(version, gc.Equals, args.Version)
} else if args.VersionIsZero {
c.Check(version, gc.Equals, 0)
}
if args.Id != "" {
c.Check(id, gc.Equals, args.Id)
} else if args.IdIsEmpty {
c.Check(id, gc.Equals, "")
}
if args.Method != "" {
c.Check(method, gc.Equals, args.Method)
}
if args.Args != nil {
c.Check(inArgs, jc.DeepEquals, args.Args)
}
if args.Results != nil {
c.Check(outResults, gc.NotNil)
testing.PatchValue(outResults, args.Results)
}
return args.Error
}

type notifyingAPICaller struct {
base.APICaller
called chan<- struct{}
}

func (c notifyingAPICaller) APICall(objType string, version int, id, request string, params, response interface{}) error {
c.called <- struct{}{}
return c.APICaller.APICall(objType, version, id, request, params, response)
}

// CheckingAPICaller returns an APICallerFunc which can report the
// number of times its APICall() method was called (if numCalls is not
// nil), as well as check if any of the arguments passed to the
// APICall() method match the values given in args (if args itself is
// not nil, otherwise no arguments are checked). The final error
// result of the APICall() will be set to err.
func CheckingAPICaller(c *gc.C, args *CheckArgs, numCalls *int, err error) base.APICallCloser {
return APICallerFunc(
func(facade string, version int, id, method string, inArgs, outResults interface{}) error {
if numCalls != nil {
*numCalls++
}
if args != nil {
checkArgs(c, args, facade, version, id, method, inArgs, outResults)
}
return err
},
)
}

// NotifyingCheckingAPICaller returns an APICallerFunc which sends a message on the channel "called" every
// time it recives a call, as well as check if any of the arguments passed to the APICall() method match
// the values given in args (if args itself is not nil, otherwise no arguments are checked). The final
// error result of the APICall() will be set to err.
func NotifyingCheckingAPICaller(c *gc.C, args *CheckArgs, called chan struct{}, err error) base.APICaller {
return APICallerFunc(
func(facade string, version int, id, method string, inArgs, outResults interface{}) error {
called <- struct{}{}
if args != nil {
checkArgs(c, args, facade, version, id, method, inArgs, outResults)
}
return err
},
)
}

// CheckingAPICallerMultiArgs checks each call against the indexed expected argument. Once expected
// arguments run out it doesn't check them. This is useful if your test continues to make calls after
// you have checked the ones you care about.
func CheckingAPICallerMultiArgs(c *gc.C, args []CheckArgs, numCalls *int, err error) base.APICallCloser {
if numCalls == nil {
panic("numCalls must be non-nill")
// NotifyingAPICaller returns an APICaller implementation which sends a
// message on the given channel every time it receives a call.
func NotifyingAPICaller(c *gc.C, called chan<- struct{}, caller base.APICaller) base.APICaller {
return notifyingAPICaller{
APICaller: caller,
called: called,
}
return APICallerFunc(
func(facade string, version int, id, method string, inArgs, outResults interface{}) error {
if len(args) > *numCalls {
checkArgs(c, &args[*numCalls], facade, version, id, method, inArgs, outResults)
}
*numCalls++
return err
},
)
}

// StubFacadeCaller is a testing stub implementation of api/base.FacadeCaller.
Expand Down
33 changes: 17 additions & 16 deletions api/cleaner/cleaner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,24 @@ var _ = gc.Suite(&CleanerSuite{})

type TestCommon struct {
apiCaller base.APICaller
apiArgs apitesting.CheckArgs
called chan struct{}
api *cleaner.API
}

// Init returns a new, initialised instance of TestCommon.
func Init(c *gc.C, method string, expectArgs, useResults interface{}, err error) (t *TestCommon) {
func Init(c *gc.C, facade, method string, expectArgs, useResults interface{}, err error) (t *TestCommon) {
t = &TestCommon{}
t.apiArgs = apitesting.CheckArgs{
Facade: "Cleaner",
caller := apitesting.APICallChecker(c, apitesting.APICall{
Facade: facade,
VersionIsZero: true,
IdIsEmpty: true,
Method: method,
Args: expectArgs,
Results: useResults,
}

Error: err,
})
t.called = make(chan struct{}, 100)
t.apiCaller = apitesting.NotifyingCheckingAPICaller(c, &t.apiArgs, t.called, err)
t.apiCaller = apitesting.NotifyingAPICaller(c, t.called, caller)
t.api = cleaner.NewAPI(t.apiCaller)

c.Check(t.api, gc.NotNil)
Expand All @@ -63,33 +62,35 @@ func AssertNumReceives(c *gc.C, watched chan struct{}, expected uint32) {
c.Errorf("timeout while waiting for a call")
}
}

time.Sleep(coretesting.ShortWait)
c.Assert(receives, gc.Equals, expected)
select {
case <-watched:
c.Fatalf("unexpected event received")
case <-time.After(coretesting.ShortWait):
}
}

func (s *CleanerSuite) TestNewAPI(c *gc.C) {
Init(c, "", nil, nil, nil)
Init(c, "Cleaner", "", nil, nil, nil)
}

func (s *CleanerSuite) TestWatchCleanups(c *gc.C) {
t := Init(c, "", nil, nil, nil)
t.apiArgs.Facade = "" // Multiple facades are called, so we can't check this.
// Multiple facades are called, so pass an empty string for the facade.
t := Init(c, "", "", nil, nil, nil)
m, err := t.api.WatchCleanups()
AssertNumReceives(c, t.called, 2)
c.Assert(err, jc.ErrorIsNil)
c.Assert(m, gc.NotNil)
}

func (s *CleanerSuite) TestCleanup(c *gc.C) {
t := Init(c, "Cleanup", nil, nil, nil)
t := Init(c, "Cleaner", "Cleanup", nil, nil, nil)
err := t.api.Cleanup()
AssertNumReceives(c, t.called, 1)
c.Assert(err, jc.ErrorIsNil)
}

func (s *CleanerSuite) TestWatchCleanupsFailFacadeCall(c *gc.C) {
t := Init(c, "WatchCleanups", nil, nil, errors.New("client error!"))
t := Init(c, "Cleaner", "WatchCleanups", nil, nil, errors.New("client error!"))
m, err := t.api.WatchCleanups()
c.Assert(err, gc.ErrorMatches, "client error!")
AssertNumReceives(c, t.called, 1)
Expand All @@ -103,7 +104,7 @@ func (s *CleanerSuite) TestWatchCleanupsFailFacadeResult(c *gc.C) {
p := params.NotifyWatchResult{
Error: &e,
}
t := Init(c, "WatchCleanups", nil, p, nil)
t := Init(c, "Cleaner", "WatchCleanups", nil, p, nil)
m, err := t.api.WatchCleanups()
AssertNumReceives(c, t.called, 1)
c.Assert(err, gc.ErrorMatches, e.Message)
Expand Down
Loading

0 comments on commit 2d5b67c

Please sign in to comment.