main bac21c6

probes_tcp_test.go RawHistoryFolder

1package deployment
2
3import (
4	"context"
5	"net"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/gordon/internal/domain"
13)
14
15// serveTCP accepts connections on loopback and hands each to handle.
16func serveTCP(t *testing.T, handle func(net.Conn)) string {
17	t.Helper()
18	ln, err := net.Listen("tcp", "127.0.0.1:0")
19	require.NoError(t, err)
20	t.Cleanup(func() { _ = ln.Close() })
21	go func() {
22		for {
23			conn, err := ln.Accept()
24			if err != nil {
25				return
26			}
27			go handle(conn)
28		}
29	}()
30	return ln.Addr().String()
31}
32
33// testSettle leaves a wide margin for the server goroutine to close first.
34const testSettle = 500 * time.Millisecond
35
36func TestTCPProbeOnce_RelayAcceptThenCloseIsNotReady(t *testing.T) {
37	// A host port relay with no listener behind it accepts and closes at once.
38	addr := serveTCP(t, func(c net.Conn) { _ = c.Close() })
39
40	err := tcpProbeOnce(context.Background(), addr, testSettle)
41
42	require.ErrorIs(t, err, errTCPProbeClosed)
43}
44
45func TestTCPProbeOnce_RelayResetIsNotReady(t *testing.T) {
46	addr := serveTCP(t, func(c net.Conn) {
47		if tc, ok := c.(*net.TCPConn); ok {
48			_ = tc.SetLinger(0) // close with RST
49		}
50		_ = c.Close()
51	})
52
53	err := tcpProbeOnce(context.Background(), addr, testSettle)
54
55	require.Error(t, err)
56}
57
58func TestTCPProbeOnce_SilentListenerIsReady(t *testing.T) {
59	// A workload that never speaks first (Postgres, Redis) keeps the connection open.
60	release := make(chan struct{})
61	t.Cleanup(func() { close(release) })
62	addr := serveTCP(t, func(c net.Conn) {
63		<-release
64		_ = c.Close()
65	})
66
67	start := time.Now()
68	err := tcpProbeOnce(context.Background(), addr, testSettle)
69
70	require.NoError(t, err)
71	assert.GreaterOrEqual(t, time.Since(start), testSettle)
72}
73
74func TestTCPProbeOnce_BannerIsReadyImmediately(t *testing.T) {
75	// A workload that speaks first (SSH, SMTP) is ready on its first byte.
76	addr := serveTCP(t, func(c net.Conn) {
77		_, _ = c.Write([]byte("SSH-2.0-test\r\n"))
78		_ = c.Close()
79	})
80
81	start := time.Now()
82	err := tcpProbeOnce(context.Background(), addr, 5*time.Second)
83
84	require.NoError(t, err)
85	assert.Less(t, time.Since(start), 2*time.Second)
86}
87
88func TestTCPProbeOnce_NoListenerFails(t *testing.T) {
89	ln, err := net.Listen("tcp", "127.0.0.1:0")
90	require.NoError(t, err)
91	addr := ln.Addr().String()
92	require.NoError(t, ln.Close())
93
94	require.Error(t, tcpProbeOnce(context.Background(), addr, testSettle))
95}
96
97func TestTCPProbeOnce_CanceledContextWinsOverData(t *testing.T) {
98	addr := serveTCP(t, func(c net.Conn) {
99		_, _ = c.Write([]byte("banner"))
100		_ = c.Close()
101	})
102	ctx, cancel := context.WithCancel(context.Background())
103	cancel()
104
105	require.Error(t, tcpProbeOnce(ctx, addr, testSettle))
106}
107
108func TestWaitTCPReady_SettleAfterBudgetIsNotReady(t *testing.T) {
109	// Each attempt settles only after the readiness budget expired: the wait
110	// must report a timeout instead of accepting the late success.
111	slowDial := func(ctx context.Context, _ string) error {
112		select {
113		case <-ctx.Done():
114		case <-time.After(400 * time.Millisecond):
115		}
116		return nil
117	}
118	deps := NewTestProbeDeps(nil, nil, slowDial)
119	spec := domain.AppService{
120		Name:      "relay",
121		TCP:       []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 9000}},
122		Readiness: domain.AppReadiness{Type: domain.AppReadinessTCP, Timeout: 200 * time.Millisecond},
123	}
124
125	start := time.Now()
126	err := waitTCPReadyWithDeps(context.Background(), deps, spec, map[int]int{9000: 40000})
127
128	require.ErrorContains(t, err, "TCP readiness timeout")
129	assert.Less(t, time.Since(start), 5*time.Second)
130}
131
132func TestWaitTCPReady_CallerCancelIsReturned(t *testing.T) {
133	deps := NewTestProbeDeps(nil, nil, func(context.Context, string) error { return errTCPProbeClosed })
134	spec := domain.AppService{
135		Name:      "relay",
136		TCP:       []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 9000}},
137		Readiness: domain.AppReadiness{Type: domain.AppReadinessTCP, Timeout: 10 * time.Second},
138	}
139	ctx, cancel := context.WithCancel(context.Background())
140	cancel()
141
142	err := waitTCPReadyWithDeps(ctx, deps, spec, map[int]int{9000: 40000})
143
144	require.ErrorIs(t, err, context.Canceled)
145}
146
147func TestTCPProbeOnce_ContextCancelStopsTheSettleWait(t *testing.T) {
148	release := make(chan struct{})
149	t.Cleanup(func() { close(release) })
150	addr := serveTCP(t, func(c net.Conn) {
151		<-release
152		_ = c.Close()
153	})
154	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
155	defer cancel()
156
157	start := time.Now()
158	err := tcpProbeOnce(ctx, addr, 10*time.Second)
159
160	require.ErrorIs(t, err, context.DeadlineExceeded)
161	assert.Less(t, time.Since(start), 5*time.Second)
162}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close