detached bc9ddfa

test(app): use generated transport mocks

commit
bc9ddfa06f28e25f56305610422bfbc1b2a67d49
author
brice · · 2026-07-15 07:02 UTC
parent
3dc8ca0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +53 -78
M internal/app/run_test.go +53 -78
@@ -10,7 +10,6 @@ import (
1010 "os/exec"
1111 "path/filepath"
1212 "strings"
13- "sync"
1413 "testing"
1514 "time"
1615
@@ -25,20 +24,23 @@ import (
2524 "github.com/stretchr/testify/require"
2625 )
2726
28-type firstHelloTestTransport struct {
29- frames []ports.Frame
30-}
31-
32-func (t *firstHelloTestTransport) Send(ports.Frame) error { return nil }
33-func (t *firstHelloTestTransport) Recv() (ports.Frame, error) {
34- if len(t.frames) == 0 {
35- return ports.Frame{}, io.EOF
36- }
37- frame := t.frames[0]
38- t.frames = t.frames[1:]
39- return frame, nil
27+// scriptRecv makes a transport yield frames in order, then wait for done
28+// before returning EOF. The shared done channel coordinates proxy readers
29+// without relying on scheduling or sleeps.
30+func scriptRecv(tr *portsmocks.MockTransport, done <-chan struct{}, frames ...ports.Frame) *portsmocks.MockTransport_Recv_Call {
31+ script := make(chan ports.Frame, len(frames))
32+ for _, frame := range frames {
33+ script <- frame
34+ }
35+ return tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
36+ select {
37+ case frame := <-script:
38+ return frame, nil
39+ case <-done:
40+ return ports.Frame{}, io.EOF
41+ }
42+ })
4043 }
41-func (t *firstHelloTestTransport) Close() error { return nil }
4244
4345 func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
4446 for _, tt := range []struct {
@@ -63,10 +65,12 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
6365 Env: []string{"CLIENT=value"},
6466 }
6567 nonHello := ports.Frame{Type: ports.MsgInput, Payload: []byte("unchanged")}
66- transport := &firstHelloTestTransport{frames: []ports.Frame{
67- {Type: ports.MsgHello, Payload: ports.MarshalHello(original)},
68+ transport := portsmocks.NewMockTransport(t)
69+ done := make(chan struct{})
70+ scriptRecv(transport, done,
71+ ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(original)},
6872 nonHello,
69- }}
73+ ).Times(2)
7074 wrapped := newFirstHelloTransport(transport, tt.session, []string{"REMOTE=one", "TOKEN=a=b=c"})
7175
7276 frame, err := wrapped.Recv()
@@ -86,26 +90,24 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
8690
8791 func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) {
8892 original := remoteProxyHello()
89- client := newRemoteProxyClient(original)
90- daemon := newRemoteProxyDaemon()
93+ client, daemon, sent := newRemoteProxyTransports(t, original)
9194
9295 err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil)
9396 require.NoError(t, err)
9497
95- got := daemon.firstHello(t)
98+ got := firstHello(t, sent)
9699 original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"}
97100 require.Equal(t, original, got)
98101 }
99102
100103 func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) {
101104 original := remoteProxyHello()
102- client := newRemoteProxyClient(original)
103- daemon := newRemoteProxyDaemon()
105+ client, daemon, sent := newRemoteProxyTransports(t, original)
104106
105107 err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil)
106108 require.NoError(t, err)
107109
108- got := daemon.firstHello(t)
110+ got := firstHello(t, sent)
109111 original.Name = "remote-work"
110112 original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"}
111113 require.Equal(t, original, got)
@@ -126,62 +128,35 @@ func remoteProxyHello() ports.Hello {
126128 }
127129 }
128130
129-type remoteProxyClient struct {
130- first ports.Frame
131- done chan struct{}
132- once sync.Once
133-}
134-
135-func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient {
136- return &remoteProxyClient{
137- first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)},
138- done: make(chan struct{}),
139- }
140-}
141-
142-func (t *remoteProxyClient) Send(ports.Frame) error { return nil }
143-func (t *remoteProxyClient) Recv() (ports.Frame, error) {
144- if t.first.Type != 0 {
145- frame := t.first
146- t.first = ports.Frame{}
147- return frame, nil
148- }
149- <-t.done
150- return ports.Frame{}, io.EOF
151-}
152-func (t *remoteProxyClient) Close() error {
153- t.once.Do(func() { close(t.done) })
154- return nil
155-}
156-
157-type remoteProxyDaemon struct {
158- sent chan ports.Frame
159- done chan struct{}
160- once sync.Once
161-}
162-
163-func newRemoteProxyDaemon() *remoteProxyDaemon {
164- return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})}
165-}
166-
167-func (t *remoteProxyDaemon) Send(frame ports.Frame) error {
168- t.sent <- frame
169- t.once.Do(func() { close(t.done) })
170- return nil
171-}
172-func (t *remoteProxyDaemon) Recv() (ports.Frame, error) {
173- <-t.done
174- return ports.Frame{}, io.EOF
175-}
176-func (t *remoteProxyDaemon) Close() error {
177- t.once.Do(func() { close(t.done) })
178- return nil
179-}
180-func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello {
181- testingT.Helper()
182- frame := <-t.sent
131+func newRemoteProxyTransports(t *testing.T, hello ports.Hello) (*portsmocks.MockTransport, *portsmocks.MockTransport, <-chan ports.Frame) {
132+ t.Helper()
133+ client := portsmocks.NewMockTransport(t)
134+ daemon := portsmocks.NewMockTransport(t)
135+ clientDone := make(chan struct{})
136+ daemonDone := make(chan struct{})
137+ sent := make(chan ports.Frame, 1)
138+
139+ scriptRecv(client, clientDone, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}).Maybe()
140+ client.EXPECT().Close().RunAndReturn(func() error {
141+ close(clientDone)
142+ return nil
143+ }).Once()
144+ daemon.EXPECT().Send(mock.Anything).RunAndReturn(func(frame ports.Frame) error {
145+ sent <- frame
146+ close(daemonDone)
147+ return nil
148+ }).Once()
149+ scriptRecv(daemon, daemonDone).Once()
150+ daemon.EXPECT().Close().Return(nil).Once()
151+
152+ return client, daemon, sent
153+}
154+
155+func firstHello(t *testing.T, sent <-chan ports.Frame) ports.Hello {
156+ t.Helper()
157+ frame := <-sent
183158 hello, err := ports.UnmarshalHello(frame.Payload)
184- require.NoError(testingT, err)
159+ require.NoError(t, err)
185160 return hello
186161 }
187162
@@ -10,7 +10,6 @@ import (
10 "os/exec"10 "os/exec"
11 "path/filepath"11 "path/filepath"
12 "strings"12 "strings"
13- "sync"
14 "testing"13 "testing"
15 "time"14 "time"
16 15
@@ -25,20 +24,23 @@ import (
25 "github.com/stretchr/testify/require"24 "github.com/stretchr/testify/require"
26 )25 )
27 26
28-type firstHelloTestTransport struct {27+// scriptRecv makes a transport yield frames in order, then wait for done
29- frames []ports.Frame28+// before returning EOF. The shared done channel coordinates proxy readers
30-}29+// without relying on scheduling or sleeps.
31-30+func scriptRecv(tr *portsmocks.MockTransport, done <-chan struct{}, frames ...ports.Frame) *portsmocks.MockTransport_Recv_Call {
32-func (t *firstHelloTestTransport) Send(ports.Frame) error { return nil }31+ script := make(chan ports.Frame, len(frames))
33-func (t *firstHelloTestTransport) Recv() (ports.Frame, error) {32+ for _, frame := range frames {
34- if len(t.frames) == 0 {33+ script <- frame
35- return ports.Frame{}, io.EOF34+ }
36- }35+ return tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
37- frame := t.frames[0]36+ select {
38- t.frames = t.frames[1:]37+ case frame := <-script:
39- return frame, nil38+ return frame, nil
39+ case <-done:
40+ return ports.Frame{}, io.EOF
41+ }
42+ })
40 }43 }
41-func (t *firstHelloTestTransport) Close() error { return nil }
42 44
43 func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {45 func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
44 for _, tt := range []struct {46 for _, tt := range []struct {
@@ -63,10 +65,12 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
63 Env: []string{"CLIENT=value"},65 Env: []string{"CLIENT=value"},
64 }66 }
65 nonHello := ports.Frame{Type: ports.MsgInput, Payload: []byte("unchanged")}67 nonHello := ports.Frame{Type: ports.MsgInput, Payload: []byte("unchanged")}
66- transport := &firstHelloTestTransport{frames: []ports.Frame{68+ transport := portsmocks.NewMockTransport(t)
67- {Type: ports.MsgHello, Payload: ports.MarshalHello(original)},69+ done := make(chan struct{})
70+ scriptRecv(transport, done,
71+ ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(original)},
68 nonHello,72 nonHello,
69- }}73+ ).Times(2)
70 wrapped := newFirstHelloTransport(transport, tt.session, []string{"REMOTE=one", "TOKEN=a=b=c"})74 wrapped := newFirstHelloTransport(transport, tt.session, []string{"REMOTE=one", "TOKEN=a=b=c"})
71 75
72 frame, err := wrapped.Recv()76 frame, err := wrapped.Recv()
@@ -86,26 +90,24 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
86 90
87 func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) {91 func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) {
88 original := remoteProxyHello()92 original := remoteProxyHello()
89- client := newRemoteProxyClient(original)93+ client, daemon, sent := newRemoteProxyTransports(t, original)
90- daemon := newRemoteProxyDaemon()
91 94
92 err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil)95 err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil)
93 require.NoError(t, err)96 require.NoError(t, err)
94 97
95- got := daemon.firstHello(t)98+ got := firstHello(t, sent)
96 original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"}99 original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"}
97 require.Equal(t, original, got)100 require.Equal(t, original, got)
98 }101 }
99 102
100 func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) {103 func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) {
101 original := remoteProxyHello()104 original := remoteProxyHello()
102- client := newRemoteProxyClient(original)105+ client, daemon, sent := newRemoteProxyTransports(t, original)
103- daemon := newRemoteProxyDaemon()
104 106
105 err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil)107 err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil)
106 require.NoError(t, err)108 require.NoError(t, err)
107 109
108- got := daemon.firstHello(t)110+ got := firstHello(t, sent)
109 original.Name = "remote-work"111 original.Name = "remote-work"
110 original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"}112 original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"}
111 require.Equal(t, original, got)113 require.Equal(t, original, got)
@@ -126,62 +128,35 @@ func remoteProxyHello() ports.Hello {
126 }128 }
127 }129 }
128 130
129-type remoteProxyClient struct {131+func newRemoteProxyTransports(t *testing.T, hello ports.Hello) (*portsmocks.MockTransport, *portsmocks.MockTransport, <-chan ports.Frame) {
130- first ports.Frame132+ t.Helper()
131- done chan struct{}133+ client := portsmocks.NewMockTransport(t)
132- once sync.Once134+ daemon := portsmocks.NewMockTransport(t)
133-}135+ clientDone := make(chan struct{})
134-136+ daemonDone := make(chan struct{})
135-func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient {137+ sent := make(chan ports.Frame, 1)
136- return &remoteProxyClient{138+
137- first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)},139+ scriptRecv(client, clientDone, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}).Maybe()
138- done: make(chan struct{}),140+ client.EXPECT().Close().RunAndReturn(func() error {
139- }141+ close(clientDone)
140-}142+ return nil
141-143+ }).Once()
142-func (t *remoteProxyClient) Send(ports.Frame) error { return nil }144+ daemon.EXPECT().Send(mock.Anything).RunAndReturn(func(frame ports.Frame) error {
143-func (t *remoteProxyClient) Recv() (ports.Frame, error) {145+ sent <- frame
144- if t.first.Type != 0 {146+ close(daemonDone)
145- frame := t.first147+ return nil
146- t.first = ports.Frame{}148+ }).Once()
147- return frame, nil149+ scriptRecv(daemon, daemonDone).Once()
148- }150+ daemon.EXPECT().Close().Return(nil).Once()
149- <-t.done151+
150- return ports.Frame{}, io.EOF152+ return client, daemon, sent
151-}153+}
152-func (t *remoteProxyClient) Close() error {154+
153- t.once.Do(func() { close(t.done) })155+func firstHello(t *testing.T, sent <-chan ports.Frame) ports.Hello {
154- return nil156+ t.Helper()
155-}157+ frame := <-sent
156-
157-type remoteProxyDaemon struct {
158- sent chan ports.Frame
159- done chan struct{}
160- once sync.Once
161-}
162-
163-func newRemoteProxyDaemon() *remoteProxyDaemon {
164- return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})}
165-}
166-
167-func (t *remoteProxyDaemon) Send(frame ports.Frame) error {
168- t.sent <- frame
169- t.once.Do(func() { close(t.done) })
170- return nil
171-}
172-func (t *remoteProxyDaemon) Recv() (ports.Frame, error) {
173- <-t.done
174- return ports.Frame{}, io.EOF
175-}
176-func (t *remoteProxyDaemon) Close() error {
177- t.once.Do(func() { close(t.done) })
178- return nil
179-}
180-func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello {
181- testingT.Helper()
182- frame := <-t.sent
183 hello, err := ports.UnmarshalHello(frame.Payload)158 hello, err := ports.UnmarshalHello(frame.Payload)
184- require.NoError(testingT, err)159+ require.NoError(t, err)
185 return hello160 return hello
186 }161 }
187 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 12

Tags 11

↑↓ moveenter openesc close