Skip to content

Commit c004812

Browse files
committed
fix(dispatch): When registering, compare methods to impl
Registering MethodSets compares the MethodSet against the implementation to make sure they match each other. Add tests for dispatch to ensure common failure cases actually work.
1 parent f171fd8 commit c004812

File tree

3 files changed

+261
-30
lines changed

3 files changed

+261
-30
lines changed

lib/dispatch.go

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,60 +151,110 @@ type callable struct {
151151
// RegisterMethods iterates the methods provided by the lib API, and makes them visible to dispatch
152152
func (inst *Instance) RegisterMethods() {
153153
reg := make(map[string]callable)
154-
// TODO(dustmop): Change registerOne to take both the MethodSet and the Impl, validate
155-
// that their signatures agree.
156-
inst.registerOne("fsi", &FSIImpl{}, reg)
157-
inst.registerOne("access", accessImpl{}, reg)
154+
inst.registerOne("fsi", inst.Filesys(), fsiImpl{}, reg)
155+
inst.registerOne("access", inst.Access(), accessImpl{}, reg)
158156
inst.regMethods = &regMethodSet{reg: reg}
159157
}
160158

161-
func (inst *Instance) registerOne(ourName string, impl interface{}, reg map[string]callable) {
159+
func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interface{}, reg map[string]callable) {
162160
implType := reflect.TypeOf(impl)
161+
msetType := reflect.TypeOf(methods)
162+
methodMap := inst.buildMethodMap(methods)
163163
// Iterate methods on the implementation, register those that have the right signature
164164
num := implType.NumMethod()
165165
for k := 0; k < num; k++ {
166-
m := implType.Method(k)
167-
lowerName := strings.ToLower(m.Name)
166+
i := implType.Method(k)
167+
lowerName := strings.ToLower(i.Name)
168168
funcName := fmt.Sprintf("%s.%s", ourName, lowerName)
169169

170-
// Validate the parameters to the method
170+
// Validate the parameters to the implementation
171171
// should have 3 input parameters: (receiver, scope, input struct)
172172
// should have 2 output parametres: (output value, error)
173173
// TODO(dustmop): allow variadic returns: error only, cursor for pagination
174-
f := m.Type
174+
f := i.Type
175175
if f.NumIn() != 3 {
176-
log.Fatalf("%s: bad number of inputs: %d", funcName, f.NumIn())
176+
panic(fmt.Sprintf("%s: bad number of inputs: %d", funcName, f.NumIn()))
177177
}
178178
if f.NumOut() != 2 {
179-
log.Fatalf("%s: bad number of outputs: %d", funcName, f.NumOut())
179+
panic(fmt.Sprintf("%s: bad number of outputs: %d", funcName, f.NumOut()))
180180
}
181181
// First input must be the receiver
182182
inType := f.In(0)
183183
if inType != implType {
184-
log.Fatalf("%s: first input param should be impl, got %v", funcName, inType)
184+
panic(fmt.Sprintf("%s: first input param should be impl, got %v", funcName, inType))
185185
}
186186
// Second input must be a scope
187187
inType = f.In(1)
188188
if inType.Name() != "scope" {
189-
log.Fatalf("%s: second input param should be scope, got %v", funcName, inType)
189+
panic(fmt.Sprintf("%s: second input param should be scope, got %v", funcName, inType))
190190
}
191191
// Third input is a pointer to the input struct
192192
inType = f.In(2)
193193
if inType.Kind() != reflect.Ptr {
194-
log.Fatalf("%s: third input param must be a struct pointer, got %v", funcName, inType)
194+
panic(fmt.Sprintf("%s: third input param must be a struct pointer, got %v", funcName, inType))
195195
}
196196
inType = inType.Elem()
197197
if inType.Kind() != reflect.Struct {
198-
log.Fatalf("%s: third input param must be a struct pointer, got %v", funcName, inType)
198+
panic(fmt.Sprintf("%s: third input param must be a struct pointer, got %v", funcName, inType))
199199
}
200200
// First output is anything
201201
outType := f.Out(0)
202202
// Second output must be an error
203203
outErrType := f.Out(1)
204204
if outErrType.Name() != "error" {
205-
log.Fatalf("%s: second output param should be error, got %v", funcName, outErrType)
205+
panic(fmt.Sprintf("%s: second output param should be error, got %v", funcName, outErrType))
206+
}
207+
208+
// Validate the parameters to the method that matches the implementation
209+
// should have 3 input parameters: (receiver, context.Context, input struct [same as impl])
210+
// should have 2 output parametres: (output value [same as impl], error)
211+
m, ok := methodMap[i.Name]
212+
if !ok {
213+
panic(fmt.Sprintf("method %s not found on MethodSet", i.Name))
214+
}
215+
f = m.Type
216+
if f.NumIn() != 3 {
217+
panic(fmt.Sprintf("%s: bad number of inputs: %d", funcName, f.NumIn()))
218+
}
219+
msetNumMethods := f.NumOut()
220+
if msetNumMethods < 1 && msetNumMethods > 2 {
221+
panic(fmt.Sprintf("%s: bad number of outputs: %d", funcName, f.NumOut()))
222+
}
223+
// First input must be the receiver
224+
mType := f.In(0)
225+
if mType.Name() != msetType.Name() {
226+
panic(fmt.Sprintf("%s: first input param should be impl, got %v", funcName, mType))
227+
}
228+
// Second input must be a context
229+
mType = f.In(1)
230+
if mType.Name() != "Context" {
231+
panic(fmt.Sprintf("%s: second input param should be context.Context, got %v", funcName, mType))
232+
}
233+
// Third input is a pointer to the input struct
234+
mType = f.In(2)
235+
if mType.Kind() != reflect.Ptr {
236+
panic(fmt.Sprintf("%s: third input param must be a pointer, got %v", funcName, mType))
237+
}
238+
mType = mType.Elem()
239+
if mType != inType {
240+
panic(fmt.Sprintf("%s: third input param must match impl, expect %v, got %v", funcName, inType, mType))
241+
}
242+
// First output, if there's more than 1, matches the impl output
243+
if msetNumMethods == 2 {
244+
mType = f.Out(0)
245+
if mType != outType {
246+
panic(fmt.Sprintf("%s: first output param must match impl, expect %v, got %v", funcName, outType, mType))
247+
}
248+
}
249+
// Last output must be an error
250+
mType = f.Out(msetNumMethods - 1)
251+
if mType.Name() != "error" {
252+
panic(fmt.Sprintf("%s: last output param should be error, got %v", funcName, mType))
206253
}
207254

255+
// Remove this method from the methodSetMap now that it has been processed
256+
delete(methodMap, i.Name)
257+
208258
// Save the method to the registration table
209259
reg[funcName] = callable{
210260
Impl: impl,
@@ -214,6 +264,23 @@ func (inst *Instance) registerOne(ourName string, impl interface{}, reg map[stri
214264
}
215265
log.Debugf("%d: registered %s(*%s) %v", k, funcName, inType, outType)
216266
}
267+
268+
for k := range methodMap {
269+
if k != "Name" {
270+
panic(fmt.Sprintf("%s: did not find implementation for method %s", msetType, k))
271+
}
272+
}
273+
}
274+
275+
func (inst *Instance) buildMethodMap(impl interface{}) map[string]reflect.Method {
276+
result := make(map[string]reflect.Method)
277+
implType := reflect.TypeOf(impl)
278+
num := implType.NumMethod()
279+
for k := 0; k < num; k++ {
280+
m := implType.Method(k)
281+
result[m.Name] = m
282+
}
283+
return result
217284
}
218285

219286
// MethodSet represents a set of methods to be registered

lib/dispatch_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package lib
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
)
8+
9+
func TestRegisterMethods(t *testing.T) {
10+
ctx := context.Background()
11+
12+
inst, cleanup := NewMemTestInstance(ctx, t)
13+
defer cleanup()
14+
m := &animalMethods{d: inst}
15+
16+
reg := make(map[string]callable)
17+
inst.registerOne("animal", m, animalImpl{}, reg)
18+
19+
expectToPanic(t, func() {
20+
reg = make(map[string]callable)
21+
inst.registerOne("animal", m, badAnimalOneImpl{}, reg)
22+
}, "animal.cat: bad number of outputs: 1")
23+
24+
expectToPanic(t, func() {
25+
reg = make(map[string]callable)
26+
inst.registerOne("animal", m, badAnimalTwoImpl{}, reg)
27+
}, "method Doggie not found on MethodSet")
28+
29+
expectToPanic(t, func() {
30+
reg = make(map[string]callable)
31+
inst.registerOne("animal", m, badAnimalThreeImpl{}, reg)
32+
}, "animal.cat: second input param should be scope, got context.Context")
33+
34+
expectToPanic(t, func() {
35+
reg = make(map[string]callable)
36+
inst.registerOne("animal", m, badAnimalFourImpl{}, reg)
37+
}, "animal.dog: third input param must be a struct pointer, got string")
38+
39+
expectToPanic(t, func() {
40+
reg = make(map[string]callable)
41+
inst.registerOne("animal", m, badAnimalFiveImpl{}, reg)
42+
}, "*lib.animalMethods: did not find implementation for method Dog")
43+
}
44+
45+
func expectToPanic(t *testing.T, regFunc func(), expectMessage string) {
46+
t.Helper()
47+
48+
doneCh := make(chan error)
49+
panicMessage := ""
50+
51+
go func() {
52+
defer func() {
53+
if r := recover(); r != nil {
54+
panicMessage = fmt.Sprintf("%s", r)
55+
}
56+
doneCh <- nil
57+
}()
58+
regFunc()
59+
}()
60+
// Block until the goroutine is done
61+
_ = <- doneCh
62+
63+
if panicMessage == "" {
64+
t.Errorf("expected a panic, did not get one")
65+
} else if panicMessage != expectMessage {
66+
t.Errorf("error mismatch, expect: %q, got: %q", expectMessage, panicMessage)
67+
}
68+
}
69+
70+
// Test data: methodSet and implementation
71+
72+
type animalMethods struct {
73+
d dispatcher
74+
}
75+
76+
func (m *animalMethods) Name() string {
77+
return "animal"
78+
}
79+
80+
type animalParams struct {
81+
Name string
82+
}
83+
84+
func (m *animalMethods) Cat(ctx context.Context, p *animalParams) (string, error) {
85+
got, err := m.d.Dispatch(ctx, dispatchMethodName(m, "cat"), p)
86+
if res, ok := got.(string); ok {
87+
return res, err
88+
}
89+
return "", dispatchReturnError(got, err)
90+
}
91+
92+
func (m *animalMethods) Dog(ctx context.Context, p *animalParams) (string, error) {
93+
got, err := m.d.Dispatch(ctx, dispatchMethodName(m, "dog"), p)
94+
if res, ok := got.(string); ok {
95+
return res, err
96+
}
97+
return "", dispatchReturnError(got, err)
98+
}
99+
100+
// Good implementation
101+
102+
type animalImpl struct {}
103+
104+
func (animalImpl) Cat(scp scope, p *animalParams) (string, error) {
105+
return fmt.Sprintf("%s says meow", p.Name), nil
106+
}
107+
108+
func (animalImpl) Dog(scp scope, p *animalParams) (string, error) {
109+
return fmt.Sprintf("%s says bark", p.Name), nil
110+
}
111+
112+
// Bad implementation #1 (cat doesn't return an error)
113+
114+
type badAnimalOneImpl struct {}
115+
116+
func (badAnimalOneImpl) Cat(scp scope, p *animalParams) string {
117+
return fmt.Sprintf("%s says meow", p.Name)
118+
}
119+
120+
func (badAnimalOneImpl) Dog(scp scope, p *animalParams) (string, error) {
121+
return fmt.Sprintf("%s says bark", p.Name), nil
122+
}
123+
124+
// Bad implementation #2 (dog method name doesn't match)
125+
126+
type badAnimalTwoImpl struct {}
127+
128+
func (badAnimalTwoImpl) Cat(scp scope, p *animalParams) (string, error) {
129+
return fmt.Sprintf("%s says meow", p.Name), nil
130+
}
131+
132+
func (badAnimalTwoImpl) Doggie(scp scope, p *animalParams) (string, error) {
133+
return fmt.Sprintf("%s says bark", p.Name), nil
134+
}
135+
136+
// Bad implementation #3 (cat doesn't accept a scope)
137+
138+
type badAnimalThreeImpl struct {}
139+
140+
func (badAnimalThreeImpl) Cat(ctx context.Context, p *animalParams) (string, error) {
141+
return fmt.Sprintf("%s says meow", p.Name), nil
142+
}
143+
144+
func (badAnimalThreeImpl) Dog(scp scope, p *animalParams) (string, error) {
145+
return fmt.Sprintf("%s says bark", p.Name), nil
146+
}
147+
148+
// Bad implementation #4 (dog input struct doesn't match)
149+
150+
type badAnimalFourImpl struct {}
151+
152+
func (badAnimalFourImpl) Cat(scp scope, p *animalParams) (string, error) {
153+
return fmt.Sprintf("%s says meow", p.Name), nil
154+
}
155+
156+
func (badAnimalFourImpl) Dog(scp scope, name string) (string, error) {
157+
return fmt.Sprintf("%s says bark", name), nil
158+
}
159+
160+
// Bad implementation #5 (dog method is missing)
161+
162+
type badAnimalFiveImpl struct {}
163+
164+
func (badAnimalFiveImpl) Cat(scp scope, p *animalParams) (string, error) {
165+
return fmt.Sprintf("%s says meow", p.Name), nil
166+
}

0 commit comments

Comments
 (0)