attach default client to local daemon
This commit is contained in:
157
internal/app/client_net_test.go
Normal file
157
internal/app/client_net_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hjbdev/patterm/internal/protocol"
|
||||
)
|
||||
|
||||
func TestNetClientFrameLoopSendsFocusedInput(t *testing.T) {
|
||||
clientT, daemonT := protocol.NewLoopbackPair()
|
||||
inR, inW := ioPipe(t)
|
||||
out := &lockedBuffer{}
|
||||
|
||||
gotInput := make(chan protocol.Input, 1)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
f, err := daemonT.Recv()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if f.Type != protocol.FrameAttach {
|
||||
t.Errorf("first frame = %s, want attach", f.Type)
|
||||
errCh <- nil
|
||||
return
|
||||
}
|
||||
sendTestFrame(t, daemonT, protocol.FrameHello, protocol.Hello{Version: 1, ClientID: "test", ProjectKey: "project"})
|
||||
sendTestFrame(t, daemonT, protocol.FrameProjectList, protocol.ProjectList{})
|
||||
model := chromeModel{
|
||||
ProjectKey: "project",
|
||||
FocusedID: "p1",
|
||||
Processes: []childModel{{ID: "p1", Name: "shell", Kind: string(KindCommand), Status: string(StatusRunning)}},
|
||||
Sidebar: []navEntryModel{{ChildID: "p1"}},
|
||||
}
|
||||
sendTestFrame(t, daemonT, protocol.FrameChrome, protocol.Chrome{ProjectKey: "project", Model: mustMarshalTest(t, model)})
|
||||
sendTestFrame(t, daemonT, protocol.FramePaneSnapshot, protocol.PaneSnapshot{PaneID: "p1", Bytes: []byte("READY")})
|
||||
for {
|
||||
f, err := daemonT.Recv()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if f.Type != protocol.FrameInput {
|
||||
continue
|
||||
}
|
||||
input, err := protocol.Decode[protocol.Input](f)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
gotInput <- input
|
||||
_ = daemonT.Close()
|
||||
errCh <- nil
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
runCh := make(chan error, 1)
|
||||
go func() {
|
||||
runCh <- RunAttachedClient(ctx, ClientOptions{
|
||||
Transport: clientT,
|
||||
Stdin: inR,
|
||||
Stdout: out,
|
||||
Cols: 80,
|
||||
Rows: 24,
|
||||
})
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) && !bytes.Contains(out.Bytes(), []byte("READY")) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if !bytes.Contains(out.Bytes(), []byte("READY")) {
|
||||
t.Fatalf("snapshot was not rendered before input; output=%q", out.String())
|
||||
}
|
||||
if _, err := inW.Write([]byte("echo hi\r")); err != nil {
|
||||
t.Fatalf("write stdin: %v", err)
|
||||
}
|
||||
select {
|
||||
case input := <-gotInput:
|
||||
if input.PaneID != "p1" || string(input.Bytes) != "echo hi\r" {
|
||||
t.Fatalf("input = %#v", input)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("client did not forward input")
|
||||
}
|
||||
cancel()
|
||||
_ = inW.Close()
|
||||
select {
|
||||
case err := <-runCh:
|
||||
if err != nil {
|
||||
t.Fatalf("client run: %v", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("client did not stop")
|
||||
}
|
||||
if err := <-errCh; err != nil && err != protocol.ErrTransportClosed {
|
||||
t.Fatalf("daemon side: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type lockedBuffer struct {
|
||||
mu sync.Mutex
|
||||
b bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) Bytes() []byte {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return append([]byte(nil), b.b.Bytes()...)
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.b.String()
|
||||
}
|
||||
|
||||
func ioPipe(t *testing.T) (*io.PipeReader, *io.PipeWriter) {
|
||||
t.Helper()
|
||||
r, w := io.Pipe()
|
||||
return r, w
|
||||
}
|
||||
|
||||
func sendTestFrame[T any](t *testing.T, tr protocol.Transport, typ protocol.FrameType, payload T) {
|
||||
t.Helper()
|
||||
f, err := protocol.NewFrame(typ, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("frame %s: %v", typ, err)
|
||||
}
|
||||
if err := tr.Send(f); err != nil {
|
||||
t.Fatalf("send %s: %v", typ, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshalTest(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user