This is an automated email from the ASF dual-hosted git repository. alexstocks pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
commit 7fadaa8d3160728254e049b3b8c2a7679b6b48c1 Author: Aether <[email protected]> AuthorDate: Mon Dec 22 07:35:30 2025 +0800 chore(test): add unit tests for registry and proxy (#3126) Signed-off-by: Aetherance <[email protected]> Co-authored-by: Aetherance <[email protected]> --- proxy/proxy_factory/invoker_test.go | 149 +++++++++++++++++++++++++++ proxy/proxy_factory/utils_test.go | 110 ++++++++++++++++++++ registry/base_configuration_listener_test.go | 79 ++++++++++++++ registry/options_test.go | 89 ++++++++++++++++ registry/service_instance_test.go | 132 ++++++++++++++++++++++++ 5 files changed, 559 insertions(+) diff --git a/proxy/proxy_factory/invoker_test.go b/proxy/proxy_factory/invoker_test.go new file mode 100644 index 000000000..11230b0c2 --- /dev/null +++ b/proxy/proxy_factory/invoker_test.go @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package proxy_factory + +import ( + "context" + "fmt" + "net/url" + "strings" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "dubbo.apache.org/dubbo-go/v3/common" + "dubbo.apache.org/dubbo-go/v3/common/constant" + "dubbo.apache.org/dubbo-go/v3/protocol/base" + "dubbo.apache.org/dubbo-go/v3/protocol/invocation" +) + +type ProxyInvokerService struct{} + +func (s *ProxyInvokerService) Hello(_ context.Context, name string) (string, error) { + return "hello:" + name, nil +} + +type PassThroughService struct{} + +func (s *PassThroughService) Service(method string, argTypes []string, args [][]byte, attachments map[string]any) (any, error) { + body := "" + if len(args) > 0 { + body = string(args[0]) + } + return fmt.Sprintf("%s|%s|%s", method, strings.Join(argTypes, ","), body), nil +} + +func registerService(t *testing.T, protocol, interfaceName string, svc common.RPCService) { + t.Helper() + _, err := common.ServiceMap.Register(interfaceName, protocol, "", "", svc) + assert.NoError(t, err) + t.Cleanup(func() { + _ = common.ServiceMap.UnRegister(interfaceName, protocol, common.ServiceKey(interfaceName, "", "")) + }) +} + +func newURL(protocol, interfaceName string) *common.URL { + return common.NewURLWithOptions( + common.WithProtocol(protocol), + common.WithPath(interfaceName), + common.WithInterface(interfaceName), + common.WithParams(url.Values{constant.InterfaceKey: {interfaceName}}), + ) +} + +func TestProxyInvoker_Invoke(t *testing.T) { + const ( + protocol = "test-protocol" + interfaceName = "ProxyInvokerService" + ) + registerService(t, protocol, interfaceName, &ProxyInvokerService{}) + u := newURL(protocol, interfaceName) + invoker := &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(u)} + + t.Run("invoke success", func(t *testing.T) { + inv := invocation.NewRPCInvocationWithOptions( + invocation.WithMethodName("Hello"), + invocation.WithArguments([]any{"world"}), + invocation.WithAttachments(map[string]any{"trace": "t1"}), + ) + result := invoker.Invoke(context.Background(), inv) + assert.NoError(t, result.Error()) + assert.Equal(t, "hello:world", result.Result()) + assert.Equal(t, "t1", result.Attachments()["trace"]) + }) + + t.Run("method not found", func(t *testing.T) { + inv := invocation.NewRPCInvocationWithOptions( + invocation.WithMethodName("Missing"), + invocation.WithArguments([]any{}), + ) + result := invoker.Invoke(context.Background(), inv) + assert.Error(t, result.Error()) + }) + + t.Run("service not found", func(t *testing.T) { + absentURL := newURL(protocol, "UnknownService") + absentInvoker := &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(absentURL)} + inv := invocation.NewRPCInvocationWithOptions( + invocation.WithMethodName("Hello"), + invocation.WithArguments([]any{}), + ) + result := absentInvoker.Invoke(context.Background(), inv) + assert.Error(t, result.Error()) + }) +} + +func TestPassThroughProxyInvoker_Invoke(t *testing.T) { + const ( + protocol = "pass-protocol" + interfaceName = "PassThroughService" + ) + registerService(t, protocol, interfaceName, &PassThroughService{}) + u := newURL(protocol, interfaceName) + invoker := &PassThroughProxyInvoker{ + ProxyInvoker: &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(u)}, + } + + t.Run("pass through success", func(t *testing.T) { + inv := invocation.NewRPCInvocationWithOptions( + invocation.WithMethodName("RawMethod"), + invocation.WithArguments([]any{[]byte("payload")}), + invocation.WithAttachments(map[string]any{ + constant.ParamsTypeKey: []string{"bytes"}, + "trace": "abc", + }), + ) + result := invoker.Invoke(context.Background(), inv) + assert.NoError(t, result.Error()) + assert.Equal(t, "RawMethod|bytes|payload", result.Result()) + assert.Equal(t, "abc", result.Attachments()["trace"]) + }) + + t.Run("argument type mismatch", func(t *testing.T) { + inv := invocation.NewRPCInvocationWithOptions( + invocation.WithMethodName("RawMethod"), + invocation.WithArguments([]any{"not-bytes"}), + ) + result := invoker.Invoke(context.Background(), inv) + assert.EqualError(t, result.Error(), "the param type is not []byte") + }) +} diff --git a/proxy/proxy_factory/utils_test.go b/proxy/proxy_factory/utils_test.go new file mode 100644 index 000000000..4afd7fdc3 --- /dev/null +++ b/proxy/proxy_factory/utils_test.go @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package proxy_factory + +import ( + "errors" + "reflect" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +type callLocalMethodSample struct{} + +func (s *callLocalMethodSample) Sum(a, b int) int { + return a + b +} + +func (s *callLocalMethodSample) PanicError() { + panic(errors.New("boom")) +} + +func (s *callLocalMethodSample) PanicString() { + panic("boom str") +} + +func (s *callLocalMethodSample) PanicUnknown() { + panic(123) +} + +func TestCallLocalMethod(t *testing.T) { + sample := &callLocalMethodSample{} + cases := []struct { + name string + method string + in []reflect.Value + assertErr func(t *testing.T, err error) + assertOut func(t *testing.T, out []reflect.Value) + }{ + { + name: "call success", + method: "Sum", + in: []reflect.Value{reflect.ValueOf(sample), reflect.ValueOf(1), reflect.ValueOf(2)}, + assertErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + assertOut: func(t *testing.T, out []reflect.Value) { + assert.Len(t, out, 1) + assert.Equal(t, 3, out[0].Interface()) + }, + }, + { + name: "panic with error", + method: "PanicError", + in: []reflect.Value{reflect.ValueOf(sample)}, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "boom") + }, + }, + { + name: "panic with string", + method: "PanicString", + in: []reflect.Value{reflect.ValueOf(sample)}, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "boom str") + }, + }, + { + name: "panic with unknown type", + method: "PanicUnknown", + in: []reflect.Value{reflect.ValueOf(sample)}, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "invoke function error, unknow exception: 123") + }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + m, ok := reflect.TypeOf(sample).MethodByName(tt.method) + if !ok { + t.Fatalf("method %s not found", tt.method) + } + out, err := callLocalMethod(m, tt.in) + if tt.assertErr != nil { + tt.assertErr(t, err) + } + if tt.assertOut != nil { + tt.assertOut(t, out) + } + }) + } +} diff --git a/registry/base_configuration_listener_test.go b/registry/base_configuration_listener_test.go new file mode 100644 index 000000000..c5e414b69 --- /dev/null +++ b/registry/base_configuration_listener_test.go @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package registry + +import ( + "net/url" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "dubbo.apache.org/dubbo-go/v3/common" + "dubbo.apache.org/dubbo-go/v3/common/constant" + "dubbo.apache.org/dubbo-go/v3/config_center" +) + +type testConfigurator struct { + url *common.URL + configured bool +} + +func (c *testConfigurator) GetUrl() *common.URL { + return c.url +} + +func (c *testConfigurator) Configure(_ *common.URL) { + c.configured = true +} + +func TestToConfigurators(t *testing.T) { + makeConfigurator := func(u *common.URL) config_center.Configurator { + return &testConfigurator{url: u} + } + + assert.Nil(t, ToConfigurators(nil, makeConfigurator)) + + emptyProtocolURL := common.NewURLWithOptions(common.WithProtocol(constant.EmptyProtocol)) + assert.Empty(t, ToConfigurators([]*common.URL{emptyProtocolURL}, makeConfigurator)) + + anyhostOnly := common.NewURLWithOptions(common.WithParams(url.Values{ + constant.AnyhostKey: {"true"}, + })) + assert.Nil(t, ToConfigurators([]*common.URL{anyhostOnly}, makeConfigurator)) + + validURL := common.NewURLWithOptions(common.WithProtocol("override"), common.WithParams(url.Values{ + constant.AnyhostKey: {"true"}, + "timeout": {"2s"}, + })) + configurators := ToConfigurators([]*common.URL{validURL}, makeConfigurator) + assert.Len(t, configurators, 1) + assert.Equal(t, validURL, configurators[0].GetUrl()) +} + +func TestBaseConfigurationListenerOverrideUrl(t *testing.T) { + cfg := &testConfigurator{} + bcl := &BaseConfigurationListener{configurators: []config_center.Configurator{cfg}} + target := common.NewURLWithOptions() + + bcl.OverrideUrl(target) + assert.True(t, cfg.configured) +} diff --git a/registry/options_test.go b/registry/options_test.go new file mode 100644 index 000000000..ece7f6a04 --- /dev/null +++ b/registry/options_test.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package registry + +import ( + "testing" + "time" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "dubbo.apache.org/dubbo-go/v3/common/constant" +) + +func TestNewOptionsRequireProtocol(t *testing.T) { + assert.Panics(t, func() { + NewOptions() + }) +} + +func TestNewOptionsWithHelpers(t *testing.T) { + tests := []struct { + name string + opts []Option + wantProtocol string + wantID string + wantTimeout string + wantAddress string + }{ + { + name: "zookeeper default id", + opts: []Option{WithZookeeper()}, + wantProtocol: constant.ZookeeperKey, + wantID: constant.ZookeeperKey, + }, + { + name: "etcd with custom id", + opts: []Option{WithEtcdV3(), WithID("custom-id")}, + wantProtocol: constant.EtcdV3Key, + wantID: "custom-id", + }, + { + name: "address overrides protocol", + opts: []Option{WithAddress("nacos://127.0.0.1:8848")}, + wantProtocol: constant.NacosKey, + wantID: constant.NacosKey, + wantAddress: "nacos://127.0.0.1:8848", + }, + { + name: "timeout option", + opts: []Option{WithZookeeper(), WithTimeout(3 * time.Second)}, + wantProtocol: constant.ZookeeperKey, + wantID: constant.ZookeeperKey, + wantTimeout: "3s", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := NewOptions(tt.opts...) + assert.Equal(t, tt.wantProtocol, options.Registry.Protocol) + assert.Equal(t, tt.wantID, options.ID) + if tt.wantTimeout != "" { + assert.Equal(t, tt.wantTimeout, options.Registry.Timeout) + } + if tt.wantAddress != "" { + assert.Equal(t, tt.wantAddress, options.Registry.Address) + } + }) + } +} diff --git a/registry/service_instance_test.go b/registry/service_instance_test.go new file mode 100644 index 000000000..2e42e74e2 --- /dev/null +++ b/registry/service_instance_test.go @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package registry + +import ( + "fmt" + "net/url" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "dubbo.apache.org/dubbo-go/v3/common" + "dubbo.apache.org/dubbo-go/v3/common/constant" + "dubbo.apache.org/dubbo-go/v3/metadata/info" +) + +func TestDefaultServiceInstance_GetAddressAndWeight(t *testing.T) { + inst := &DefaultServiceInstance{Host: "127.0.0.1", Port: 20880} + assert.Equal(t, "127.0.0.1:20880", inst.GetAddress()) + + instNoPort := &DefaultServiceInstance{Host: "127.0.0.1"} + assert.Equal(t, "127.0.0.1", instNoPort.GetAddress()) + + instWithAddress := &DefaultServiceInstance{Address: "custom"} + assert.Equal(t, "custom", instWithAddress.GetAddress()) + + assert.EqualValues(t, constant.DefaultWeight, (&DefaultServiceInstance{}).GetWeight()) + assert.Equal(t, int64(50), (&DefaultServiceInstance{Weight: 50}).GetWeight()) +} + +func TestDefaultServiceInstance_ToURLsWithEndpoints(t *testing.T) { + serviceURL := common.NewURLWithOptions( + common.WithProtocol("tri"), + common.WithIp("127.0.0.1"), + common.WithPort("2000"), + common.WithPath("DemoService"), + common.WithInterface("DemoService"), + common.WithMethods([]string{"SayHello"}), + common.WithParams(url.Values{constant.WeightKey: {"0"}}), + ) + serviceInfo := info.NewServiceInfoWithURL(serviceURL) + + t.Run("pick matching endpoint", func(t *testing.T) { + instance := &DefaultServiceInstance{ + Host: "127.0.0.1", + Port: 20880, + Metadata: map[string]string{ + constant.ServiceInstanceEndpoints: `[{"port":3000,"protocol":"tri"},{"port":3001,"protocol":"rest"}]`, + }, + Tag: "gray", + } + + urls := instance.ToURLs(serviceInfo) + assert.Len(t, urls, 1) + got := urls[0] + assert.Equal(t, "tri", got.Protocol) + assert.Equal(t, "127.0.0.1", got.Ip) + assert.Equal(t, "3000", got.Port) + assert.Equal(t, "DemoService", got.Service()) + assert.Equal(t, "gray", got.GetParam(constant.Tagkey, "")) + assert.Equal(t, fmt.Sprint(constant.DefaultWeight), got.GetParam(constant.WeightKey, "")) + }) + + t.Run("fallback without endpoints", func(t *testing.T) { + instance := &DefaultServiceInstance{ + Host: "127.0.0.1", + Port: 20880, + Metadata: map[string]string{ + constant.ServiceInstanceEndpoints: "invalid-json", + }, + } + urls := instance.ToURLs(serviceInfo) + assert.Len(t, urls, 1) + assert.Equal(t, "20880", urls[0].Port) + }) +} + +func TestDefaultServiceInstance_GetEndPointsAndCopy(t *testing.T) { + instance := &DefaultServiceInstance{ + Host: "127.0.0.1", + Port: 20880, + Tag: "blue", + Enable: true, + Healthy: true, + Metadata: map[string]string{ + constant.ServiceInstanceEndpoints: `[{"port":20880,"protocol":"tri"}]`, + "custom": "value", + }, + ServiceMetadata: info.NewMetadataInfo("app", ""), + } + + endpoints := instance.GetEndPoints() + assert.Len(t, endpoints, 1) + assert.Equal(t, 20880, endpoints[0].Port) + assert.Equal(t, "tri", endpoints[0].Protocol) + + copied := instance.Copy(&Endpoint{Port: 3001}) + copiedInstance, ok := copied.(*DefaultServiceInstance) + if !ok { + t.Fatalf("expected *DefaultServiceInstance, got %T", copied) + } + assert.Equal(t, 3001, copiedInstance.Port) + assert.Equal(t, instance.GetAddress(), copiedInstance.ID) + assert.Equal(t, instance.Metadata, copiedInstance.Metadata) + assert.Equal(t, instance.Tag, copiedInstance.Tag) + + broken := &DefaultServiceInstance{ + Metadata: map[string]string{ + constant.ServiceInstanceEndpoints: "{broken", + }, + } + assert.Nil(t, broken.GetEndPoints()) +}
