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())
+}

Reply via email to