This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new 96053821e test(triple): enhance unit tests for protocol/triple package
(#3133)
96053821e is described below
commit 96053821e888aaaeda7b4f6875b099d00c06f9c8
Author: CAICAII <[email protected]>
AuthorDate: Mon Dec 22 07:43:36 2025 +0800
test(triple): enhance unit tests for protocol/triple package (#3133)
---
protocol/triple/client_test.go | 560 +++++++++++++++++++++++++++++++++
protocol/triple/server_test.go | 343 +++++++++++++++++++-
protocol/triple/triple_invoker_test.go | 456 +++++++++++++++++++++++++++
protocol/triple/triple_test.go | 69 ++++
4 files changed, 1427 insertions(+), 1 deletion(-)
diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index 8f39c4458..4f40e6f74 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -18,6 +18,7 @@
package triple
import (
+ "context"
"net/http"
"testing"
"time"
@@ -31,6 +32,7 @@ import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/global"
+ tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
)
func TestClientManager_HTTP2AndHTTP3(t *testing.T) {
@@ -94,3 +96,561 @@ func TestDualTransport(t *testing.T) {
})
assert.True(t, ok, "transport should implement http.RoundTripper")
}
+
+func TestClientManager_GetClient(t *testing.T) {
+ tests := []struct {
+ desc string
+ cm *clientManager
+ method string
+ expectErr bool
+ }{
+ {
+ desc: "method exists",
+ cm: &clientManager{
+ triClients: map[string]*tri.Client{
+ "TestMethod":
tri.NewClient(&http.Client{}, "http://localhost:8080/test"),
+ },
+ },
+ method: "TestMethod",
+ expectErr: false,
+ },
+ {
+ desc: "method not exists",
+ cm: &clientManager{
+ triClients: map[string]*tri.Client{
+ "TestMethod":
tri.NewClient(&http.Client{}, "http://localhost:8080/test"),
+ },
+ },
+ method: "NonExistMethod",
+ expectErr: true,
+ },
+ {
+ desc: "empty triClients",
+ cm: &clientManager{
+ triClients: map[string]*tri.Client{},
+ },
+ method: "AnyMethod",
+ expectErr: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ client, err := test.cm.getClient(test.method)
+ if test.expectErr {
+ assert.NotNil(t, err)
+ assert.Nil(t, client)
+ assert.Contains(t, err.Error(), "missing triple
client")
+ } else {
+ assert.Nil(t, err)
+ assert.NotNil(t, client)
+ }
+ })
+ }
+}
+
+func TestClientManager_Close(t *testing.T) {
+ cm := &clientManager{
+ isIDL: true,
+ triClients: map[string]*tri.Client{
+ "Method1": tri.NewClient(&http.Client{},
"http://localhost:8080/test1"),
+ "Method2": tri.NewClient(&http.Client{},
"http://localhost:8080/test2"),
+ },
+ }
+
+ err := cm.close()
+ assert.Nil(t, err)
+}
+
+func TestClientManager_CallMethods_MissingClient(t *testing.T) {
+ cm := &clientManager{
+ triClients: map[string]*tri.Client{},
+ }
+ ctx := context.Background()
+
+ t.Run("callUnary missing client", func(t *testing.T) {
+ err := cm.callUnary(ctx, "NonExist", nil, nil)
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "missing triple client")
+ })
+
+ t.Run("callClientStream missing client", func(t *testing.T) {
+ stream, err := cm.callClientStream(ctx, "NonExist")
+ assert.NotNil(t, err)
+ assert.Nil(t, stream)
+ assert.Contains(t, err.Error(), "missing triple client")
+ })
+
+ t.Run("callServerStream missing client", func(t *testing.T) {
+ stream, err := cm.callServerStream(ctx, "NonExist", nil)
+ assert.NotNil(t, err)
+ assert.Nil(t, stream)
+ assert.Contains(t, err.Error(), "missing triple client")
+ })
+
+ t.Run("callBidiStream missing client", func(t *testing.T) {
+ stream, err := cm.callBidiStream(ctx, "NonExist")
+ assert.NotNil(t, err)
+ assert.Nil(t, stream)
+ assert.Contains(t, err.Error(), "missing triple client")
+ })
+}
+
+func Test_genKeepAliveOptions(t *testing.T) {
+ defaultInterval, _ :=
time.ParseDuration(constant.DefaultKeepAliveInterval)
+ defaultTimeout, _ :=
time.ParseDuration(constant.DefaultKeepAliveTimeout)
+
+ tests := []struct {
+ desc string
+ url *common.URL
+ tripleConf *global.TripleConfig
+ expectOptsLen int
+ expectInterval time.Duration
+ expectTimeout time.Duration
+ expectErr bool
+ }{
+ {
+ desc: "nil triple config",
+ url: common.NewURLWithOptions(),
+ tripleConf: nil,
+ expectOptsLen: 2, // readMaxBytes, sendMaxBytes
+ expectInterval: defaultInterval,
+ expectTimeout: defaultTimeout,
+ expectErr: false,
+ },
+ {
+ desc: "url with max msg size",
+ url: common.NewURLWithOptions(
+
common.WithParamsValue(constant.MaxCallRecvMsgSize, "10MB"),
+
common.WithParamsValue(constant.MaxCallSendMsgSize, "10MB"),
+ ),
+ tripleConf: nil,
+ expectOptsLen: 2,
+ expectInterval: defaultInterval,
+ expectTimeout: defaultTimeout,
+ expectErr: false,
+ },
+ {
+ desc: "url with keepalive params",
+ url: common.NewURLWithOptions(
+
common.WithParamsValue(constant.KeepAliveInterval, "60s"),
+
common.WithParamsValue(constant.KeepAliveTimeout, "20s"),
+ ),
+ tripleConf: nil,
+ expectOptsLen: 2,
+ expectInterval: 60 * time.Second,
+ expectTimeout: 20 * time.Second,
+ expectErr: false,
+ },
+ {
+ desc: "triple config with keepalive",
+ url: common.NewURLWithOptions(),
+ tripleConf: &global.TripleConfig{
+ KeepAliveInterval: "45s",
+ KeepAliveTimeout: "15s",
+ },
+ expectOptsLen: 2,
+ expectInterval: 45 * time.Second,
+ expectTimeout: 15 * time.Second,
+ expectErr: false,
+ },
+ {
+ desc: "triple config with invalid interval",
+ url: common.NewURLWithOptions(),
+ tripleConf: &global.TripleConfig{
+ KeepAliveInterval: "invalid",
+ },
+ expectErr: true,
+ },
+ {
+ desc: "triple config with invalid timeout",
+ url: common.NewURLWithOptions(),
+ tripleConf: &global.TripleConfig{
+ KeepAliveTimeout: "invalid",
+ },
+ expectErr: true,
+ },
+ {
+ desc: "empty triple config",
+ url: common.NewURLWithOptions(),
+ tripleConf: &global.TripleConfig{},
+ expectOptsLen: 2,
+ expectInterval: defaultInterval,
+ expectTimeout: defaultTimeout,
+ expectErr: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ opts, interval, timeout, err :=
genKeepAliveOptions(test.url, test.tripleConf)
+ if test.expectErr {
+ assert.NotNil(t, err)
+ } else {
+ assert.Nil(t, err)
+ assert.Equal(t, test.expectOptsLen, len(opts))
+ assert.Equal(t, test.expectInterval, interval)
+ assert.Equal(t, test.expectTimeout, timeout)
+ }
+ })
+ }
+}
+
+func Test_newClientManager_Serialization(t *testing.T) {
+ tests := []struct {
+ desc string
+ serialization string
+ expectIDL bool
+ expectPanic bool
+ }{
+ {
+ desc: "protobuf serialization",
+ serialization: constant.ProtobufSerialization,
+ expectIDL: true,
+ expectPanic: false,
+ },
+ {
+ desc: "json serialization",
+ serialization: constant.JSONSerialization,
+ expectIDL: true,
+ expectPanic: false,
+ },
+ {
+ desc: "hessian2 serialization",
+ serialization: constant.Hessian2Serialization,
+ expectIDL: false,
+ expectPanic: false,
+ },
+ {
+ desc: "msgpack serialization",
+ serialization: constant.MsgpackSerialization,
+ expectIDL: false,
+ expectPanic: false,
+ },
+ {
+ desc: "unsupported serialization",
+ serialization: "unsupported",
+ expectPanic: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+
common.WithParamsValue(constant.SerializationKey, test.serialization),
+ )
+
+ if test.expectPanic {
+ assert.Panics(t, func() {
+ _, _ = newClientManager(url)
+ })
+ } else {
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+ assert.Equal(t, test.expectIDL, cm.isIDL)
+ }
+ })
+ }
+}
+
+func Test_newClientManager_NoMethods(t *testing.T) {
+ // Test when url has no methods and no RpcServiceKey attribute
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ )
+
+ cm, err := newClientManager(url)
+ assert.NotNil(t, err)
+ assert.Nil(t, cm)
+ assert.Contains(t, err.Error(), "can't get methods")
+}
+
+func Test_newClientManager_WithMethods(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"Method1", "Method2", "Method3"}),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+ assert.Equal(t, 3, len(cm.triClients))
+ assert.Contains(t, cm.triClients, "Method1")
+ assert.Contains(t, cm.triClients, "Method2")
+ assert.Contains(t, cm.triClients, "Method3")
+}
+
+func Test_newClientManager_WithGroupAndVersion(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ common.WithParamsValue(constant.GroupKey, "testGroup"),
+ common.WithParamsValue(constant.VersionKey, "1.0.0"),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_WithTimeout(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ common.WithParamsValue(constant.TimeoutKey, "5s"),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_InvalidTLSConfig(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+ // Set invalid TLS config type
+ url.SetAttribute(constant.TLSConfigKey, "invalid-type")
+
+ cm, err := newClientManager(url)
+ assert.NotNil(t, err)
+ assert.Nil(t, cm)
+ assert.Contains(t, err.Error(), "TLSConfig configuration failed")
+}
+
+func Test_newClientManager_HTTP3WithoutTLS(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+ // Enable HTTP/3 without TLS config
+ tripleConfig := &global.TripleConfig{
+ Http3: &global.Http3Config{
+ Enable: true,
+ },
+ }
+ url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+ cm, err := newClientManager(url)
+ assert.NotNil(t, err)
+ assert.Nil(t, cm)
+ assert.Contains(t, err.Error(), "must have TLS config")
+}
+
+// mockService is a mock service for testing reflection-based client creation
+type mockService struct{}
+
+func (m *mockService) Reference() string {
+ return "mockService"
+}
+
+func (m *mockService) TestMethod1(ctx context.Context, req string) (string,
error) {
+ return req, nil
+}
+
+func (m *mockService) TestMethod2(ctx context.Context, req int) (int, error) {
+ return req, nil
+}
+
+func Test_newClientManager_WithRpcService(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ // No methods specified, will use reflection
+ )
+ url.SetAttribute(constant.RpcServiceKey, &mockService{})
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+ // Should have methods from mockService (Reference, TestMethod1,
TestMethod2)
+ assert.True(t, len(cm.triClients) >= 2)
+}
+
+func TestDualTransport_Structure(t *testing.T) {
+ keepAliveInterval := 30 * time.Second
+ keepAliveTimeout := 5 * time.Second
+
+ transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+ assert.NotNil(t, transport)
+
+ dt, ok := transport.(*dualTransport)
+ assert.True(t, ok)
+ assert.NotNil(t, dt.http2Transport)
+ assert.NotNil(t, dt.http3Transport)
+ assert.NotNil(t, dt.altSvcCache)
+}
+
+func Test_newClientManager_HTTP2WithTLS(t *testing.T) {
+ // This test requires valid TLS config files
+ // Skip if files don't exist
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ // Set a valid TLS config structure but with non-existent files
+ // This will test the TLS config parsing path
+ tlsConfig := &global.TLSConfig{
+ CACertFile: "non-existent-ca.crt",
+ TLSCertFile: "non-existent-server.crt",
+ TLSKeyFile: "non-existent-server.key",
+ }
+ url.SetAttribute(constant.TLSConfigKey, tlsConfig)
+
+ // Should fail due to missing cert files, but tests the TLS path
+ cm, err := newClientManager(url)
+ // Either succeeds (if TLS validation is lenient) or fails with TLS
error
+ if err != nil {
+ // Expected - TLS files don't exist
+ t.Logf("Expected TLS error: %v", err)
+ } else {
+ assert.NotNil(t, cm)
+ }
+}
+
+func Test_newClientManager_URLPrefixHandling(t *testing.T) {
+ tests := []struct {
+ desc string
+ location string
+ }{
+ {
+ desc: "location without prefix",
+ location: "localhost:20000",
+ },
+ {
+ desc: "location with http prefix",
+ location: "http://localhost:20000",
+ },
+ {
+ desc: "location with https prefix",
+ location: "https://localhost:20000",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation(test.location),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+ assert.Equal(t, 1, len(cm.triClients))
+ })
+ }
+}
+
+func Test_newClientManager_KeepAliveError(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ // Set triple config with invalid keepalive that will cause error
+ tripleConfig := &global.TripleConfig{
+ KeepAliveInterval: "invalid-duration",
+ }
+ url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+ cm, err := newClientManager(url)
+ assert.NotNil(t, err)
+ assert.Nil(t, cm)
+}
+
+func Test_newClientManager_DefaultProtocol(t *testing.T) {
+ // Test default HTTP/2 protocol selection
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_EmptyTripleConfig(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ // Set empty triple config (Http3 is nil)
+ tripleConfig := &global.TripleConfig{}
+ url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_Http3Disabled(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ // Set triple config with Http3 disabled
+ tripleConfig := &global.TripleConfig{
+ Http3: &global.Http3Config{
+ Enable: false,
+ },
+ }
+ url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_MultipleMethods(t *testing.T) {
+ methods := []string{"Method1", "Method2", "Method3", "Method4",
"Method5"}
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithMethods(methods),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+ assert.Equal(t, len(methods), len(cm.triClients))
+
+ for _, method := range methods {
+ _, exists := cm.triClients[method]
+ assert.True(t, exists, "method %s should exist", method)
+ }
+}
+
+func Test_newClientManager_InterfaceName(t *testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithLocation("localhost:20000"),
+ common.WithPath("com.example.TestService"),
+ common.WithInterface("com.example.ITestService"),
+ common.WithMethods([]string{"TestMethod"}),
+ )
+
+ cm, err := newClientManager(url)
+ assert.Nil(t, err)
+ assert.NotNil(t, cm)
+}
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index c6b705141..d6129fea6 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -18,15 +18,21 @@
package triple
import (
+ "context"
+ "fmt"
"net/http"
+ "sync"
"testing"
)
import (
"github.com/stretchr/testify/assert"
+
+ "google.golang.org/grpc"
)
import (
+ "dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/global"
)
@@ -135,7 +141,7 @@ func TestServer_ProtocolSelection(t *testing.T) {
// Extract the protocol selection logic for testing
var callProtocol string
- if tripleConfig != nil && tripleConfig.Http3 != nil &&
tripleConfig.Http3.Enable {
+ if tripleConfig.Http3 != nil &&
tripleConfig.Http3.Enable {
callProtocol = constant.CallHTTP2AndHTTP3
} else {
callProtocol = constant.CallHTTP2
@@ -145,3 +151,338 @@ func TestServer_ProtocolSelection(t *testing.T) {
})
}
}
+
+func TestNewServer(t *testing.T) {
+ tests := []struct {
+ desc string
+ cfg *global.TripleConfig
+ }{
+ {
+ desc: "nil config",
+ cfg: nil,
+ },
+ {
+ desc: "empty config",
+ cfg: &global.TripleConfig{},
+ },
+ {
+ desc: "config with http3",
+ cfg: &global.TripleConfig{
+ Http3: &global.Http3Config{
+ Enable: true,
+ },
+ },
+ },
+ {
+ desc: "config with keepalive",
+ cfg: &global.TripleConfig{
+ KeepAliveInterval: "30s",
+ KeepAliveTimeout: "10s",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ server := NewServer(test.cfg)
+ assert.NotNil(t, server)
+ assert.Equal(t, test.cfg, server.cfg)
+ assert.NotNil(t, server.services)
+ assert.Empty(t, server.services)
+ })
+ }
+}
+
+func TestServer_GetServiceInfo(t *testing.T) {
+ t.Run("empty services", func(t *testing.T) {
+ server := NewServer(nil)
+ info := server.GetServiceInfo()
+ assert.NotNil(t, info)
+ assert.Empty(t, info)
+ })
+
+ t.Run("with services", func(t *testing.T) {
+ server := NewServer(nil)
+ // manually add service info for testing
+ server.mu.Lock()
+ server.services["test.Service"] = grpc.ServiceInfo{
+ Methods: []grpc.MethodInfo{
+ {Name: "Method1", IsClientStream: false,
IsServerStream: false},
+ },
+ }
+ server.mu.Unlock()
+
+ info := server.GetServiceInfo()
+ assert.NotNil(t, info)
+ assert.Equal(t, 1, len(info))
+ assert.Contains(t, info, "test.Service")
+ })
+
+ t.Run("returns copy not reference", func(t *testing.T) {
+ server := NewServer(nil)
+ server.mu.Lock()
+ server.services["test.Service"] = grpc.ServiceInfo{}
+ server.mu.Unlock()
+
+ info1 := server.GetServiceInfo()
+ info2 := server.GetServiceInfo()
+
+ // modify info1 should not affect info2
+ delete(info1, "test.Service")
+ assert.Contains(t, info2, "test.Service")
+ })
+}
+
+func TestServer_SaveServiceInfo(t *testing.T) {
+ tests := []struct {
+ desc string
+ interfaceName string
+ info *common.ServiceInfo
+ expect func(t *testing.T, server *Server)
+ }{
+ {
+ desc: "unary method",
+ interfaceName: "test.UnaryService",
+ info: &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "UnaryMethod", Type:
constant.CallUnary},
+ },
+ },
+ expect: func(t *testing.T, server *Server) {
+ info := server.GetServiceInfo()
+ assert.Contains(t, info, "test.UnaryService")
+ assert.Equal(t, 1,
len(info["test.UnaryService"].Methods))
+ assert.Equal(t, "UnaryMethod",
info["test.UnaryService"].Methods[0].Name)
+ assert.False(t,
info["test.UnaryService"].Methods[0].IsClientStream)
+ assert.False(t,
info["test.UnaryService"].Methods[0].IsServerStream)
+ },
+ },
+ {
+ desc: "client stream method",
+ interfaceName: "test.ClientStreamService",
+ info: &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "ClientStreamMethod", Type:
constant.CallClientStream},
+ },
+ },
+ expect: func(t *testing.T, server *Server) {
+ info := server.GetServiceInfo()
+ assert.Contains(t, info,
"test.ClientStreamService")
+ assert.True(t,
info["test.ClientStreamService"].Methods[0].IsClientStream)
+ assert.False(t,
info["test.ClientStreamService"].Methods[0].IsServerStream)
+ },
+ },
+ {
+ desc: "server stream method",
+ interfaceName: "test.ServerStreamService",
+ info: &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "ServerStreamMethod", Type:
constant.CallServerStream},
+ },
+ },
+ expect: func(t *testing.T, server *Server) {
+ info := server.GetServiceInfo()
+ assert.Contains(t, info,
"test.ServerStreamService")
+ assert.False(t,
info["test.ServerStreamService"].Methods[0].IsClientStream)
+ assert.True(t,
info["test.ServerStreamService"].Methods[0].IsServerStream)
+ },
+ },
+ {
+ desc: "bidi stream method",
+ interfaceName: "test.BidiStreamService",
+ info: &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "BidiStreamMethod", Type:
constant.CallBidiStream},
+ },
+ },
+ expect: func(t *testing.T, server *Server) {
+ info := server.GetServiceInfo()
+ assert.Contains(t, info,
"test.BidiStreamService")
+ assert.True(t,
info["test.BidiStreamService"].Methods[0].IsClientStream)
+ assert.True(t,
info["test.BidiStreamService"].Methods[0].IsServerStream)
+ },
+ },
+ {
+ desc: "multiple methods",
+ interfaceName: "test.MultiMethodService",
+ info: &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "Method1", Type:
constant.CallUnary},
+ {Name: "Method2", Type:
constant.CallClientStream},
+ {Name: "Method3", Type:
constant.CallServerStream},
+ {Name: "Method4", Type:
constant.CallBidiStream},
+ },
+ },
+ expect: func(t *testing.T, server *Server) {
+ info := server.GetServiceInfo()
+ assert.Contains(t, info,
"test.MultiMethodService")
+ assert.Equal(t, 4,
len(info["test.MultiMethodService"].Methods))
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ server := NewServer(nil)
+ server.saveServiceInfo(test.interfaceName, test.info)
+ test.expect(t, server)
+ })
+ }
+}
+
+func TestServer_SaveServiceInfo_Concurrent(t *testing.T) {
+ server := NewServer(nil)
+ var wg sync.WaitGroup
+ concurrency := 10
+
+ for i := 0; i < concurrency; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+ info := &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {Name: "Method", Type:
constant.CallUnary},
+ },
+ }
+ server.saveServiceInfo(fmt.Sprintf("test.Service%d",
idx), info)
+ }(i)
+ }
+
+ wg.Wait()
+ assert.Equal(t, concurrency, len(server.GetServiceInfo()))
+}
+
+func Test_getHanOpts(t *testing.T) {
+ tests := []struct {
+ desc string
+ url *common.URL
+ tripleConf *global.TripleConfig
+ expectLen int
+ }{
+ {
+ desc: "basic url without triple config",
+ url: common.NewURLWithOptions(),
+ tripleConf: nil,
+ expectLen: 4, // group, version, readMaxBytes,
sendMaxBytes
+ },
+ {
+ desc: "url with group and version",
+ url: common.NewURLWithOptions(
+ common.WithParamsValue(constant.GroupKey,
"testGroup"),
+ common.WithParamsValue(constant.VersionKey,
"1.0.0"),
+ ),
+ tripleConf: nil,
+ expectLen: 4,
+ },
+ {
+ desc: "url with max msg size",
+ url: common.NewURLWithOptions(
+
common.WithParamsValue(constant.MaxServerRecvMsgSize, "10MB"),
+
common.WithParamsValue(constant.MaxServerSendMsgSize, "10MB"),
+ ),
+ tripleConf: nil,
+ expectLen: 4,
+ },
+ {
+ desc: "with triple config max msg size",
+ url: common.NewURLWithOptions(),
+ tripleConf: &global.TripleConfig{
+ MaxServerRecvMsgSize: "20MB",
+ MaxServerSendMsgSize: "20MB",
+ },
+ expectLen: 6, // base 4 + 2 from tripleConf
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ opts := getHanOpts(test.url, test.tripleConf)
+ assert.Equal(t, test.expectLen, len(opts))
+ })
+ }
+}
+
+// mockRPCService is a mock service for testing createServiceInfoWithReflection
+type mockRPCService struct{}
+
+func (m *mockRPCService) Reference() string {
+ return "mockRPCService"
+}
+
+func (m *mockRPCService) TestMethod(ctx context.Context, req string) (string,
error) {
+ return req, nil
+}
+
+func (m *mockRPCService) TestMethodWithMultipleArgs(ctx context.Context, arg1
string, arg2 int) (string, error) {
+ return arg1, nil
+}
+
+func Test_createServiceInfoWithReflection(t *testing.T) {
+ t.Run("basic service", func(t *testing.T) {
+ svc := &mockRPCService{}
+ info := createServiceInfoWithReflection(svc)
+
+ assert.NotNil(t, info)
+ assert.NotEmpty(t, info.Methods)
+
+ // should have TestMethod, TestMethodWithMultipleArgs, and
$invoke
+ methodNames := make([]string, 0)
+ for _, m := range info.Methods {
+ methodNames = append(methodNames, m.Name)
+ }
+ assert.Contains(t, methodNames, "TestMethod")
+ assert.Contains(t, methodNames, "TestMethodWithMultipleArgs")
+ assert.Contains(t, methodNames, "$invoke") // generic call
method
+ })
+
+ t.Run("method type is CallUnary", func(t *testing.T) {
+ svc := &mockRPCService{}
+ info := createServiceInfoWithReflection(svc)
+
+ for _, m := range info.Methods {
+ assert.Equal(t, constant.CallUnary, m.Type)
+ }
+ })
+
+ t.Run("ReqInitFunc returns correct params", func(t *testing.T) {
+ svc := &mockRPCService{}
+ info := createServiceInfoWithReflection(svc)
+
+ for _, m := range info.Methods {
+ if m.Name == "TestMethod" {
+ params := m.ReqInitFunc()
+ assert.NotNil(t, params)
+ paramsSlice, ok := params.([]any)
+ assert.True(t, ok)
+ assert.Equal(t, 1, len(paramsSlice)) // only
req param (ctx is excluded)
+ }
+ if m.Name == "TestMethodWithMultipleArgs" {
+ params := m.ReqInitFunc()
+ paramsSlice, ok := params.([]any)
+ assert.True(t, ok)
+ assert.Equal(t, 2, len(paramsSlice)) // arg1
and arg2
+ }
+ }
+ })
+
+ t.Run("generic invoke method", func(t *testing.T) {
+ svc := &mockRPCService{}
+ info := createServiceInfoWithReflection(svc)
+
+ var invokeMethod *common.MethodInfo
+ for i := range info.Methods {
+ if info.Methods[i].Name == "$invoke" {
+ invokeMethod = &info.Methods[i]
+ break
+ }
+ }
+
+ assert.NotNil(t, invokeMethod)
+ assert.Equal(t, constant.CallUnary, invokeMethod.Type)
+
+ params := invokeMethod.ReqInitFunc()
+ paramsSlice, ok := params.([]any)
+ assert.True(t, ok)
+ assert.Equal(t, 3, len(paramsSlice)) // methodName, argv types,
argv
+ })
+}
diff --git a/protocol/triple/triple_invoker_test.go
b/protocol/triple/triple_invoker_test.go
index 09788c450..9f8575aaa 100644
--- a/protocol/triple/triple_invoker_test.go
+++ b/protocol/triple/triple_invoker_test.go
@@ -20,6 +20,7 @@ package triple
import (
"context"
"net/http"
+ "sync"
"testing"
)
@@ -54,6 +55,7 @@ func Test_parseInvocation(t *testing.T) {
},
expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "miss CallType")
},
},
{
@@ -69,6 +71,7 @@ func Test_parseInvocation(t *testing.T) {
},
expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "CallType
should be string")
},
},
{
@@ -84,6 +87,87 @@ func Test_parseInvocation(t *testing.T) {
},
expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "miss
MethodName")
+ },
+ },
+ {
+ desc: "success with CallUnary",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ iv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("TestMethod"),
+
invocation.WithParameterRawValues([]any{"req", "resp"}),
+ )
+ iv.SetAttribute(constant.CallTypeKey,
constant.CallUnary)
+ return iv
+ },
+ expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
+ assert.Nil(t, err)
+ assert.Equal(t, constant.CallUnary, callType)
+ assert.Equal(t, "TestMethod", methodName)
+ assert.Equal(t, 2, len(inRaw))
+ },
+ },
+ {
+ desc: "success with CallClientStream",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ iv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithMethodName("StreamMethod"),
+ )
+ iv.SetAttribute(constant.CallTypeKey,
constant.CallClientStream)
+ return iv
+ },
+ expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
+ assert.Nil(t, err)
+ assert.Equal(t, constant.CallClientStream,
callType)
+ assert.Equal(t, "StreamMethod", methodName)
+ },
+ },
+ {
+ desc: "success with CallServerStream",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ iv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithMethodName("ServerStreamMethod"),
+
invocation.WithParameterRawValues([]any{"req"}),
+ )
+ iv.SetAttribute(constant.CallTypeKey,
constant.CallServerStream)
+ return iv
+ },
+ expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
+ assert.Nil(t, err)
+ assert.Equal(t, constant.CallServerStream,
callType)
+ assert.Equal(t, "ServerStreamMethod",
methodName)
+ assert.Equal(t, 1, len(inRaw))
+ },
+ },
+ {
+ desc: "success with CallBidiStream",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ iv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithMethodName("BidiStreamMethod"),
+ )
+ iv.SetAttribute(constant.CallTypeKey,
constant.CallBidiStream)
+ return iv
+ },
+ expect: func(t *testing.T, callType string, inRaw
[]any, methodName string, err error) {
+ assert.Nil(t, err)
+ assert.Equal(t, constant.CallBidiStream,
callType)
+ assert.Equal(t, "BidiStreamMethod", methodName)
},
},
}
@@ -157,6 +241,52 @@ func Test_parseAttachments(t *testing.T) {
},
expect: func(t *testing.T, ctx context.Context, err
error) {
assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "invalid")
+ },
+ },
+ {
+ desc: "url has timeout key",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(
+ common.WithParamsValue(constant.TimeoutKey,
"3000"),
+ ),
+ invo: func() base.Invocation {
+ return invocation.NewRPCInvocationWithOptions()
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ header :=
http.Header(tri.ExtractFromOutgoingContext(ctx))
+ assert.NotNil(t, header)
+ assert.Equal(t, "3000",
header.Get(constant.TimeoutKey))
+ },
+ },
+ {
+ desc: "empty context attachments",
+ ctx: func() context.Context {
+ return context.Background()
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ return invocation.NewRPCInvocationWithOptions()
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ },
+ },
+ {
+ desc: "context with non-map attachment value",
+ ctx: func() context.Context {
+ return context.WithValue(context.Background(),
constant.AttachmentKey, "not-a-map")
+ },
+ url: common.NewURLWithOptions(),
+ invo: func() base.Invocation {
+ return invocation.NewRPCInvocationWithOptions()
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ // non-map attachment should be ignored
+ assert.Nil(t, err)
},
},
}
@@ -171,3 +301,329 @@ func Test_parseAttachments(t *testing.T) {
})
}
}
+
+// newTestTripleInvoker creates a TripleInvoker for testing without network
connection
+func newTestTripleInvoker(url *common.URL, cm *clientManager) *TripleInvoker {
+ return &TripleInvoker{
+ BaseInvoker: *base.NewBaseInvoker(url),
+ quitOnce: sync.Once{},
+ clientGuard: &sync.RWMutex{},
+ clientManager: cm,
+ }
+}
+
+func TestTripleInvoker_SetGetClientManager(t *testing.T) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, nil)
+
+ // initially nil
+ assert.Nil(t, ti.getClientManager())
+
+ // set clientManager
+ cm := &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ }
+ ti.setClientManager(cm)
+ assert.Equal(t, cm, ti.getClientManager())
+
+ // set to nil
+ ti.setClientManager(nil)
+ assert.Nil(t, ti.getClientManager())
+}
+
+func TestTripleInvoker_IsAvailable(t *testing.T) {
+ tests := []struct {
+ desc string
+ clientManager *clientManager
+ expect bool
+ }{
+ {
+ desc: "clientManager is nil",
+ clientManager: nil,
+ expect: false,
+ },
+ {
+ desc: "clientManager is not nil",
+ clientManager: &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ },
+ expect: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, test.clientManager)
+ assert.Equal(t, test.expect, ti.IsAvailable())
+ })
+ }
+}
+
+func TestTripleInvoker_IsDestroyed(t *testing.T) {
+ tests := []struct {
+ desc string
+ clientManager *clientManager
+ destroyed bool
+ expect bool
+ }{
+ {
+ desc: "clientManager is nil",
+ clientManager: nil,
+ destroyed: false,
+ expect: false,
+ },
+ {
+ desc: "clientManager is not nil and not destroyed",
+ clientManager: &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ },
+ destroyed: false,
+ expect: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, test.clientManager)
+ assert.Equal(t, test.expect, ti.IsDestroyed())
+ })
+ }
+}
+
+func TestTripleInvoker_Destroy(t *testing.T) {
+ t.Run("destroy with clientManager", func(t *testing.T) {
+ url := common.NewURLWithOptions()
+ cm := &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ }
+ ti := newTestTripleInvoker(url, cm)
+
+ assert.True(t, ti.IsAvailable())
+ assert.NotNil(t, ti.getClientManager())
+
+ ti.Destroy()
+
+ assert.False(t, ti.IsAvailable())
+ assert.Nil(t, ti.getClientManager())
+ })
+
+ t.Run("destroy without clientManager", func(t *testing.T) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, nil)
+
+ ti.Destroy()
+
+ assert.False(t, ti.IsAvailable())
+ assert.Nil(t, ti.getClientManager())
+ })
+
+ t.Run("destroy called multiple times", func(t *testing.T) {
+ url := common.NewURLWithOptions()
+ cm := &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ }
+ ti := newTestTripleInvoker(url, cm)
+
+ // first destroy
+ ti.Destroy()
+ assert.Nil(t, ti.getClientManager())
+
+ // second destroy should not panic
+ ti.Destroy()
+ assert.Nil(t, ti.getClientManager())
+ })
+}
+
+func TestTripleInvoker_Invoke(t *testing.T) {
+ tests := []struct {
+ desc string
+ setup func() (*TripleInvoker, base.Invocation)
+ expectErr error
+ expectErrMsg string
+ }{
+ {
+ desc: "invoker is destroyed",
+ setup: func() (*TripleInvoker, base.Invocation) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, &clientManager{
+ isIDL: true,
+ triClients:
make(map[string]*tri.Client),
+ })
+ ti.Destroy()
+ inv := invocation.NewRPCInvocationWithOptions()
+ return ti, inv
+ },
+ expectErr: base.ErrDestroyedInvoker,
+ },
+ {
+ desc: "clientManager is nil",
+ setup: func() (*TripleInvoker, base.Invocation) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, nil)
+ inv := invocation.NewRPCInvocationWithOptions()
+ return ti, inv
+ },
+ expectErr: base.ErrClientClosed,
+ },
+ {
+ desc: "parseInvocation error - miss callType",
+ setup: func() (*TripleInvoker, base.Invocation) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, &clientManager{
+ isIDL: true,
+ triClients:
make(map[string]*tri.Client),
+ })
+ inv := invocation.NewRPCInvocationWithOptions()
+ return ti, inv
+ },
+ expectErrMsg: "miss CallType",
+ },
+ {
+ desc: "mergeAttachmentToOutgoing error - invalid
attachment",
+ setup: func() (*TripleInvoker, base.Invocation) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, &clientManager{
+ isIDL: true,
+ triClients:
make(map[string]*tri.Client),
+ })
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("TestMethod"),
+
invocation.WithAttachment("invalid_key", 123), // invalid attachment type
+ )
+ inv.SetAttribute(constant.CallTypeKey,
constant.CallUnary)
+ return ti, inv
+ },
+ expectErrMsg: "invalid",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ ti, inv := test.setup()
+ result := ti.Invoke(context.Background(), inv)
+ if test.expectErr != nil {
+ assert.Equal(t, test.expectErr, result.Error())
+ }
+ if test.expectErrMsg != "" {
+ assert.NotNil(t, result.Error())
+ assert.Contains(t, result.Error().Error(),
test.expectErrMsg)
+ }
+ })
+ }
+}
+
+func TestTripleInvoker_Invoke_Concurrent(t *testing.T) {
+ url := common.NewURLWithOptions()
+ ti := newTestTripleInvoker(url, &clientManager{
+ isIDL: true,
+ triClients: make(map[string]*tri.Client),
+ })
+
+ var wg sync.WaitGroup
+ concurrency := 10
+
+ for i := 0; i < concurrency; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ inv := invocation.NewRPCInvocationWithOptions()
+ _ = ti.Invoke(context.Background(), inv)
+ }()
+ }
+
+ wg.Wait()
+}
+
+func Test_mergeAttachmentToOutgoing(t *testing.T) {
+ tests := []struct {
+ desc string
+ ctx context.Context
+ invo func() base.Invocation
+ expect func(t *testing.T, ctx context.Context, err error)
+ }{
+ {
+ desc: "with timeout attachment",
+ ctx: context.Background(),
+ invo: func() base.Invocation {
+ inv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithAttachment(constant.TimeoutKey, "5000"),
+ )
+ return inv
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ timeout := ctx.Value(tri.TimeoutKey{})
+ assert.Equal(t, "5000", timeout)
+ },
+ },
+ {
+ desc: "with string attachment",
+ ctx: context.Background(),
+ invo: func() base.Invocation {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithAttachment("custom-key",
"custom-value"),
+ )
+ return inv
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ header :=
http.Header(tri.ExtractFromOutgoingContext(ctx))
+ assert.Equal(t, "custom-value",
header.Get("custom-key"))
+ },
+ },
+ {
+ desc: "with string slice attachment",
+ ctx: context.Background(),
+ invo: func() base.Invocation {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithAttachment("multi-key",
[]string{"val1", "val2"}),
+ )
+ return inv
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ header :=
http.Header(tri.ExtractFromOutgoingContext(ctx))
+ assert.Equal(t, []string{"val1", "val2"},
header.Values("multi-key"))
+ },
+ },
+ {
+ desc: "with invalid attachment type",
+ ctx: context.Background(),
+ invo: func() base.Invocation {
+ inv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithAttachment("invalid-key", 12345),
+ )
+ return inv
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "invalid")
+ },
+ },
+ {
+ desc: "with empty attachments",
+ ctx: context.Background(),
+ invo: func() base.Invocation {
+ return invocation.NewRPCInvocationWithOptions()
+ },
+ expect: func(t *testing.T, ctx context.Context, err
error) {
+ assert.Nil(t, err)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ inv := test.invo()
+ ctx, err := mergeAttachmentToOutgoing(test.ctx, inv)
+ test.expect(t, ctx, err)
+ })
+ }
+}
diff --git a/protocol/triple/triple_test.go b/protocol/triple/triple_test.go
new file mode 100644
index 000000000..80c73aa21
--- /dev/null
+++ b/protocol/triple/triple_test.go
@@ -0,0 +1,69 @@
+/*
+ * 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 triple
+
+import (
+ "testing"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common/extension"
+)
+
+func TestNewTripleProtocol(t *testing.T) {
+ tp := NewTripleProtocol()
+
+ assert.NotNil(t, tp)
+ assert.NotNil(t, tp.serverMap)
+ assert.Empty(t, tp.serverMap)
+}
+
+func TestGetProtocol(t *testing.T) {
+ // reset singleton for test isolation
+ tripleProtocol = nil
+
+ p1 := GetProtocol()
+ assert.NotNil(t, p1)
+
+ // should return same instance (singleton)
+ p2 := GetProtocol()
+ assert.Same(t, p1, p2)
+}
+
+func TestTripleProtocolRegistration(t *testing.T) {
+ // verify protocol is registered via init()
+ p := extension.GetProtocol(TRIPLE)
+ assert.NotNil(t, p)
+}
+
+func TestTripleConstant(t *testing.T) {
+ assert.Equal(t, "tri", TRIPLE)
+}
+
+func TestTripleProtocol_Destroy_EmptyServerMap(t *testing.T) {
+ tp := NewTripleProtocol()
+
+ // should not panic when serverMap is empty
+ assert.NotPanics(t, func() {
+ tp.Destroy()
+ })
+}