github.com/elves/Elvish@v0.12.0/eval/testutils.go (about) 1 // Framework for testing Elvish script. This file does not file a _test.go 2 // suffix so that it can be used from other packages that also want to test the 3 // modules they implement (e.g. edit: and re:). 4 // 5 // The entry point for the framework is the Test function, which accepts a 6 // *testing.T and a variadic number of test cases. Test cases are constructed 7 // using the That function followed by methods that add constraints on the test 8 // case. Overall, a test looks like: 9 // 10 // Test(t, 11 // That("put x").Puts("x"), 12 // That("echo x").Prints("x\n")) 13 // 14 // If some setup is needed, use the TestWithSetup function instead. 15 16 package eval 17 18 import ( 19 "bytes" 20 "errors" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "reflect" 25 "testing" 26 27 "github.com/elves/elvish/eval/vals" 28 "github.com/elves/elvish/parse" 29 "github.com/elves/elvish/util" 30 ) 31 32 // TestCase is a test case for Test. 33 type TestCase struct { 34 text string 35 want 36 } 37 38 type want struct { 39 out []interface{} 40 bytesOut []byte 41 err error 42 } 43 44 // A special value for want.err to indicate that any error, as long as not nil, 45 // is OK 46 var errAny = errors.New("any error") 47 48 // The following functions and methods are used to build Test structs. They are 49 // supposed to read like English, so a test that "put x" should put "x" reads: 50 // 51 // That("put x").Puts("x") 52 53 // That returns a new Test with the specified source code. 54 func That(text string) TestCase { 55 return TestCase{text: text} 56 } 57 58 // DoesNothing returns t unchanged. It is used to mark that a piece of code 59 // should simply does nothing. In particular, it shouldn't have any output and 60 // does not error. 61 func (t TestCase) DoesNothing() TestCase { 62 return t 63 } 64 65 // Puts returns an altered Test that requires the source code to produce the 66 // specified values in the value channel when evaluated. 67 func (t TestCase) Puts(vs ...interface{}) TestCase { 68 t.want.out = vs 69 return t 70 } 71 72 // Puts returns an altered Test that requires the source code to produce the 73 // specified strings in the value channel when evaluated. 74 func (t TestCase) PutsStrings(ss []string) TestCase { 75 t.want.out = make([]interface{}, len(ss)) 76 for i, s := range ss { 77 t.want.out[i] = s 78 } 79 return t 80 } 81 82 // Prints returns an altered test that requires the source code to produce 83 // the specified output in the byte pipe when evaluated. 84 func (t TestCase) Prints(s string) TestCase { 85 t.want.bytesOut = []byte(s) 86 return t 87 } 88 89 // ErrorsWith returns an altered Test that requires the source code to result in 90 // the specified error when evaluted. 91 func (t TestCase) ErrorsWith(err error) TestCase { 92 t.want.err = err 93 return t 94 } 95 96 // Errors returns an altered Test that requires the source code to result in any 97 // error when evaluated. 98 func (t TestCase) Errors() TestCase { 99 return t.ErrorsWith(errAny) 100 } 101 102 // Test runs test cases. For each test case, a new Evaler is created with 103 // NewEvaler. 104 func Test(t *testing.T, tests ...TestCase) { 105 TestWithSetup(t, func(*Evaler) {}, tests...) 106 } 107 108 // Test runs test cases. For each test case, a new Evaler is created with 109 // NewEvaler and passed to the setup function. 110 func TestWithSetup(t *testing.T, setup func(*Evaler), tests ...TestCase) { 111 for _, tt := range tests { 112 ev := NewEvaler() 113 setup(ev) 114 out, bytesOut, err := evalAndCollect(t, ev, []string{tt.text}, len(tt.want.out)) 115 116 first := true 117 errorf := func(format string, args ...interface{}) { 118 if first { 119 first = false 120 t.Errorf("eval(%q) fails:", tt.text) 121 } 122 t.Errorf(" "+format, args...) 123 } 124 125 if !matchOut(tt.want.out, out) { 126 errorf("got out=%v, want %v", out, tt.want.out) 127 } 128 if !bytes.Equal(tt.want.bytesOut, bytesOut) { 129 errorf("got bytesOut=%q, want %q", bytesOut, tt.want.bytesOut) 130 } 131 if !matchErr(tt.want.err, err) { 132 errorf("got err=%v, want %v", err, tt.want.err) 133 } 134 135 ev.Close() 136 } 137 } 138 139 func evalAndCollect(t *testing.T, ev *Evaler, texts []string, chsize int) ([]interface{}, []byte, error) { 140 // Collect byte output 141 bytesOut := []byte{} 142 pr, pw, _ := os.Pipe() 143 bytesDone := make(chan struct{}) 144 go func() { 145 for { 146 var buf [64]byte 147 nr, err := pr.Read(buf[:]) 148 bytesOut = append(bytesOut, buf[:nr]...) 149 if err != nil { 150 break 151 } 152 } 153 close(bytesDone) 154 }() 155 156 // Channel output 157 outs := []interface{}{} 158 159 // Eval error. Only that of the last text is saved. 160 var ex error 161 162 for i, text := range texts { 163 name := fmt.Sprintf("test%d.elv", i) 164 src := NewScriptSource(name, name, text) 165 166 op := mustParseAndCompile(t, ev, src) 167 168 outCh := make(chan interface{}, chsize) 169 outDone := make(chan struct{}) 170 go func() { 171 for v := range outCh { 172 outs = append(outs, v) 173 } 174 close(outDone) 175 }() 176 177 ports := []*Port{ 178 {File: os.Stdin, Chan: ClosedChan}, 179 {File: pw, Chan: outCh}, 180 {File: os.Stderr, Chan: BlackholeChan}, 181 } 182 183 ex = ev.eval(op, ports, src) 184 close(outCh) 185 <-outDone 186 } 187 188 pw.Close() 189 <-bytesDone 190 pr.Close() 191 192 return outs, bytesOut, ex 193 } 194 195 func mustParseAndCompile(t *testing.T, ev *Evaler, src *Source) Op { 196 n, err := parse.Parse(src.name, src.code) 197 if err != nil { 198 t.Fatalf("Parse(%q) error: %s", src.code, err) 199 } 200 op, err := ev.Compile(n, src) 201 if err != nil { 202 t.Fatalf("Compile(Parse(%q)) error: %s", src.code, err) 203 } 204 return op 205 } 206 207 func matchOut(want, got []interface{}) bool { 208 if len(got) == 0 && len(want) == 0 { 209 return true 210 } 211 if len(got) != len(want) { 212 return false 213 } 214 for i := range got { 215 if !vals.Equal(got[i], want[i]) { 216 return false 217 } 218 } 219 return true 220 } 221 222 func matchErr(want, got error) bool { 223 if got == nil { 224 return want == nil 225 } 226 return want == errAny || reflect.DeepEqual(got.(*Exception).Cause, want) 227 } 228 229 // MustMkdirAll calls os.MkdirAll and panics if an error is returned. It is 230 // mainly useful in tests. 231 func MustMkdirAll(name string, perm os.FileMode) { 232 err := os.MkdirAll(name, perm) 233 if err != nil { 234 panic(err) 235 } 236 } 237 238 // MustCreateEmpty creates an empty file, and panics if an error occurs. It is 239 // mainly useful in tests. 240 func MustCreateEmpty(name string) { 241 file, err := os.Create(name) 242 if err != nil { 243 panic(err) 244 } 245 file.Close() 246 } 247 248 // MustWriteFile calls ioutil.WriteFile and panics if an error occurs. It is 249 // mainly useful in tests. 250 func MustWriteFile(filename string, data []byte, perm os.FileMode) { 251 err := ioutil.WriteFile(filename, data, perm) 252 if err != nil { 253 panic(err) 254 } 255 } 256 257 // InTempHome is like util.InTempDir, but it also sets HOME to the temporary 258 // directory when f is called. 259 func InTempHome(f func(string)) { 260 util.InTempDir(func(tmpHome string) { 261 oldHome := os.Getenv("HOME") 262 os.Setenv("HOME", tmpHome) 263 f(tmpHome) 264 os.Setenv("HOME", oldHome) 265 }) 266 }