From 1c7cb23389cf2c99765621863b60ac7c70ab402c Mon Sep 17 00:00:00 2001 From: Toby Date: Tue, 25 Jul 2023 16:11:11 -0700 Subject: [PATCH] feat(wip): test reworks --- core/client/.mockery.yaml | 9 + core/client/mock_udpIO.go | 131 ++++++++++ core/client/udp_test.go | 185 +++++++------- core/go.mod | 10 +- core/go.sum | 16 +- core/server/.mockery.yaml | 15 ++ core/server/mock_UDPConn.go | 185 ++++++++++++++ core/server/mock_udpEventLogger.go | 100 ++++++++ core/server/mock_udpIO.go | 185 ++++++++++++++ core/server/udp_test.go | 388 +++++++++++------------------ go.work.sum | 6 +- 11 files changed, 899 insertions(+), 331 deletions(-) create mode 100644 core/client/.mockery.yaml create mode 100644 core/client/mock_udpIO.go create mode 100644 core/server/.mockery.yaml create mode 100644 core/server/mock_UDPConn.go create mode 100644 core/server/mock_udpEventLogger.go create mode 100644 core/server/mock_udpIO.go diff --git a/core/client/.mockery.yaml b/core/client/.mockery.yaml new file mode 100644 index 0000000..9500c13 --- /dev/null +++ b/core/client/.mockery.yaml @@ -0,0 +1,9 @@ +with-expecter: true +inpackage: true +dir: . +packages: + github.com/apernet/hysteria/core/client: + interfaces: + udpIO: + config: + mockname: mockUDPIO diff --git a/core/client/mock_udpIO.go b/core/client/mock_udpIO.go new file mode 100644 index 0000000..6e0b1ba --- /dev/null +++ b/core/client/mock_udpIO.go @@ -0,0 +1,131 @@ +// Code generated by mockery v2.32.0. DO NOT EDIT. + +package client + +import ( + protocol "github.com/apernet/hysteria/core/internal/protocol" + mock "github.com/stretchr/testify/mock" +) + +// mockUDPIO is an autogenerated mock type for the udpIO type +type mockUDPIO struct { + mock.Mock +} + +type mockUDPIO_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { + return &mockUDPIO_Expecter{mock: &_m.Mock} +} + +// ReceiveMessage provides a mock function with given fields: +func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { + ret := _m.Called() + + var r0 *protocol.UDPMessage + var r1 error + if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*protocol.UDPMessage) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' +type mockUDPIO_ReceiveMessage_Call struct { + *mock.Call +} + +// ReceiveMessage is a helper method to define mock.On call +func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { + return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendMessage provides a mock function with given fields: _a0, _a1 +func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' +type mockUDPIO_SendMessage_Call struct { + *mock.Call +} + +// SendMessage is a helper method to define mock.On call +// - _a0 []byte +// - _a1 *protocol.UDPMessage +func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { + return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} +} + +func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*protocol.UDPMessage)) + }) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPIO(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPIO { + mock := &mockUDPIO{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/client/udp_test.go b/core/client/udp_test.go index f40a5bc..d931bfa 100644 --- a/core/client/udp_test.go +++ b/core/client/udp_test.go @@ -2,108 +2,121 @@ package client import ( "errors" - "fmt" io2 "io" "testing" "time" + "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/goleak" + coreErrs "github.com/apernet/hysteria/core/errors" "github.com/apernet/hysteria/core/internal/protocol" - "go.uber.org/goleak" ) -type udpEchoIO struct { - MsgCh chan *protocol.UDPMessage - CloseCh chan struct{} -} - -func (io *udpEchoIO) ReceiveMessage() (*protocol.UDPMessage, error) { - select { - case m := <-io.MsgCh: - return m, nil - case <-io.CloseCh: - return nil, errors.New("closed") - } -} - -func (io *udpEchoIO) SendMessage(buf []byte, msg *protocol.UDPMessage) error { - nMsg := *msg - nMsg.Data = make([]byte, len(msg.Data)) - copy(nMsg.Data, msg.Data) - io.MsgCh <- &nMsg - return nil -} - -func (io *udpEchoIO) Close() { - close(io.CloseCh) -} - func TestUDPSessionManager(t *testing.T) { - io := &udpEchoIO{ - MsgCh: make(chan *protocol.UDPMessage, 10), - CloseCh: make(chan struct{}), - } + io := newMockUDPIO(t) + receiveCh := make(chan *protocol.UDPMessage, 4) + io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { + m := <-receiveCh + if m == nil { + return nil, errors.New("closed") + } + return m, nil + }) sm := newUDPSessionManager(io) - rChan := make(chan error, 5) + // Test UDP session IO + udpConn1, err := sm.NewUDP() + assert.Equal(t, err, nil) + udpConn2, err := sm.NewUDP() + assert.Equal(t, err, nil) - for i := 0; i < 5; i++ { - go func(id int) { - conn, err := sm.NewUDP() - if err != nil { - rChan <- err - return - } - defer conn.Close() - - addr := fmt.Sprintf("wow.com:%d", id) - for j := 0; j < 2; j++ { - s := fmt.Sprintf("hello %d %d", id, j) - err = conn.Send([]byte(s), addr) - if err != nil { - rChan <- err - return - } - bs, addr, err := conn.Receive() - if err != nil { - rChan <- err - return - } - if string(bs) != s || addr != addr { - rChan <- fmt.Errorf("unexpected message: %s %s", bs, addr) - return - } - } - rChan <- nil // Success - }(i) + msg1 := &protocol.UDPMessage{ + SessionID: 1, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "random.site.com:9000", + Data: []byte("hello friend"), } + io.EXPECT().SendMessage(mock.Anything, msg1).Return(nil).Once() + err = udpConn1.Send(msg1.Data, msg1.Addr) + assert.Equal(t, err, nil) - // Check the results - for i := 0; i < 5; i++ { - err := <-rChan - if err != nil { - t.Fatal(err) - } + msg2 := &protocol.UDPMessage{ + SessionID: 2, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "another.site.org:8000", + Data: []byte("mr robot"), } + io.EXPECT().SendMessage(mock.Anything, msg2).Return(nil).Once() + err = udpConn2.Send(msg2.Data, msg2.Addr) + assert.Equal(t, err, nil) + + respMsg1 := &protocol.UDPMessage{ + SessionID: 1, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg1.Addr, + Data: []byte("goodbye captain price"), + } + receiveCh <- respMsg1 + data, addr, err := udpConn1.Receive() + assert.Equal(t, err, nil) + assert.Equal(t, data, respMsg1.Data) + assert.Equal(t, addr, respMsg1.Addr) + + respMsg2 := &protocol.UDPMessage{ + SessionID: 2, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg2.Addr, + Data: []byte("white rose"), + } + receiveCh <- respMsg2 + data, addr, err = udpConn2.Receive() + assert.Equal(t, err, nil) + assert.Equal(t, data, respMsg2.Data) + assert.Equal(t, addr, respMsg2.Addr) + + respMsg3 := &protocol.UDPMessage{ + SessionID: 55, // Bogus session ID that doesn't exist + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "burgerking.com:27017", + Data: []byte("impossible whopper"), + } + receiveCh <- respMsg3 + // No test for this, just make sure it doesn't panic + + // Test close UDP connection unblocks Receive() + errChan := make(chan error, 1) + go func() { + _, _, err := udpConn1.Receive() + errChan <- err + }() + assert.Equal(t, udpConn1.Close(), nil) + assert.Equal(t, <-errChan, io2.EOF) + + // Test close IO unblocks Receive() and blocks new UDP creation + errChan = make(chan error, 1) + go func() { + _, _, err := udpConn2.Receive() + errChan <- err + }() + close(receiveCh) + assert.Equal(t, <-errChan, io2.EOF) + _, err = sm.NewUDP() + assert.Equal(t, err, coreErrs.ClosedError{}) // Leak checks - // Create another UDP session - conn, err := sm.NewUDP() - if err != nil { - t.Fatal(err) - } - io.Close() - time.Sleep(1 * time.Second) // Give some time for the goroutines to exit - _, _, err = conn.Receive() - if err != io2.EOF { - t.Fatal("expected EOF after closing io") - } - _, err = sm.NewUDP() - if !errors.Is(err, coreErrs.ClosedError{}) { - t.Fatal("expected ClosedError after closing io") - } - if sm.Count() != 0 { - t.Error("session count should be 0") - } + time.Sleep(1 * time.Second) + assert.Equal(t, sm.Count(), 0, "session count should be 0") goleak.VerifyNone(t) } diff --git a/core/go.mod b/core/go.mod index e8c6c3b..4a00b7b 100644 --- a/core/go.mod +++ b/core/go.mod @@ -3,20 +3,26 @@ module github.com/apernet/hysteria/core go 1.20 require ( + github.com/magiconair/properties v1.8.7 github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.8.4 go.uber.org/goleak v1.2.1 golang.org/x/time v0.3.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect - github.com/stretchr/testify v1.8.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect golang.org/x/crypto v0.4.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.10.0 // indirect @@ -25,6 +31,8 @@ require ( golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.9.1 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.36.1-0.20230627042819-0a89ea8e4c8d diff --git a/core/go.sum b/core/go.sum index 14e5737..b0ff8df 100644 --- a/core/go.sum +++ b/core/go.sum @@ -3,6 +3,7 @@ github.com/apernet/quic-go v0.36.1-0.20230627042819-0a89ea8e4c8d/go.mod h1:zPetv github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -18,6 +19,14 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= @@ -31,12 +40,13 @@ github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8G github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= @@ -84,6 +94,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/server/.mockery.yaml b/core/server/.mockery.yaml new file mode 100644 index 0000000..b7cf743 --- /dev/null +++ b/core/server/.mockery.yaml @@ -0,0 +1,15 @@ +with-expecter: true +inpackage: true +dir: . +packages: + github.com/apernet/hysteria/core/server: + interfaces: + udpIO: + config: + mockname: mockUDPIO + udpEventLogger: + config: + mockname: mockUDPEventLogger + UDPConn: + config: + mockname: mockUDPConn diff --git a/core/server/mock_UDPConn.go b/core/server/mock_UDPConn.go new file mode 100644 index 0000000..34a34ee --- /dev/null +++ b/core/server/mock_UDPConn.go @@ -0,0 +1,185 @@ +// Code generated by mockery v2.32.0. DO NOT EDIT. + +package server + +import mock "github.com/stretchr/testify/mock" + +// mockUDPConn is an autogenerated mock type for the UDPConn type +type mockUDPConn struct { + mock.Mock +} + +type mockUDPConn_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter { + return &mockUDPConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *mockUDPConn) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockUDPConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call { + return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// ReadFrom provides a mock function with given fields: b +func (_m *mockUDPConn) ReadFrom(b []byte) (int, string, error) { + ret := _m.Called(b) + + var r0 int + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) string); ok { + r1 = rf(b) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func([]byte) error); ok { + r2 = rf(b) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' +type mockUDPConn_ReadFrom_Call struct { + *mock.Call +} + +// ReadFrom is a helper method to define mock.On call +// - b []byte +func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call { + return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} +} + +func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(run) + return _c +} + +// WriteTo provides a mock function with given fields: b, addr +func (_m *mockUDPConn) WriteTo(b []byte, addr string) (int, error) { + ret := _m.Called(b, addr) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok { + return rf(b, addr) + } + if rf, ok := ret.Get(0).(func([]byte, string) int); ok { + r0 = rf(b, addr) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte, string) error); ok { + r1 = rf(b, addr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' +type mockUDPConn_WriteTo_Call struct { + *mock.Call +} + +// WriteTo is a helper method to define mock.On call +// - b []byte +// - addr string +func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call { + return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} +} + +func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *mockUDPConn_WriteTo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(string)) + }) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *mockUDPConn_WriteTo_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPConn(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPConn { + mock := &mockUDPConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/server/mock_udpEventLogger.go b/core/server/mock_udpEventLogger.go new file mode 100644 index 0000000..08f3ff8 --- /dev/null +++ b/core/server/mock_udpEventLogger.go @@ -0,0 +1,100 @@ +// Code generated by mockery v2.32.0. DO NOT EDIT. + +package server + +import mock "github.com/stretchr/testify/mock" + +// mockUDPEventLogger is an autogenerated mock type for the udpEventLogger type +type mockUDPEventLogger struct { + mock.Mock +} + +type mockUDPEventLogger_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPEventLogger) EXPECT() *mockUDPEventLogger_Expecter { + return &mockUDPEventLogger_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: sessionID, err +func (_m *mockUDPEventLogger) Close(sessionID uint32, err error) { + _m.Called(sessionID, err) +} + +// mockUDPEventLogger_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockUDPEventLogger_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +// - sessionID uint32 +// - err error +func (_e *mockUDPEventLogger_Expecter) Close(sessionID interface{}, err interface{}) *mockUDPEventLogger_Close_Call { + return &mockUDPEventLogger_Close_Call{Call: _e.mock.On("Close", sessionID, err)} +} + +func (_c *mockUDPEventLogger_Close_Call) Run(run func(sessionID uint32, err error)) *mockUDPEventLogger_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32), args[1].(error)) + }) + return _c +} + +func (_c *mockUDPEventLogger_Close_Call) Return() *mockUDPEventLogger_Close_Call { + _c.Call.Return() + return _c +} + +func (_c *mockUDPEventLogger_Close_Call) RunAndReturn(run func(uint32, error)) *mockUDPEventLogger_Close_Call { + _c.Call.Return(run) + return _c +} + +// New provides a mock function with given fields: sessionID, reqAddr +func (_m *mockUDPEventLogger) New(sessionID uint32, reqAddr string) { + _m.Called(sessionID, reqAddr) +} + +// mockUDPEventLogger_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New' +type mockUDPEventLogger_New_Call struct { + *mock.Call +} + +// New is a helper method to define mock.On call +// - sessionID uint32 +// - reqAddr string +func (_e *mockUDPEventLogger_Expecter) New(sessionID interface{}, reqAddr interface{}) *mockUDPEventLogger_New_Call { + return &mockUDPEventLogger_New_Call{Call: _e.mock.On("New", sessionID, reqAddr)} +} + +func (_c *mockUDPEventLogger_New_Call) Run(run func(sessionID uint32, reqAddr string)) *mockUDPEventLogger_New_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32), args[1].(string)) + }) + return _c +} + +func (_c *mockUDPEventLogger_New_Call) Return() *mockUDPEventLogger_New_Call { + _c.Call.Return() + return _c +} + +func (_c *mockUDPEventLogger_New_Call) RunAndReturn(run func(uint32, string)) *mockUDPEventLogger_New_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPEventLogger creates a new instance of mockUDPEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPEventLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPEventLogger { + mock := &mockUDPEventLogger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/server/mock_udpIO.go b/core/server/mock_udpIO.go new file mode 100644 index 0000000..ddd44b3 --- /dev/null +++ b/core/server/mock_udpIO.go @@ -0,0 +1,185 @@ +// Code generated by mockery v2.32.0. DO NOT EDIT. + +package server + +import ( + protocol "github.com/apernet/hysteria/core/internal/protocol" + mock "github.com/stretchr/testify/mock" +) + +// mockUDPIO is an autogenerated mock type for the udpIO type +type mockUDPIO struct { + mock.Mock +} + +type mockUDPIO_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { + return &mockUDPIO_Expecter{mock: &_m.Mock} +} + +// ReceiveMessage provides a mock function with given fields: +func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { + ret := _m.Called() + + var r0 *protocol.UDPMessage + var r1 error + if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*protocol.UDPMessage) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' +type mockUDPIO_ReceiveMessage_Call struct { + *mock.Call +} + +// ReceiveMessage is a helper method to define mock.On call +func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { + return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendMessage provides a mock function with given fields: _a0, _a1 +func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' +type mockUDPIO_SendMessage_Call struct { + *mock.Call +} + +// SendMessage is a helper method to define mock.On call +// - _a0 []byte +// - _a1 *protocol.UDPMessage +func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { + return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} +} + +func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*protocol.UDPMessage)) + }) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(run) + return _c +} + +// UDP provides a mock function with given fields: reqAddr +func (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) { + ret := _m.Called(reqAddr) + + var r0 UDPConn + var r1 error + if rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(string) UDPConn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(UDPConn) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' +type mockUDPIO_UDP_Call struct { + *mock.Call +} + +// UDP is a helper method to define mock.On call +// - reqAddr string +func (_e *mockUDPIO_Expecter) UDP(reqAddr interface{}) *mockUDPIO_UDP_Call { + return &mockUDPIO_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} +} + +func (_c *mockUDPIO_UDP_Call) Run(run func(reqAddr string)) *mockUDPIO_UDP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *mockUDPIO_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockUDPIO_UDP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_UDP_Call) RunAndReturn(run func(string) (UDPConn, error)) *mockUDPIO_UDP_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPIO(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPIO { + mock := &mockUDPIO{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/server/udp_test.go b/core/server/udp_test.go index a8e8296..1fea5d2 100644 --- a/core/server/udp_test.go +++ b/core/server/udp_test.go @@ -2,260 +2,174 @@ package server import ( "errors" - "fmt" "testing" "time" - "github.com/apernet/hysteria/core/internal/protocol" + "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/mock" "go.uber.org/goleak" + + "github.com/apernet/hysteria/core/internal/protocol" ) -var ( - errUDPBlocked = errors.New("blocked") - errUDPClosed = errors.New("closed") -) - -type echoUDPConnPkt struct { - Data []byte - Addr string - Close bool -} - -type echoUDPConn struct { - PktCh chan echoUDPConnPkt -} - -func (c *echoUDPConn) ReadFrom(b []byte) (int, string, error) { - pkt := <-c.PktCh - if pkt.Close { - return 0, "", errUDPClosed - } - n := copy(b, pkt.Data) - return n, pkt.Addr, nil -} - -func (c *echoUDPConn) WriteTo(b []byte, addr string) (int, error) { - nb := make([]byte, len(b)) - copy(nb, b) - c.PktCh <- echoUDPConnPkt{ - Data: nb, - Addr: addr, - } - return len(b), nil -} - -func (c *echoUDPConn) Close() error { - c.PktCh <- echoUDPConnPkt{ - Close: true, - } - return nil -} - -type udpMockIO struct { - ReceiveCh <-chan *protocol.UDPMessage - SendCh chan<- *protocol.UDPMessage - UDPClose bool // ReadFrom() returns error immediately - BlockUDP bool // Block UDP connection creation -} - -func (io *udpMockIO) ReceiveMessage() (*protocol.UDPMessage, error) { - m := <-io.ReceiveCh - if m == nil { - return nil, errUDPClosed - } - return m, nil -} - -func (io *udpMockIO) SendMessage(buf []byte, msg *protocol.UDPMessage) error { - nMsg := *msg - nMsg.Data = make([]byte, len(msg.Data)) - copy(nMsg.Data, msg.Data) - io.SendCh <- &nMsg - return nil -} - -func (io *udpMockIO) UDP(reqAddr string) (UDPConn, error) { - if io.BlockUDP { - return nil, errUDPBlocked - } - conn := &echoUDPConn{ - PktCh: make(chan echoUDPConnPkt, 10), - } - if io.UDPClose { - conn.PktCh <- echoUDPConnPkt{ - Close: true, - } - } - return conn, nil -} - -type udpMockEventNew struct { - SessionID uint32 - ReqAddr string -} - -type udpMockEventClose struct { - SessionID uint32 - Err error -} - -type udpMockEventLogger struct { - NewCh chan<- udpMockEventNew - CloseCh chan<- udpMockEventClose -} - -func (l *udpMockEventLogger) New(sessionID uint32, reqAddr string) { - l.NewCh <- udpMockEventNew{sessionID, reqAddr} -} - -func (l *udpMockEventLogger) Close(sessionID uint32, err error) { - l.CloseCh <- udpMockEventClose{sessionID, err} -} - func TestUDPSessionManager(t *testing.T) { - msgReceiveCh := make(chan *protocol.UDPMessage, 10) - msgSendCh := make(chan *protocol.UDPMessage, 10) - io := &udpMockIO{ - ReceiveCh: msgReceiveCh, - SendCh: msgSendCh, - } - eventNewCh := make(chan udpMockEventNew, 10) - eventCloseCh := make(chan udpMockEventClose, 10) - eventLogger := &udpMockEventLogger{ - NewCh: eventNewCh, - CloseCh: eventCloseCh, - } + io := newMockUDPIO(t) + eventLogger := newMockUDPEventLogger(t) sm := newUDPSessionManager(io, eventLogger, 2*time.Second) + + msgCh := make(chan *protocol.UDPMessage, 4) + io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { + m := <-msgCh + if m == nil { + return nil, errors.New("closed") + } + return m, nil + }) + go sm.Run() - t.Run("session creation & timeout", func(t *testing.T) { - ms := []*protocol.UDPMessage{ - { - SessionID: 1234, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "example.com:5353", - Data: []byte("hello"), - }, - { - SessionID: 5678, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "example.com:9999", - Data: []byte("goodbye"), - }, - { - SessionID: 1234, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "example.com:5353", - Data: []byte(" world"), - }, - { - SessionID: 5678, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "example.com:9999", - Data: []byte(" girl"), - }, - } - for _, m := range ms { - msgReceiveCh <- m - } - // New event order should be consistent with message order - for i := 0; i < 2; i++ { - newEvent := <-eventNewCh - if newEvent.SessionID != ms[i].SessionID || newEvent.ReqAddr != ms[i].Addr { - t.Errorf("unexpected new event value: %d:%s", newEvent.SessionID, newEvent.ReqAddr) - } - } - // Message order is not guaranteed so we use a map - msgMap := make(map[string]bool) - for i := 0; i < 4; i++ { - msg := <-msgSendCh - msgMap[fmt.Sprintf("%d:%s:%s", msg.SessionID, msg.Addr, string(msg.Data))] = true - } - for _, m := range ms { - key := fmt.Sprintf("%d:%s:%s", m.SessionID, m.Addr, string(m.Data)) - if !msgMap[key] { - t.Errorf("missing message: %s", key) - } - } - // Timeout check - startTime := time.Now() - closeMap := make(map[uint32]bool) - for i := 0; i < 2; i++ { - closeEvent := <-eventCloseCh - closeMap[closeEvent.SessionID] = true - } - for i := 0; i < 2; i++ { - if !closeMap[ms[i].SessionID] { - t.Errorf("missing close event: %d", ms[i].SessionID) - } - } - dur := time.Since(startTime) - if dur < 2*time.Second || dur > 4*time.Second { - t.Errorf("unexpected timeout duration: %s", dur) + udpReadFunc := func(addr string, ch chan []byte, b []byte) (int, string, error) { + bs := <-ch + if bs == nil { + return 0, "", errors.New("closed") } + n := copy(b, bs) + return n, addr, nil + } + + // Test normal session creation & timeout + msg1 := &protocol.UDPMessage{ + SessionID: 1234, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "address1.com:9000", + Data: []byte("hello"), + } + eventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once() + udpConn1 := newMockUDPConn(t) + udpConn1Ch := make(chan []byte, 1) + io.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once() + udpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once() + udpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { + return udpReadFunc(msg1.Addr, udpConn1Ch, b) }) + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg1.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg1.Addr, + Data: []byte("hi back"), + }).Return(nil).Once() + msgCh <- msg1 + udpConn1Ch <- []byte("hi back") - t.Run("UDP connection close", func(t *testing.T) { - // Close UDP connection immediately after creation - io.UDPClose = true - - m := &protocol.UDPMessage{ - SessionID: 8888, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "mygod.org:1514", - Data: []byte("goodnight"), - } - msgReceiveCh <- m - // Should have both new and close events immediately - newEvent := <-eventNewCh - if newEvent.SessionID != m.SessionID || newEvent.ReqAddr != m.Addr { - t.Errorf("unexpected new event value: %d:%s", newEvent.SessionID, newEvent.ReqAddr) - } - closeEvent := <-eventCloseCh - if closeEvent.SessionID != m.SessionID || closeEvent.Err != errUDPClosed { - t.Errorf("unexpected close event value: %d:%s", closeEvent.SessionID, closeEvent.Err) - } + msg2 := &protocol.UDPMessage{ + SessionID: 5678, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "address2.net:12450", + Data: []byte("how are you"), + } + eventLogger.EXPECT().New(msg2.SessionID, msg2.Addr).Return().Once() + udpConn2 := newMockUDPConn(t) + udpConn2Ch := make(chan []byte, 1) + io.EXPECT().UDP(msg2.Addr).Return(udpConn2, nil).Once() + udpConn2.EXPECT().WriteTo(msg2.Data, msg2.Addr).Return(11, nil).Once() + udpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { + return udpReadFunc(msg2.Addr, udpConn2Ch, b) }) + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg2.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg2.Addr, + Data: []byte("im fine"), + }).Return(nil).Once() + msgCh <- msg2 + udpConn2Ch <- []byte("im fine") - t.Run("UDP IO failure", func(t *testing.T) { - // Block UDP connection creation - io.BlockUDP = true + msg3 := &protocol.UDPMessage{ + SessionID: 1234, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "address1.com:9000", + Data: []byte("who are you?"), + } + udpConn1.EXPECT().WriteTo(msg3.Data, msg3.Addr).Return(12, nil).Once() + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg3.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg3.Addr, + Data: []byte("im your father"), + }).Return(nil).Once() + msgCh <- msg3 + udpConn1Ch <- []byte("im your father") - m := &protocol.UDPMessage{ - SessionID: 9999, - PacketID: 0, - FragID: 0, - FragCount: 1, - Addr: "xxx.net:12450", - Data: []byte("nope"), - } - msgReceiveCh <- m - // Should have both new and close events immediately - newEvent := <-eventNewCh - if newEvent.SessionID != m.SessionID || newEvent.ReqAddr != m.Addr { - t.Errorf("unexpected new event value: %d:%s", newEvent.SessionID, newEvent.ReqAddr) - } - closeEvent := <-eventCloseCh - if closeEvent.SessionID != m.SessionID || closeEvent.Err != errUDPBlocked { - t.Errorf("unexpected close event value: %d:%s", closeEvent.SessionID, closeEvent.Err) - } - }) + // Make sure timeout works (connections closed & close events emitted) + udpConn1.EXPECT().Close().RunAndReturn(func() error { + close(udpConn1Ch) + return nil + }).Once() + udpConn2.EXPECT().Close().RunAndReturn(func() error { + close(udpConn2Ch) + return nil + }).Once() + eventLogger.EXPECT().Close(msg1.SessionID, nil).Once() + eventLogger.EXPECT().Close(msg2.SessionID, nil).Once() + + time.Sleep(3 * time.Second) // Wait for timeout + mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2) + + // Test UDP connection close error propagation + errUDPClosed := errors.New("UDP connection closed") + msg4 := &protocol.UDPMessage{ + SessionID: 666, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "oh-no.com:27015", + Data: []byte("dont say bye"), + } + eventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once() + udpConn4 := newMockUDPConn(t) + io.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once() + udpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once() + udpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, "", errUDPClosed).Once() + udpConn4.EXPECT().Close().Return(nil).Once() + eventLogger.EXPECT().Close(msg4.SessionID, errUDPClosed).Once() + msgCh <- msg4 + + time.Sleep(1 * time.Second) + mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn4) + + // Test UDP connection creation error propagation + errUDPIO := errors.New("UDP IO error") + msg5 := &protocol.UDPMessage{ + SessionID: 777, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "callmemaybe.com:15353", + Data: []byte("babe i miss you"), + } + eventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once() + io.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once() + eventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once() + msgCh <- msg5 + + time.Sleep(1 * time.Second) + mock.AssertExpectationsForObjects(t, io, eventLogger) // Leak checks - msgReceiveCh <- nil - time.Sleep(1 * time.Second) // Give some time for the goroutines to exit - if sm.Count() != 0 { - t.Error("session count should be 0") - } + close(msgCh) // This will return error from ReceiveMessage(), should stop the session manager + time.Sleep(1 * time.Second) // Wait one more second just to be sure + assert.Equal(t, sm.Count(), 0, "session count should be 0") goleak.VerifyNone(t) } diff --git a/go.work.sum b/go.work.sum index 4e42e3a..222df4f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -26,11 +26,7 @@ github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfE github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= @@ -49,9 +45,9 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=