Skip to content

Commit fad9258

Browse files
committed
connection: add tests
This verifies that both unix:/// and absolute paths work and that the timing behavior is as expected (wait for server, react promptly once it appears).
1 parent 8147e17 commit fad9258

File tree

7 files changed

+2572
-2
lines changed

7 files changed

+2572
-2
lines changed

Gopkg.lock

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

connection/connection_test.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package connection
18+
19+
import (
20+
"context"
21+
"io/ioutil"
22+
"net"
23+
"os"
24+
"path"
25+
"sync"
26+
"testing"
27+
"time"
28+
29+
"google.golang.org/grpc"
30+
"google.golang.org/grpc/codes"
31+
"google.golang.org/grpc/connectivity"
32+
"google.golang.org/grpc/status"
33+
34+
"github.com/stretchr/testify/assert"
35+
"github.com/stretchr/testify/require"
36+
)
37+
38+
func tmpDir(t *testing.T) string {
39+
dir, err := ioutil.TempDir("", "connect")
40+
require.NoError(t, err, "creating temp directory")
41+
return dir
42+
}
43+
44+
const (
45+
serverSock = "server.sock"
46+
)
47+
48+
// startServer creates a gRPC server without any registered services.
49+
// The returned address can be used to connect to it. The cleanup
50+
// function stops it. It can be called multiple times.
51+
func startServer(t *testing.T, tmp string) (string, func()) {
52+
addr := path.Join(tmp, serverSock)
53+
listener, err := net.Listen("unix", addr)
54+
require.NoError(t, err, "listening on %s", addr)
55+
server := grpc.NewServer()
56+
var wg sync.WaitGroup
57+
wg.Add(1)
58+
go func() {
59+
defer wg.Done()
60+
if err := server.Serve(listener); err != nil {
61+
t.Logf("starting server failed: %s", err)
62+
}
63+
}()
64+
return addr, func() {
65+
server.Stop()
66+
wg.Wait()
67+
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
68+
t.Logf("remove Unix socket: %s", err)
69+
}
70+
}
71+
}
72+
73+
func TestConnect(t *testing.T) {
74+
tmp := tmpDir(t)
75+
defer os.RemoveAll(tmp)
76+
addr, stopServer := startServer(t, tmp)
77+
defer stopServer()
78+
79+
conn, err := Connect(addr)
80+
if assert.NoError(t, err, "connect via absolute path") &&
81+
assert.NotNil(t, conn, "got a connection") {
82+
assert.Equal(t, connectivity.Ready, conn.GetState(), "connection ready")
83+
err = conn.Close()
84+
assert.NoError(t, err, "closing connection")
85+
}
86+
}
87+
88+
func TestConnectUnix(t *testing.T) {
89+
tmp := tmpDir(t)
90+
defer os.RemoveAll(tmp)
91+
addr, stopServer := startServer(t, tmp)
92+
defer stopServer()
93+
94+
conn, err := Connect("unix:///" + addr)
95+
if assert.NoError(t, err, "connect with unix:/// prefix") &&
96+
assert.NotNil(t, conn, "got a connection") {
97+
assert.Equal(t, connectivity.Ready, conn.GetState(), "connection ready")
98+
err = conn.Close()
99+
assert.NoError(t, err, "closing connection")
100+
}
101+
}
102+
103+
func TestWaitForServer(t *testing.T) {
104+
tmp := tmpDir(t)
105+
defer os.RemoveAll(tmp)
106+
107+
// We cannot test that Connect() waits forever for the server
108+
// to appear, because then we would have to let the test run
109+
// forever.... What we can test is that it returns shortly
110+
// after the server appears.
111+
startTime := time.Now()
112+
var startTimeServer time.Time
113+
var stopServer func()
114+
var wg sync.WaitGroup
115+
wg.Add(1)
116+
defer func() {
117+
wg.Wait()
118+
stopServer()
119+
}()
120+
// Here we pick a relatively long delay before we start the
121+
// server. If gRPC did go into an exponential backoff before
122+
// retrying the connection attempt, then it probably would
123+
// not react promptly to the server becoming ready. Currently
124+
// it looks like gRPC tries to connect once per second, with
125+
// no exponential backoff.
126+
delay := 10 * time.Second
127+
go func() {
128+
defer wg.Done()
129+
t.Logf("sleeping %s before starting server", delay)
130+
time.Sleep(delay)
131+
startTimeServer = time.Now()
132+
_, stopServer = startServer(t, tmp)
133+
}()
134+
conn, err := Connect(path.Join(tmp, serverSock))
135+
if assert.NoError(t, err, "connect via absolute path") {
136+
endTime := time.Now()
137+
assert.NotNil(t, conn, "got a connection")
138+
assert.Equal(t, connectivity.Ready.String(), conn.GetState().String(), "connection ready")
139+
if assert.InEpsilon(t, 1*time.Second, endTime.Sub(startTimeServer), 5, "connection established shortly after server starts") {
140+
assert.InEpsilon(t, delay, endTime.Sub(startTime), 1)
141+
}
142+
err = conn.Close()
143+
assert.NoError(t, err, "closing connection")
144+
}
145+
}
146+
147+
func TestTimout(t *testing.T) {
148+
tmp := tmpDir(t)
149+
defer os.RemoveAll(tmp)
150+
151+
startTime := time.Now()
152+
timeout := 5 * time.Second
153+
conn, err := connect(path.Join(tmp, "no-such.sock"), []grpc.DialOption{grpc.WithTimeout(timeout)}, nil)
154+
endTime := time.Now()
155+
if assert.Error(t, err, "connection should fail") {
156+
assert.InEpsilon(t, timeout, endTime.Sub(startTime), 1, "connection timeout")
157+
} else {
158+
err := conn.Close()
159+
assert.NoError(t, err, "closing connection")
160+
}
161+
}
162+
163+
func TestReconnect(t *testing.T) {
164+
tmp := tmpDir(t)
165+
defer os.RemoveAll(tmp)
166+
addr, stopServer := startServer(t, tmp)
167+
defer func() {
168+
stopServer()
169+
}()
170+
171+
// Allow reconnection (the default).
172+
conn, err := Connect(addr)
173+
if assert.NoError(t, err, "connect via absolute path") &&
174+
assert.NotNil(t, conn, "got a connection") {
175+
defer conn.Close()
176+
assert.Equal(t, connectivity.Ready, conn.GetState(), "connection ready")
177+
178+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
179+
errStatus, _ := status.FromError(err)
180+
assert.Equal(t, codes.Unimplemented, errStatus.Code(), "not implemented")
181+
}
182+
183+
stopServer()
184+
startTime := time.Now()
185+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
186+
endTime := time.Now()
187+
errStatus, _ := status.FromError(err)
188+
assert.Equal(t, codes.Unavailable, errStatus.Code(), "connection lost")
189+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be detected quickly")
190+
}
191+
192+
// No reconnection either when the server comes back.
193+
_, stopServer = startServer(t, tmp)
194+
// We need to give gRPC some time. It does not attempt to reconnect
195+
// immediately. If we send the method call too soon, the test passes
196+
// even though a later method call will go through again.
197+
time.Sleep(5 * time.Second)
198+
startTime = time.Now()
199+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
200+
endTime := time.Now()
201+
errStatus, _ := status.FromError(err)
202+
assert.Equal(t, codes.Unimplemented, errStatus.Code(), "not implemented")
203+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be covered from quickly")
204+
}
205+
}
206+
}
207+
208+
func TestDisconnect(t *testing.T) {
209+
tmp := tmpDir(t)
210+
defer os.RemoveAll(tmp)
211+
addr, stopServer := startServer(t, tmp)
212+
defer func() {
213+
stopServer()
214+
}()
215+
216+
reconnectCount := 0
217+
conn, err := Connect(addr, OnConnectionLoss(func() bool {
218+
reconnectCount++
219+
// Don't reconnect.
220+
return false
221+
}))
222+
if assert.NoError(t, err, "connect via absolute path") &&
223+
assert.NotNil(t, conn, "got a connection") {
224+
defer conn.Close()
225+
assert.Equal(t, connectivity.Ready, conn.GetState(), "connection ready")
226+
227+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
228+
errStatus, _ := status.FromError(err)
229+
assert.Equal(t, codes.Unimplemented, errStatus.Code(), "not implemented")
230+
}
231+
232+
stopServer()
233+
startTime := time.Now()
234+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
235+
endTime := time.Now()
236+
errStatus, _ := status.FromError(err)
237+
assert.Equal(t, codes.Unavailable, errStatus.Code(), "connection lost")
238+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be detected quickly")
239+
}
240+
241+
// No reconnection either when the server comes back.
242+
_, stopServer = startServer(t, tmp)
243+
// We need to give gRPC some time. It does not attempt to reconnect
244+
// immediately. If we send the method call too soon, the test passes
245+
// even though a later method call will go through again.
246+
time.Sleep(5 * time.Second)
247+
startTime = time.Now()
248+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
249+
endTime := time.Now()
250+
errStatus, _ := status.FromError(err)
251+
assert.Equal(t, codes.Unavailable, errStatus.Code(), "connection still lost")
252+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be detected quickly")
253+
}
254+
255+
assert.Equal(t, 1, reconnectCount, "connection loss callback should be called once")
256+
}
257+
}
258+
259+
func TestExplicitReconnect(t *testing.T) {
260+
tmp := tmpDir(t)
261+
defer os.RemoveAll(tmp)
262+
addr, stopServer := startServer(t, tmp)
263+
defer func() {
264+
stopServer()
265+
}()
266+
267+
reconnectCount := 0
268+
conn, err := Connect(addr, OnConnectionLoss(func() bool {
269+
reconnectCount++
270+
// Reconnect.
271+
return true
272+
}))
273+
if assert.NoError(t, err, "connect via absolute path") &&
274+
assert.NotNil(t, conn, "got a connection") {
275+
defer conn.Close()
276+
assert.Equal(t, connectivity.Ready, conn.GetState(), "connection ready")
277+
278+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
279+
errStatus, _ := status.FromError(err)
280+
assert.Equal(t, codes.Unimplemented, errStatus.Code(), "not implemented")
281+
}
282+
283+
stopServer()
284+
startTime := time.Now()
285+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
286+
endTime := time.Now()
287+
errStatus, _ := status.FromError(err)
288+
assert.Equal(t, codes.Unavailable, errStatus.Code(), "connection lost")
289+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be detected quickly")
290+
}
291+
292+
// No reconnection either when the server comes back.
293+
_, stopServer = startServer(t, tmp)
294+
// We need to give gRPC some time. It does not attempt to reconnect
295+
// immediately. If we send the method call too soon, the test passes
296+
// even though a later method call will go through again.
297+
time.Sleep(5 * time.Second)
298+
startTime = time.Now()
299+
if err := conn.Invoke(context.Background(), "/connect.v0.Test/Ping", nil, nil); assert.Error(t, err) {
300+
endTime := time.Now()
301+
errStatus, _ := status.FromError(err)
302+
assert.Equal(t, codes.Unimplemented, errStatus.Code(), "connection still lost")
303+
assert.InEpsilon(t, time.Second, endTime.Sub(startTime), 1, "connection loss should be recovered from quickly")
304+
}
305+
306+
assert.Equal(t, 1, reconnectCount, "connection loss callback should be called once")
307+
}
308+
}

vendor/github.com/stretchr/testify/require/doc.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/stretchr/testify/require/forward_requirements.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)