Files
patterm/internal/app/client_net_test.go

158 lines
3.6 KiB
Go

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
}