189 lines
4.0 KiB
Go
189 lines
4.0 KiB
Go
package terminal
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
|
||
)
|
||
|
||
// fakeSource is a scripted terminal Source: Attach hands out fake Streams from
|
||
// an embedded spawner (or a custom attachFn closure); IsAlive is scriptable.
|
||
// attachErr makes Attach fail.
|
||
type fakeSource struct {
|
||
spawner *fakeSpawner
|
||
attachFn func(ctx context.Context, rows, cols uint16) (ports.Stream, error)
|
||
mu sync.Mutex
|
||
alive bool
|
||
aliveErr error
|
||
attachErr error
|
||
}
|
||
|
||
func (f *fakeSource) Attach(ctx context.Context, _ ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) {
|
||
if f.attachErr != nil {
|
||
return nil, f.attachErr
|
||
}
|
||
if f.attachFn != nil {
|
||
return f.attachFn(ctx, rows, cols)
|
||
}
|
||
if f.spawner == nil {
|
||
f.spawner = &fakeSpawner{}
|
||
}
|
||
return f.spawner.spawn(rows, cols)
|
||
}
|
||
|
||
func (f *fakeSource) IsAlive(context.Context, ports.RuntimeHandle) (bool, error) {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return f.alive, f.aliveErr
|
||
}
|
||
|
||
func (f *fakeSource) setAlive(v bool) {
|
||
f.mu.Lock()
|
||
f.alive = v
|
||
f.mu.Unlock()
|
||
}
|
||
|
||
func (f *fakeSource) setAliveResult(v bool, err error) {
|
||
f.mu.Lock()
|
||
f.alive = v
|
||
f.aliveErr = err
|
||
f.mu.Unlock()
|
||
}
|
||
|
||
// fakePTY is a scripted ports.Stream: Read drains the out channel, Write
|
||
// records, Resize records, and Close unblocks reads.
|
||
type fakePTY struct {
|
||
out chan []byte
|
||
closed chan struct{}
|
||
once sync.Once
|
||
|
||
mu sync.Mutex
|
||
written []byte
|
||
resizes [][2]uint16
|
||
}
|
||
|
||
func newFakePTY() *fakePTY {
|
||
return &fakePTY{out: make(chan []byte, 64), closed: make(chan struct{})}
|
||
}
|
||
|
||
func (p *fakePTY) push(b []byte) { p.out <- b }
|
||
|
||
func (p *fakePTY) Read(b []byte) (int, error) {
|
||
select {
|
||
case chunk := <-p.out:
|
||
return copy(b, chunk), nil
|
||
case <-p.closed:
|
||
return 0, io.EOF
|
||
}
|
||
}
|
||
|
||
func (p *fakePTY) Write(b []byte) (int, error) {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
p.written = append(p.written, b...)
|
||
return len(b), nil
|
||
}
|
||
|
||
func (p *fakePTY) Resize(rows, cols uint16) error {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
p.resizes = append(p.resizes, [2]uint16{rows, cols})
|
||
return nil
|
||
}
|
||
|
||
func (p *fakePTY) Close() error {
|
||
p.once.Do(func() { close(p.closed) })
|
||
return nil
|
||
}
|
||
|
||
func (p *fakePTY) writtenBytes() []byte {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
out := make([]byte, len(p.written))
|
||
copy(out, p.written)
|
||
return out
|
||
}
|
||
|
||
func (p *fakePTY) resizeCalls() [][2]uint16 {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
return append([][2]uint16(nil), p.resizes...)
|
||
}
|
||
|
||
// fakeSpawner hands out pre-built fakePTYs in order; once exhausted it returns
|
||
// idle PTYs that block until closed (so a re-attach loop does not busy-spin).
|
||
// It is the attach seam the fakeSource backs: each Attach call is one spawn.
|
||
type fakeSpawner struct {
|
||
mu sync.Mutex
|
||
ptys []*fakePTY
|
||
n int
|
||
err error
|
||
created []*fakePTY
|
||
sizes [][2]uint16 // rows×cols passed to each attach call, in order
|
||
}
|
||
|
||
func (f *fakeSpawner) spawn(rows, cols uint16) (ports.Stream, error) {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
if f.err != nil {
|
||
return nil, f.err
|
||
}
|
||
f.sizes = append(f.sizes, [2]uint16{rows, cols})
|
||
var p *fakePTY
|
||
if f.n < len(f.ptys) {
|
||
p = f.ptys[f.n]
|
||
} else {
|
||
p = newFakePTY()
|
||
}
|
||
f.n++
|
||
f.created = append(f.created, p)
|
||
return p, nil
|
||
}
|
||
|
||
func (f *fakeSpawner) calls() int {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return f.n
|
||
}
|
||
|
||
func (f *fakeSpawner) spawnSizes() [][2]uint16 {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return append([][2]uint16(nil), f.sizes...)
|
||
}
|
||
|
||
// eventually polls cond until true or the deadline, failing the test otherwise.
|
||
func eventually(t *testing.T, d time.Duration, cond func() bool) {
|
||
t.Helper()
|
||
deadline := time.Now().Add(d)
|
||
for time.Now().Before(deadline) {
|
||
if cond() {
|
||
return
|
||
}
|
||
time.Sleep(2 * time.Millisecond)
|
||
}
|
||
t.Fatal("condition not met within " + d.String())
|
||
}
|
||
|
||
// safeBytes is a concurrency-safe byte accumulator for subscriber callbacks.
|
||
type safeBytes struct {
|
||
mu sync.Mutex
|
||
b []byte
|
||
}
|
||
|
||
func (s *safeBytes) add(p []byte) {
|
||
s.mu.Lock()
|
||
s.b = append(s.b, p...)
|
||
s.mu.Unlock()
|
||
}
|
||
|
||
func (s *safeBytes) string() string {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return string(s.b)
|
||
}
|