//go:build linux package term import ( "io" "os" "syscall" "testing" "time" "github.com/bnema/vev/pkg/rawterm" ) // openPTYPair opens a real Linux PTY master/slave pair directly via // /dev/ptmx, self-contained (no import of the vev pty adapter package, // which is being built in parallel). func openPTYPair(t *testing.T) (master, slave *os.File) { t.Helper() m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0) if err != nil { t.Fatalf("open /dev/ptmx: %v", err) } s, err := rawterm.PreparePty(int(m.Fd())) if err != nil { _ = m.Close() t.Fatalf("prepare pty: %v", err) } return m, s } // TestTerminal_RealPTY_RawModeSizeAndEscapes is the ONE integration test // exercising the real-tty paths (MakeRaw/Restore, GetSize) against an // actual PTY, since those are pure-syscall lines that unit tests with // pipes can't cover. func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { master, slave := openPTYPair(t) defer func() { _ = master.Close() }() defer func() { _ = slave.Close() }() if err := rawterm.SetWinsizeFull(int(slave.Fd()), rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384}); err != nil { t.Fatalf("set winsize: %v", err) } // Drain everything written to the slave (escape sequences) so the // batched writer's Flush never blocks on the PTY's internal buffer. captured := make(chan []byte, 1) go func() { buf, _ := io.ReadAll(master) captured <- buf }() tm := NewWithFiles(slave, slave) geometry, err := tm.Geometry() if err != nil { t.Fatalf("Geometry: %v", err) } if geometry.Cols != 80 || geometry.Rows != 24 { t.Fatalf("Geometry = %+v, want {Cols:80 Rows:24}", geometry) } if geometry.PixelWidth != 640 || geometry.PixelHeight != 384 { t.Fatalf("Geometry = %+v, want 640x384 pixels", geometry) } restore, err := tm.EnterRaw() if err != nil { t.Fatalf("EnterRaw: %v", err) } if tm.rawSkipped { t.Fatalf("expected rawSkipped=false for a real PTY") } if err := restore(); err != nil { t.Fatalf("restore: %v", err) } // Idempotent. if err := restore(); err != nil { t.Fatalf("second restore: %v", err) } // Closing the slave lets the reader goroutine observe EOF on the // master naturally. Don't also close master here: that would race // the reader's in-flight Read against this goroutine's Close on the // same fd and could discard already-buffered-but-unread data. if err := slave.Close(); err != nil { t.Fatalf("close slave: %v", err) } select { case got := <-captured: want := altScreenEnter + autowrapDisable + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + focusReportEnable + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + focusReportDisable + autowrapEnable + altScreenExit if string(got) != want { t.Fatalf("captured escapes = %q, want %q", got, want) } case <-time.After(5 * time.Second): t.Fatalf("timed out waiting for captured PTY output") } }