okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
d4534d5
main
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 { 16in * io.PipeReader 17feed * 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. 22ended chan struct {} 23endedOnce sync.Once 24} 25 26var _ Console = (* pipeConsole )(nil ) 27 28func newPipeConsole ()* pipeConsole { 29r ,w := io .Pipe () 30return & 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 ) { 38n ,err := c .in .Read (p ) 39if err != nil { 40c .endedOnce .Do (func () {close (c .ended ) }) 41 } 42return 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 { 50v ,_ := c .LookupEnv (k ) 51return v 52} 53 54func (c * pipeConsole )LookupEnv (k string ) (string ,bool ) { 55if k == "TERM" { 56return "xterm" ,true 57 } 58return "" ,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 ) { 73t .Helper () 74_ = c .feed .Close () 75deadline := time .After (5 * time .Second ) 76for waiting := true ;waiting ; { 77select { 78case <- c .ended : 79waiting = false 80case <- tm .Events (): 81case <- deadline : 82t .Fatal ("the input loop never saw the end of its input" ) 83 } 84 } 85if err := tm .Stop ();err != nil { 86t .Errorf ("Stop: %v" ,err ) 87 } 88}