github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/prog/progtest/progtest.go (about) 1 // Package progtest provides utilities for testing subprograms. 2 // 3 // This package intentionally has no test file; it is excluded from test 4 // coverage. 5 package progtest 6 7 import ( 8 "io/ioutil" 9 "os" 10 "strings" 11 "testing" 12 13 "src.elv.sh/pkg/testutil" 14 ) 15 16 // Fixture is a test fixture suitable for testing programs. 17 type Fixture struct { 18 pipes [3]*pipe 19 dirCleanup func() 20 } 21 22 func captureOutput(p *pipe) { 23 b, err := ioutil.ReadAll(p.r) 24 if err != nil { 25 panic(err) 26 } 27 p.output <- b 28 } 29 30 // Setup sets up a test fixture. The caller is responsible for calling the 31 // Cleanup method of the returned Fixture. 32 func Setup() *Fixture { 33 _, dirCleanup := testutil.InTestDir() 34 pipes := [3]*pipe{makePipe(false), makePipe(true), makePipe(true)} 35 return &Fixture{pipes, dirCleanup} 36 } 37 38 // Cleanup cleans up the test fixture. 39 func (f *Fixture) Cleanup() { 40 f.pipes[0].close() 41 f.pipes[1].close() 42 f.pipes[2].close() 43 f.dirCleanup() 44 } 45 46 // Fds returns the file descriptors in the fixture. 47 func (f *Fixture) Fds() [3]*os.File { 48 return [3]*os.File{f.pipes[0].r, f.pipes[1].w, f.pipes[2].w} 49 } 50 51 // FeedIn feeds input to the standard input. 52 func (f *Fixture) FeedIn(s string) { 53 _, err := f.pipes[0].w.WriteString(s) 54 if err != nil { 55 panic(err) 56 } 57 f.pipes[0].w.Close() 58 f.pipes[0].wClosed = true 59 } 60 61 // TestOut tests that the output on the given FD matches the given text. 62 func (f *Fixture) TestOut(t *testing.T, fd int, wantOut string) { 63 t.Helper() 64 if out := f.pipes[fd].get(); out != wantOut { 65 t.Errorf("got out %q, want %q", out, wantOut) 66 } 67 } 68 69 // TestOutSnippet tests that the output on the given FD contains the given text. 70 func (f *Fixture) TestOutSnippet(t *testing.T, fd int, wantOutSnippet string) { 71 t.Helper() 72 if err := f.pipes[fd].get(); !strings.Contains(err, wantOutSnippet) { 73 t.Errorf("got out %q, want string containing %q", err, wantOutSnippet) 74 } 75 } 76 77 type pipe struct { 78 r, w *os.File 79 rClosed, wClosed bool 80 saved string 81 output chan []byte 82 } 83 84 func makePipe(capture bool) *pipe { 85 r, w, err := os.Pipe() 86 if err != nil { 87 panic(err) 88 } 89 if !capture { 90 return &pipe{r: r, w: w} 91 } 92 output := make(chan []byte, 1) 93 p := pipe{r: r, w: w, output: output} 94 go captureOutput(&p) 95 return &p 96 } 97 98 func (p *pipe) get() string { 99 if !p.wClosed { 100 // Close the write side so captureOutput goroutine sees EOF and 101 // terminates allowing us to capture and cache the output. 102 p.w.Close() 103 p.wClosed = true 104 if p.output != nil { 105 p.saved = string(<-p.output) 106 } 107 } 108 return p.saved 109 } 110 111 func (p *pipe) close() { 112 if !p.wClosed { 113 p.w.Close() 114 p.wClosed = true 115 if p.output != nil { 116 p.saved = string(<-p.output) 117 } 118 } 119 if !p.rClosed { 120 p.r.Close() 121 p.rClosed = true 122 } 123 if p.output != nil { 124 close(p.output) 125 p.output = nil 126 } 127 } 128 129 // MustWriteFile writes a file with the given name and content. It panics if the 130 // write fails. 131 func MustWriteFile(name, content string) { 132 err := ioutil.WriteFile(name, []byte(content), 0600) 133 if err != nil { 134 panic(err) 135 } 136 } 137 138 // Elvish returns an argument slice starting with "elvish". 139 func Elvish(args ...string) []string { 140 return append([]string{"elvish"}, args...) 141 } 142 143 // TestError tests the error result of a program. 144 func TestError(t *testing.T, f *Fixture, exit int, wantErrSnippet string) { 145 t.Helper() 146 if exit != 2 { 147 t.Errorf("got exit %v, want 2", exit) 148 } 149 f.TestOutSnippet(t, 2, wantErrSnippet) 150 }