1//go:build darwin
2
3package pty_test
4
5import (
6 "bytes"
7 "context"
8 "io"
9 "os"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/bnema/vev/internal/adapters/pty"
15 "github.com/bnema/vev/internal/domain"
16 "github.com/bnema/vev/internal/ports"
17 "github.com/stretchr/testify/require"
18)
19
20func readAllDarwin(t *testing.T, p ports.PTY, deadline time.Duration) []byte {
21 t.Helper()
22 type result struct {
23 output []byte
24 err error
25 }
26 resultCh := make(chan result, 1)
27 go func() {
28 var output bytes.Buffer
29 _, err := io.Copy(&output, p)
30 resultCh <- result{output: output.Bytes(), err: err}
31 }()
32
33 select {
34 case result := <-resultCh:
35 require.NoError(t, result.err)
36 return result.output
37 case <-time.After(deadline):
38 t.Fatal("timed out reading from pty")
39 return nil
40 }
41}
42
43func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) {
44 if testing.Short() {
45 t.Skip("skipping pty integration test in -short mode")
46 }
47
48 factory := pty.NewFactory()
49 outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
50 require.NoError(t, err)
51 t.Cleanup(func() { _ = outputPTY.Close() })
52 require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second)))
53
54 echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
55 require.NoError(t, err)
56 t.Cleanup(func() { _ = echoPTY.Close() })
57 _, err = io.WriteString(echoPTY, "roundtrip\n")
58 require.NoError(t, err)
59
60 output := readUntilDarwin(t, echoPTY, "roundtrip", 5*time.Second)
61 require.Contains(t, output, "roundtrip")
62}
63
64func readUntilDarwin(t *testing.T, p ports.PTY, marker string, deadline time.Duration) string {
65 t.Helper()
66 type result struct {
67 output string
68 err error
69 }
70 resultCh := make(chan result, 1)
71 go func() {
72 var output strings.Builder
73 buf := make([]byte, 256)
74 for !strings.Contains(output.String(), marker) {
75 n, err := p.Read(buf)
76 output.Write(buf[:n])
77 if err != nil {
78 resultCh <- result{output: output.String(), err: err}
79 return
80 }
81 }
82 resultCh <- result{output: output.String()}
83 }()
84
85 select {
86 case result := <-resultCh:
87 require.NoError(t, result.err)
88 return result.output
89 case <-time.After(deadline):
90 t.Fatal("timed out waiting for pty readiness")
91 return ""
92 }
93}
94
95func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) {
96 if testing.Short() {
97 t.Skip("skipping pty integration test in -short mode")
98 }
99
100 const ready = "__vev_ready__"
101 p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "stty -echo; printf '__vev_ready__\\n'; IFS= read -r release; stty size"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
102 require.NoError(t, err)
103 t.Cleanup(func() { _ = p.Close() })
104
105 require.Contains(t, readUntilDarwin(t, p, ready, 5*time.Second), ready)
106 pgid, err := p.ForegroundPgid()
107 require.NoError(t, err)
108 require.Greater(t, pgid, 0)
109 require.NoError(t, p.Resize(domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}}))
110 _, err = io.WriteString(p, "release\n")
111 require.NoError(t, err)
112 require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second))))
113}
114
115func TestOpen_DarwinContextCancellationEndsStream(t *testing.T) {
116 if testing.Short() {
117 t.Skip("skipping pty integration test in -short mode")
118 }
119
120 ctx, cancel := context.WithCancel(context.Background())
121 defer cancel()
122 p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
123 require.NoError(t, err)
124 t.Cleanup(func() { _ = p.Close() })
125
126 streamEnded := make(chan error, 1)
127 go func() {
128 _, err := io.Copy(io.Discard, p)
129 streamEnded <- err
130 }()
131
132 cancel()
133 select {
134 case err := <-streamEnded:
135 require.NoError(t, err)
136 case <-time.After(5 * time.Second):
137 t.Fatal("pty stream did not end after context cancellation")
138 }
139}