okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

jane michelle arcfix: give event loop its own copy per read (#178)d4534d5

main
2.7 KiB88 linesraw
1package uv
2
3import (
4	"io"
5	"sync"
6	"testing"
7	"time"
8
9	"github.com/charmbracelet/x/term"
10)
11
12// pipeConsole is a Console over an io.Pipe: a terminal that is not there, so a
13// test can feed the input loop bytes exactly as a terminal would. its input is
14// not a File, so newPollReader takes the fallback reader on every platform.
15type pipeConsole struct {
16	in   *io.PipeReader
17	feed *io.PipeWriter
18
19	// ended is closed the first time a read of the input fails. the input
20	// loop returns on that error without touching the terminal again, so from
21	// then on Stop cannot race it.
22	ended     chan struct{}
23	endedOnce sync.Once
24}
25
26var _ Console = (*pipeConsole)(nil)
27
28func newPipeConsole() *pipeConsole {
29	r, w := io.Pipe()
30	return &pipeConsole{in: r, feed: w, ended: make(chan struct{})}
31}
32
33// send writes s as the terminal would. it returns once the input loop has
34// read it, which is how a pipe works.
35func (c *pipeConsole) send(s string) { _, _ = io.WriteString(c.feed, s) }
36
37func (c *pipeConsole) Read(p []byte) (int, error) {
38	n, err := c.in.Read(p)
39	if err != nil {
40		c.endedOnce.Do(func() { close(c.ended) })
41	}
42	return n, err
43}
44
45func (c *pipeConsole) Write(p []byte) (int, error) { return io.Discard.Write(p) }
46func (c *pipeConsole) Close() error                { return c.in.Close() }
47func (c *pipeConsole) Environ() []string           { return []string{"TERM=xterm"} }
48
49func (c *pipeConsole) Getenv(k string) string {
50	v, _ := c.LookupEnv(k)
51	return v
52}
53
54func (c *pipeConsole) LookupEnv(k string) (string, bool) {
55	if k == "TERM" {
56		return "xterm", true
57	}
58	return "", false
59}
60
61func (c *pipeConsole) Reader() io.Reader             { return c }
62func (c *pipeConsole) Writer() io.Writer             { return io.Discard }
63func (c *pipeConsole) MakeRaw() (*term.State, error) { return nil, nil }
64func (c *pipeConsole) Restore() error                { return nil }
65func (c *pipeConsole) GetSize() (int, int, error)    { return 80, 24, nil }
66func (c *pipeConsole) GetWinsize() (*Winsize, error) { return &Winsize{Col: 80, Row: 24}, nil }
67
68// stopAfterInputEnds ends the input and waits until the input loop has seen
69// that before calling Stop, so a test that is not about Stop cannot trip over
70// a Stop that races the input loop. events are drained meanwhile, since an
71// event loop blocked on delivery would keep the input loop from reading.
72func stopAfterInputEnds(t *testing.T, tm *Terminal, c *pipeConsole) {
73	t.Helper()
74	_ = c.feed.Close()
75	deadline := time.After(5 * time.Second)
76	for waiting := true; waiting; {
77		select {
78		case <-c.ended:
79			waiting = false
80		case <-tm.Events():
81		case <-deadline:
82			t.Fatal("the input loop never saw the end of its input")
83		}
84	}
85	if err := tm.Stop(); err != nil {
86		t.Errorf("Stop: %v", err)
87	}
88}