petergtz / pegomock

Pegomock is a powerful, yet simple mocking framework for the Go programming language
Apache License 2.0
253 stars 28 forks source link

Issue with Matcher I do not understand (AssignableTo returns unexpected false) #42

Closed berupp closed 7 years ago

berupp commented 7 years ago

I am building a REST endpoint using gorilla/mux. I am passing an object into my http.Handlerfunc which will get enriched with the request's context when the endpoint is called.

Now the code sample at the bottom reproduces the issue. I also added a Println to matcher.go:

func (matcher *AnyMatcher) Matches(param Param) bool {
        matcher.actual = reflect.TypeOf(param)
        if matcher.actual == nil {
                switch matcher.Type.Kind() {
                case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
                        reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
                        return true
                default:
                        return false
                }
        }
        fmt.Printf("Matcher Type: %v - Param Type: %v -> matches %v\n", matcher.Type, matcher.actual, matcher.actual.AssignableTo(matcher.Type) )
        return matcher.actual.AssignableTo(matcher.Type)
}

The output, when I run the sample code below, is as follows:

Matcher Type: *context.valueCtx - Param Type: *context.valueCtx -> matches false
Shouldn't be nil <nil>

What I do not understand is: The types are the same, but AssignableTo returns false. I guess I am making a wrong assumption about reflect.TypeOf because my understanding is: If I reflect.TypeOf and object, the type should be completely independent from the content of the object. So why would the code fail? I am clueless.

The sample code:

package main
import(
    "github.com/petergtz/pegomock"
    "golang.org/x/net/context"
    "net/http"
    "fmt"
    "reflect"
    "net/http/httptest"
    "github.com/gorilla/mux"
)

func main() {
        //Create mock
    mockClient := NewMockSomeClient()
        //Return mock when Withontext is called with any context (`mux` creates a *context.valueCtx so does the Matcher)
    pegomock.When(mockClient.WithContext(AnyContext())).ThenReturn(mockClient)
    responseRecorder := httptest.NewRecorder()
    request, _ := http.NewRequest("GET", "/path/to/success", nil)

    r := mux.NewRouter()
    r.HandleFunc("/path/to/success", NewEndpoint(mockClient))
    r.ServeHTTP(responseRecorder, request)

}
func AnyContext() context.Context {
    ctx := context.WithValue(context.Background(), "", "")
    pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf(ctx)))
    return ctx
}

func NewEndpoint(client SomeClient) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request){
        clientWithContext := client.WithContext(r.Context())
        fmt.Printf("Shouldn't be nil %v", clientWithContext)
    }
}
//Interface to mock
type SomeClient interface {
    WithContext(context.Context) SomeClient
}

//MOCK - Generated but package adjusted, so it can be in the same file
type MockSomeClient struct {
    fail func(message string, callerSkip ...int)
}

func NewMockSomeClient() *MockSomeClient {
    return &MockSomeClient{fail: pegomock.GlobalFailHandler}
}

func (mock *MockSomeClient) WithContext(_param0 context.Context) SomeClient {
    params := []pegomock.Param{_param0}
    result := pegomock.GetGenericMockFrom(mock).Invoke("WithContext", params, []reflect.Type{reflect.TypeOf((*SomeClient)(nil)).Elem()})
    var ret0 SomeClient
    if len(result) != 0 {
        if result[0] != nil {
            ret0 = result[0].(SomeClient)
        }
    }
    return ret0
}

func (mock *MockSomeClient) VerifyWasCalledOnce() *VerifierSomeClient {
    return &VerifierSomeClient{mock, pegomock.Times(1), nil}
}

func (mock *MockSomeClient) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierSomeClient {
    return &VerifierSomeClient{mock, invocationCountMatcher, nil}
}

func (mock *MockSomeClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierSomeClient {
    return &VerifierSomeClient{mock, invocationCountMatcher, inOrderContext}
}

type VerifierSomeClient struct {
    mock                   *MockSomeClient
    invocationCountMatcher pegomock.Matcher
    inOrderContext         *pegomock.InOrderContext
}

func (verifier *VerifierSomeClient) WithContext(_param0 context.Context) *SomeClient_WithContext_OngoingVerification {
    params := []pegomock.Param{_param0}
    methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "WithContext", params)
    return &SomeClient_WithContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}

type SomeClient_WithContext_OngoingVerification struct {
    mock              *MockSomeClient
    methodInvocations []pegomock.MethodInvocation
}

func (c *SomeClient_WithContext_OngoingVerification) GetCapturedArguments() context.Context {
    _param0 := c.GetAllCapturedArguments()
    return _param0[len(_param0)-1]
}

func (c *SomeClient_WithContext_OngoingVerification) GetAllCapturedArguments() (_param0 []context.Context) {
    params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
    if len(params) > 0 {
        _param0 = make([]context.Context, len(params[0]))
        for u, param := range params[0] {
            _param0[u] = param.(context.Context)
        }
    }
    return
}
petergtz commented 7 years ago

Interesting. I just tried to reproduce this and couldn't. I copied your code verbatim (btw thanks, this made it trivial to reproduce!) and got this:

$ go run main.go 
Matcher Type: *context.valueCtx - Param Type: *context.valueCtx -> matches true
Shouldn't be nil &{<nil>}

To make that last line more readable, I changed it to use https://github.com/davecgh/go-spew:

func NewEndpoint(client SomeClient) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        clientWithContext := client.WithContext(r.Context())
        spew.Dump(clientWithContext)
    }
}

That gives me:

$ go run main.go 
Matcher Type: *context.valueCtx - Param Type: *context.valueCtx -> matches true
(*main.MockSomeClient)(0xc42009c040)({
 fail: (func(string, ...int)) <nil>
})

I'm using

$ go version
go version go1.7.3 linux/amd64

Is it possible that you're using a different go version?

berupp commented 7 years ago

Hi Peter Thanks for answering so quickly. I am puzzled. I ran the original tests on OS/X so, now I tried it on a linux VM but I am getting the same failure result.

-bash-4.2$ go run main.go
O: *context.valueCtx, P: *context.valueCtx - match false
(interface {}) <nil>
-bash-4.2$ go version
go version go1.7.3 linux/amd64
-bash-4.2$

I'm asking my coworkers to run it on their machines.

berupp commented 7 years ago

Okay so my coworkers ran it and get the same result as you. I set up go on my Windows machine and I also get the expected, correct result.

So something really odd seems to be going on with my Mac...anyway, closing.

Thanks for your help