blob: 9ff7e9ae32a5bc5b6c3697c8b39ac364a348097b [file] [log] [blame]
Jiri Simsad7616c92015-03-24 23:44:30 -07001// Copyright 2015 The Vanadium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -07005// Package expect provides support for testing the contents from a buffered
6// input stream. It supports literal and pattern based matching. It is
7// line oriented; all of the methods (expect ReadAll) strip trailing newlines
8// from their return values. It places a timeout on all its operations.
9// It will generally be used to read from the stdout stream of subprocesses
10// in tests and other situations and to make 'assertions'
11// about what is to be read.
12//
13// A Session type is used to store state, in particular error state, across
14// consecutive invocations of its method set. If a particular method call
15// encounters an error then subsequent calls on that Session will have no
16// effect. This allows for a series of assertions to be made, one per line,
17// and for errors to be checked at the end. In addition Session is designed
18// to be easily used with the testing package; passing a testing.T instance
19// to NewSession allows it to set errors directly and hence tests will pass or
20// fail according to whether the expect assertions are met or not.
21//
22// Care is taken to ensure that the file and line number of the first
23// failed assertion in the session are recorded in the error stored in
24// the Session.
25//
26// Examples
27//
28// func TestSomething(t *testing.T) {
29// buf := []byte{}
30// buffer := bytes.NewBuffer(buf)
31// buffer.WriteString("foo\n")
32// buffer.WriteString("bar\n")
33// buffer.WriteString("baz\n")
James Ring3c8316a2015-02-04 10:48:04 -080034// s := expect.NewSession(t, bufio.NewReader(buffer), time.Second)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070035// s.Expect("foo")
36// s.Expect("bars)
37// if got, want := s.ReadLine(), "baz"; got != want {
38// t.Errorf("got %v, want %v", got, want)
39// }
40// }
41//
42package expect
43
44import (
45 "bufio"
46 "errors"
47 "fmt"
48 "io"
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -070049 "os"
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070050 "path/filepath"
51 "regexp"
52 "runtime"
53 "strings"
54 "time"
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -070055
Jiri Simsa337af232015-02-27 14:36:46 -080056 "v.io/x/lib/vlog"
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070057)
58
59var (
60 Timeout = errors.New("timeout")
61)
62
63// Session represents the state of an expect session.
64type Session struct {
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070065 input *bufio.Reader
66 timeout time.Duration
67 t Testing
68 verbose bool
69 oerr, err error
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070070}
71
72type Testing interface {
73 Error(args ...interface{})
James Ring3c8316a2015-02-04 10:48:04 -080074 Errorf(format string, args ...interface{})
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -070075 Log(args ...interface{})
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070076}
77
78// NewSession creates a new Session. The parameter t may be safely be nil.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070079func NewSession(t Testing, input io.Reader, timeout time.Duration) *Session {
80 return &Session{t: t, timeout: timeout, input: bufio.NewReader(input)}
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070081}
82
83// Failed returns true if an error has been encountered by a prior call.
84func (s *Session) Failed() bool {
85 return s.err != nil
86}
87
88// Error returns the error code (possibly nil) currently stored in the Session.
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070089// This will include the file and line of the calling function that experienced
90// the error. Use OriginalError to obtain the original error code.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070091func (s *Session) Error() error {
92 return s.err
93}
94
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070095// OriginalError returns any error code (possibly nil) returned by the
96// underlying library routines called.
97func (s *Session) OriginalError() error {
98 return s.oerr
99}
100
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700101// SetVerbosity enables/disable verbose debugging information, in particular,
102// every line of input read will be logged via Testing.Logf or, if it is nil,
103// to stderr.
104func (s *Session) SetVerbosity(v bool) {
105 s.verbose = v
106}
107
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700108func (s *Session) log(err error, format string, args ...interface{}) {
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700109 if !s.verbose {
110 return
111 }
112 _, path, line, _ := runtime.Caller(2)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700113 errstr := ""
114 if err != nil {
115 errstr = err.Error() + ": "
116 }
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700117 loc := fmt.Sprintf("%s:%d", filepath.Base(path), line)
118 o := strings.TrimRight(fmt.Sprintf(format, args...), "\n\t ")
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700119 vlog.VI(2).Infof("%s: %s%s", loc, errstr, o)
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700120 if s.t == nil {
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700121 fmt.Fprintf(os.Stderr, "%s: %s%s\n", loc, errstr, o)
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700122 return
123 }
124 s.t.Log(loc, o)
125}
126
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700127// ReportError calls Testing.Error to report any error currently stored
128// in the Session.
129func (s *Session) ReportError() {
130 if s.err != nil && s.t != nil {
131 s.t.Error(s.err)
132 }
133}
134
135// error must always be called from a public function that is called
136// directly by an external user, otherwise the file:line info will
137// be incorrect.
138func (s *Session) error(err error) error {
139 _, file, line, _ := runtime.Caller(2)
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -0700140 s.oerr = err
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700141 s.err = fmt.Errorf("%s:%d: %s", filepath.Base(file), line, err)
142 s.ReportError()
143 return s.err
144}
145
146type reader func(r *bufio.Reader) (string, error)
147
148func readAll(r *bufio.Reader) (string, error) {
149 all := ""
150 for {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700151 l, err := r.ReadString('\n')
152 all += l
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700153 if err != nil {
154 if err == io.EOF {
155 return all, nil
156 }
157 return all, err
158 }
159 }
160}
161
162func readLine(r *bufio.Reader) (string, error) {
163 return r.ReadString('\n')
164}
165
166func (s *Session) read(f reader) (string, error) {
167 ch := make(chan string, 1)
168 ech := make(chan error, 1)
169 go func(fn reader, io *bufio.Reader) {
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700170 str, err := fn(io)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700171 if err != nil {
172 ech <- err
173 return
174 }
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700175 ch <- str
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700176 }(f, s.input)
177 select {
178 case err := <-ech:
179 return "", err
180 case m := <-ch:
181 return m, nil
182 case <-time.After(s.timeout):
183 return "", Timeout
184 }
185}
186
187// Expect asserts that the next line in the input matches the supplied string.
188func (s *Session) Expect(expected string) {
189 if s.Failed() {
190 return
191 }
192 line, err := s.read(readLine)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700193 s.log(err, "Expect: %s", line)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700194 if err != nil {
195 s.error(err)
196 return
197 }
198 line = strings.TrimRight(line, "\n")
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700199
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700200 if line != expected {
201 s.error(fmt.Errorf("got %q, want %q", line, expected))
202 }
203 return
204}
205
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700206// Expectf asserts that the next line in the input matches the result of
207// formatting the supplied arguments. It's equivalent to
208// Expect(fmt.Sprintf(args))
209func (s *Session) Expectf(format string, args ...interface{}) {
210 if s.Failed() {
211 return
212 }
213 line, err := s.read(readLine)
214 s.log(err, "Expect: %s", line)
215 if err != nil {
216 s.error(err)
217 return
218 }
219 line = strings.TrimRight(line, "\n")
220 expected := fmt.Sprintf(format, args...)
221 if line != expected {
222 s.error(fmt.Errorf("got %q, want %q", line, expected))
223 }
224 return
225}
226
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700227func (s *Session) expectRE(pattern string, n int) (string, [][]string, error) {
228 if s.Failed() {
229 return "", nil, s.err
230 }
231 re, err := regexp.Compile(pattern)
232 if err != nil {
233 return "", nil, err
234 }
235 line, err := s.read(readLine)
236 if err != nil {
237 return "", nil, err
238 }
239 line = strings.TrimRight(line, "\n")
240 return line, re.FindAllStringSubmatch(line, n), err
241}
242
243// ExpectRE asserts that the next line in the input matches the pattern using
James Ringc0264952015-02-14 09:55:32 -0800244// regexp.MustCompile(pattern).FindAllStringSubmatch(..., n).
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700245func (s *Session) ExpectRE(pattern string, n int) [][]string {
246 if s.Failed() {
247 return [][]string{}
248 }
249 l, m, err := s.expectRE(pattern, n)
Bogdan Caprita490a4512014-11-20 21:12:19 -0800250 s.log(err, "ExpectRE: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700251 if err != nil {
252 s.error(err)
253 return [][]string{}
254 }
255 if len(m) == 0 {
256 s.error(fmt.Errorf("%q found no match in %q", pattern, l))
257 }
258 return m
259}
260
261// ExpectVar asserts that the next line in the input matches the pattern
262// <name>=<value> and returns <value>.
263func (s *Session) ExpectVar(name string) string {
264 if s.Failed() {
265 return ""
266 }
267 l, m, err := s.expectRE(name+"=(.*)", 1)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700268 s.log(err, "ExpectVar: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700269 if err != nil {
270 s.error(err)
271 return ""
272 }
273 if len(m) != 1 || len(m[0]) != 2 {
274 s.error(fmt.Errorf("failed to find value for %q in %q", name, l))
275 return ""
276 }
277 return m[0][1]
278}
279
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700280// ExpectSetRE verifies whether the supplied set of regular expression
281// parameters matches the next n (where n is the number of parameters)
282// lines of input. Each line is read and matched against the supplied
283// patterns in the order that they are supplied as parameters. Consequently
284// the set may contain repetitions if the same pattern is expected multiple
James Ring3c8316a2015-02-04 10:48:04 -0800285// times. The value returned is either:
James Ringc0264952015-02-14 09:55:32 -0800286// * nil in the case of an error, or
287// * nil if n lines are read or EOF is encountered before all expressions are
288// matched, or
James Ring3c8316a2015-02-04 10:48:04 -0800289// * an array of length len(expected), whose ith element contains the result
290// of FindStringSubmatch of expected[i] on the matching string (never
291// nil). If there are no capturing groups in expected[i], the return
292// value's [i][0] element will be the entire matching string
293func (s *Session) ExpectSetRE(expected ...string) [][]string {
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700294 if s.Failed() {
James Ring3c8316a2015-02-04 10:48:04 -0800295 return nil
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700296 }
James Ring3c8316a2015-02-04 10:48:04 -0800297 if match, err := s.expectSetRE(len(expected), expected...); err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800298 s.error(err)
James Ring3c8316a2015-02-04 10:48:04 -0800299 return nil
300 } else {
301 return match
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800302 }
303}
304
James Ringc0264952015-02-14 09:55:32 -0800305// ExpectSetEventuallyRE is like ExpectSetRE except that it reads as much
306// output as required rather than just the next n lines. The value returned is
307// either:
308// * nil in the case of an error, or
309// * nil if EOF is encountered before all expressions are matched, or
James Ring3c8316a2015-02-04 10:48:04 -0800310// * an array of length len(expected), whose ith element contains the result
311// of FindStringSubmatch of expected[i] on the matching string (never
312// nil). If there are no capturing groups in expected[i], the return
313// value's [i][0] will contain the entire matching string
James Ringc0264952015-02-14 09:55:32 -0800314// This function stops consuming output as soon as all regular expressions are
315// matched.
James Ring3c8316a2015-02-04 10:48:04 -0800316func (s *Session) ExpectSetEventuallyRE(expected ...string) [][]string {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800317 if s.Failed() {
James Ring3c8316a2015-02-04 10:48:04 -0800318 return nil
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800319 }
James Ring3c8316a2015-02-04 10:48:04 -0800320 if matches, err := s.expectSetRE(-1, expected...); err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800321 s.error(err)
James Ring3c8316a2015-02-04 10:48:04 -0800322 return nil
323 } else {
324 return matches
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800325 }
326}
327
328// expectSetRE will look for the expected set of patterns in the next
James Ringc0264952015-02-14 09:55:32 -0800329// numLines of output or in all remaining output. If all expressions are
330// matched, no more output is consumed.
James Ring3c8316a2015-02-04 10:48:04 -0800331func (s *Session) expectSetRE(numLines int, expected ...string) ([][]string, error) {
James Ringc0264952015-02-14 09:55:32 -0800332 matches := make([][]string, len(expected))
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700333 regexps := make([]*regexp.Regexp, len(expected))
334 for i, expRE := range expected {
335 re, err := regexp.Compile(expRE)
336 if err != nil {
James Ring3c8316a2015-02-04 10:48:04 -0800337 return nil, err
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700338 }
339 regexps[i] = re
340 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800341 i := 0
James Ringc0264952015-02-14 09:55:32 -0800342 matchCount := 0
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800343 for {
James Ringc0264952015-02-14 09:55:32 -0800344 if matchCount == len(expected) {
345 break
346 }
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700347 line, err := s.read(readLine)
348 line = strings.TrimRight(line, "\n")
349 s.log(err, "ExpectSetRE: %s", line)
350 if err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800351 if numLines >= 0 {
James Ring3c8316a2015-02-04 10:48:04 -0800352 return nil, err
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800353 }
354 break
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700355 }
James Ringc0264952015-02-14 09:55:32 -0800356
357 // Match the line against all regexp's and remove each regexp
358 // that matches.
359 for i, re := range regexps {
360 if re == nil {
361 continue
362 }
363 match := re.FindStringSubmatch(line)
364 if match != nil {
365 matchCount++
366 regexps[i] = nil
367 matches[i] = match
368 // Don't allow this line to be matched by more than one re.
369 break
370 }
371 }
372
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800373 i++
374 if numLines > 0 && i >= numLines {
375 break
376 }
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700377 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800378
James Ringc0264952015-02-14 09:55:32 -0800379 // It's an error if there are any unmatched regexps.
380 unmatchedRes := make([]string, 0)
381 for i, re := range regexps {
382 if re != nil {
383 unmatchedRes = append(unmatchedRes, expected[i])
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700384 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800385 }
James Ringc0264952015-02-14 09:55:32 -0800386 if len(unmatchedRes) > 0 {
387 return nil, fmt.Errorf("found no match for [%v]", strings.Join(unmatchedRes, ","))
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700388 }
James Ring3c8316a2015-02-04 10:48:04 -0800389 return matches, nil
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700390}
391
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700392// ReadLine reads the next line, if any, from the input stream. It will set
393// the error state to io.EOF if it has read past the end of the stream.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700394// ReadLine has no effect if an error has already occurred.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700395func (s *Session) ReadLine() string {
396 if s.Failed() {
397 return ""
398 }
399 l, err := s.read(readLine)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700400 s.log(err, "Readline: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700401 if err != nil {
402 s.error(err)
403 }
404 return strings.TrimRight(l, "\n")
405}
406
407// ReadAll reads all remaining input on the stream. Unlike all of the other
408// methods it does not strip newlines from the input.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700409// ReadAll has no effect if an error has already occurred.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700410func (s *Session) ReadAll() (string, error) {
411 if s.Failed() {
412 return "", s.err
413 }
414 return s.read(readAll)
415}
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700416
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700417func (s *Session) ExpectEOF() error {
418 if s.Failed() {
419 return s.err
420 }
421 buf := [1024]byte{}
422 n, err := s.input.Read(buf[:])
423 if n != 0 || err == nil {
424 s.error(fmt.Errorf("unexpected input %d bytes: %q", n, string(buf[:n])))
425 return s.err
426 }
427 if err != io.EOF {
428 s.error(err)
429 return s.err
430 }
431 return nil
432}
433
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700434// Finish reads all remaining input on the stream regardless of any
435// prior errors and writes it to the supplied io.Writer parameter if non-nil.
436// It returns both the data read and the prior error, if any, otherwise it
437// returns any error that occurred reading the rest of the input.
438func (s *Session) Finish(w io.Writer) (string, error) {
439 a, err := s.read(readAll)
440 if w != nil {
441 fmt.Fprint(w, a)
442 }
443 if s.Failed() {
444 return a, s.err
445 }
446 return a, err
447}