cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / e2e / pipe_test.go
pipe_test.go 5.6K
1package e2e23import (4 "bytes"5 "os"6 "strings"7 "sync"8 "syscall"9 "testing"1011 "github.com/creack/pty"12)1314// lockedBuffer is a piped session's standard output, written by the15// command's copying goroutine and read by the test.16type lockedBuffer struct {17 mu sync.Mutex18 b bytes.Buffer19}2021func (l *lockedBuffer) Write(p []byte) (int, error) {22 l.mu.Lock()23 defer l.mu.Unlock()24 return l.b.Write(p)25}2627func (l *lockedBuffer) String() string {28 l.mu.Lock()29 defer l.mu.Unlock()30 return l.b.String()31}3233// startPiped starts the session's command as a pipeline would, with34// stdin on its standard input and its standard output captured, and a35// new pseudo-terminal as its controlling terminal (what /dev/tty opens)36// on standard error: a shell's `... | 012 --pipe | ...` in a terminal.37func (s *session) startPiped(stdin string) (*os.File, error) {38 ptmx, tty, err := pty.Open()39 if err != nil {40 return nil, err41 }42 defer tty.Close()43 if err := pty.Setsize(ptmx, &pty.Winsize{Cols: s.cols, Rows: s.rows}); err != nil {44 ptmx.Close()45 return nil, err46 }47 s.stdout = &lockedBuffer{}48 s.cmd.Stdin = strings.NewReader(stdin)49 s.cmd.Stdout = s.stdout50 s.cmd.Stderr = tty51 s.cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 2}52 if err := s.cmd.Start(); err != nil {53 ptmx.Close()54 return nil, err55 }56 return ptmx, nil57}5859// finish waits for a piped session to end, returning its standard60// output and exit code.61func (s *session) finish() (string, int) {62 s.t.Helper()63 s.waitExit()64 s.cmd.Wait()65 return s.stdout.String(), s.cmd.ProcessState.ExitCode()66}6768const lsNUON = "[[name, size, modified]; [CLAUDE.md, 1646b, 2026-09-27T11:27:31-06:00], [go.mod, 812b, 2026-09-20T08:00:00-06:00], [e2e, 4.2kb, 2026-09-28T17:45:10-06:00]]\n"6970// TestPipeEditAndSend is ls | to nuon | 012 --pipe | ...: the table71// arrives on standard input and shows with its types on the terminal,72// an edit is made, and quitting sends it on standard output, which gets73// nothing else.74func TestPipeEditAndSend(t *testing.T) {75 t.Parallel()76 s := startWith(t, options{piped: true, stdin: lsNUON, env: []string{"TZ=America/Denver"}}, "--pipe")77 s.waitFor("Read 4 rows of NUON from standard input")78 s.waitFor("4.2 kB")79 s.waitFor("Ctrl+Q send the sheet as NUON")80 s.keys("<down>", "<down>", "<down>", "<down>", "notes.txt", "<tab>", "2048", "<enter>")81 s.waitFor("2048") // typed, a plain number: it goes out as an int82 s.keys("<ctrl+q>")83 s.waitFor("Send the sheet as NUON?")84 s.keys("<enter>")85 out, code := s.finish()86 want := "[[name, size, modified]; [CLAUDE.md, 1646b, 2026-09-27T11:27:31-06:00],\n" +87 "[go.mod, 812b, 2026-09-20T08:00:00-06:00],\n[e2e, 4200b, 2026-09-28T17:45:10-06:00],\n[notes.txt, 2048, null]]\n"88 if code != 0 || out != want {89 t.Errorf("exit %d, stdout:\n%s\nwant\n%s", code, out, want)90 }91}9293// TestPipeSendSelectionAsCSV sends a selection, in the format --to94// asks for.95func TestPipeSendSelectionAsCSV(t *testing.T) {96 t.Parallel()97 s := startWith(t, options{piped: true, stdin: lsNUON}, "--pipe", "--to", "csv")98 s.waitFor("4.2 kB")99 s.keys("<shift+down>", "<shift+down>", "<shift+right>")100 s.waitFor("send A1:B3 as CSV")101 s.keys("<ctrl+q>")102 s.waitFor("Send A1:B3 as CSV?")103 s.keys("<enter>")104 out, code := s.finish()105 if code != 0 || out != "name,size\nCLAUDE.md,1.6 kB\ngo.mod,812 B\n" {106 t.Errorf("exit %d, stdout %q", code, out)107 }108}109110// TestPipeSendSelectionWithoutAsking: --send selection makes Ctrl+Q111// send the selection at once.112func TestPipeSendSelectionWithoutAsking(t *testing.T) {113 t.Parallel()114 s := startWith(t, options{piped: true, stdin: lsNUON}, "--pipe", "--send", "selection", "--to", "csv")115 s.waitFor("Ctrl+Q send the sheet as CSV")116 s.keys("<shift+down>", "<shift+right>")117 s.waitFor("Ctrl+Q send A1:B2 as CSV")118 s.keys("<ctrl+q>")119 out, code := s.finish()120 if code != 0 || out != "name,size\nCLAUDE.md,1.6 kB\n" {121 t.Errorf("exit %d, stdout %q", code, out)122 }123}124125// TestPipeSendSheetWithoutAsking: --send sheet makes Ctrl+Q send the126// whole sheet, whatever is selected; File > Quit without sending still127// sends nothing.128func TestPipeSendSheetWithoutAsking(t *testing.T) {129 t.Parallel()130 s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n3,4\n"}, "--pipe", "--send", "sheet")131 s.waitFor("Read 3 rows of CSV from standard input")132 s.keys("<shift+down>")133 s.waitFor("Ctrl+Q send the sheet as CSV")134 s.keys("<ctrl+q>")135 out, code := s.finish()136 if code != 0 || out != "a,b\n1,2\n3,4\n" {137 t.Errorf("exit %d, stdout %q", code, out)138 }139140 s = startWith(t, options{piped: true, stdin: "a,b\n1,2\n"}, "--pipe", "--send", "sheet")141 s.waitFor("Read 2 rows of CSV from standard input")142 s.keys("<ctrl+k>", "without sending")143 s.waitFor("Quit without sending")144 s.keys("<enter>")145 out, code = s.finish()146 if code != 1 || out != "" {147 t.Errorf("quit without sending: exit %d, stdout %q", code, out)148 }149}150151// TestPipeQuitWithoutSending exits with status 1 and writes nothing.152func TestPipeQuitWithoutSending(t *testing.T) {153 t.Parallel()154 s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n"}, "--pipe")155 s.waitFor("Read 2 rows of CSV from standard input")156 s.keys("<ctrl+q>", "d")157 out, code := s.finish()158 if code != 1 || out != "" {159 t.Errorf("exit %d, stdout %q", code, out)160 }161}162163// TestStdinOnly: 012 - reads standard input and writes nothing to164// standard output.165func TestStdinOnly(t *testing.T) {166 t.Parallel()167 s := startWith(t, options{piped: true, stdin: `[{"a": 1, "b": "x"}]`}, "-")168 s.waitFor("Read 2 rows of JSON from standard input")169 s.keys("<ctrl+q>")170 s.waitFor("You have unsaved changes.")171 s.keys("d")172 out, code := s.finish()173 if code != 0 || out != "" {174 t.Errorf("exit %d, stdout %q", code, out)175 }176}