blob: f5d4664bda8581384108f771b2d9f1e565b0e904 [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
Todd Wang8c4e5cc2015-04-09 11:30:52 -07005// Package expect implements support for checking expectations against a
6// buffered input stream. It supports literal and pattern based matching. It is
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -07007// line oriented; all of the methods (expect ReadAll) strip trailing newlines
Todd Wang8c4e5cc2015-04-09 11:30:52 -07008// from their return values. It places a timeout on all its operations. It will
9// generally be used to read from the stdout stream of subprocesses in tests and
10// other situations and to make 'assertions' about what is to be read.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070011//
12// A Session type is used to store state, in particular error state, across
13// consecutive invocations of its method set. If a particular method call
14// encounters an error then subsequent calls on that Session will have no
15// effect. This allows for a series of assertions to be made, one per line,
16// and for errors to be checked at the end. In addition Session is designed
17// to be easily used with the testing package; passing a testing.T instance
18// to NewSession allows it to set errors directly and hence tests will pass or
19// fail according to whether the expect assertions are met or not.
20//
21// Care is taken to ensure that the file and line number of the first
22// failed assertion in the session are recorded in the error stored in
23// the Session.
24//
25// Examples
26//
27// func TestSomething(t *testing.T) {
28// buf := []byte{}
29// buffer := bytes.NewBuffer(buf)
30// buffer.WriteString("foo\n")
31// buffer.WriteString("bar\n")
32// buffer.WriteString("baz\n")
James Ring3c8316a2015-02-04 10:48:04 -080033// s := expect.NewSession(t, bufio.NewReader(buffer), time.Second)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070034// s.Expect("foo")
35// s.Expect("bars)
36// if got, want := s.ReadLine(), "baz"; got != want {
37// t.Errorf("got %v, want %v", got, want)
38// }
39// }
40//
41package expect
42
43import (
44 "bufio"
45 "errors"
46 "fmt"
47 "io"
48 "path/filepath"
49 "regexp"
50 "runtime"
51 "strings"
52 "time"
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -070053
Cosmos Nicolaoue3b19322015-06-18 16:05:08 -070054 "v.io/x/ref/internal/logger"
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070055)
56
57var (
58 Timeout = errors.New("timeout")
59)
60
61// Session represents the state of an expect session.
62type Session struct {
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070063 input *bufio.Reader
64 timeout time.Duration
65 t Testing
66 verbose bool
67 oerr, err error
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070068}
69
70type Testing interface {
71 Error(args ...interface{})
James Ring3c8316a2015-02-04 10:48:04 -080072 Errorf(format string, args ...interface{})
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -070073 Log(args ...interface{})
Cosmos Nicolaoue3b19322015-06-18 16:05:08 -070074 Logf(format string, args ...interface{})
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070075}
76
77// NewSession creates a new Session. The parameter t may be safely be nil.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070078func NewSession(t Testing, input io.Reader, timeout time.Duration) *Session {
79 return &Session{t: t, timeout: timeout, input: bufio.NewReader(input)}
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070080}
81
82// Failed returns true if an error has been encountered by a prior call.
83func (s *Session) Failed() bool {
84 return s.err != nil
85}
86
87// Error returns the error code (possibly nil) currently stored in the Session.
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070088// This will include the file and line of the calling function that experienced
89// the error. Use OriginalError to obtain the original error code.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -070090func (s *Session) Error() error {
91 return s.err
92}
93
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -070094// OriginalError returns any error code (possibly nil) returned by the
95// underlying library routines called.
96func (s *Session) OriginalError() error {
97 return s.oerr
98}
99
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700100// SetVerbosity enables/disable verbose debugging information, in particular,
101// every line of input read will be logged via Testing.Logf or, if it is nil,
102// to stderr.
103func (s *Session) SetVerbosity(v bool) {
104 s.verbose = v
105}
106
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700107func (s *Session) log(err error, format string, args ...interface{}) {
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700108 if !s.verbose {
109 return
110 }
111 _, path, line, _ := runtime.Caller(2)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700112 errstr := ""
113 if err != nil {
114 errstr = err.Error() + ": "
115 }
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700116 loc := fmt.Sprintf("%s:%d", filepath.Base(path), line)
117 o := strings.TrimRight(fmt.Sprintf(format, args...), "\n\t ")
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700118 if s.t == nil {
Cosmos Nicolaoue3b19322015-06-18 16:05:08 -0700119 logger.Global().Infof("%s: %s%s\n", loc, errstr, o)
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700120 return
121 }
Cosmos Nicolaoue3b19322015-06-18 16:05:08 -0700122 s.t.Logf("%s: %s%s", loc, errstr, o)
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700123 s.t.Log(loc, o)
124}
125
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700126// ReportError calls Testing.Error to report any error currently stored
127// in the Session.
128func (s *Session) ReportError() {
129 if s.err != nil && s.t != nil {
130 s.t.Error(s.err)
131 }
132}
133
134// error must always be called from a public function that is called
135// directly by an external user, otherwise the file:line info will
136// be incorrect.
137func (s *Session) error(err error) error {
138 _, file, line, _ := runtime.Caller(2)
Cosmos Nicolaou19a7cd42014-09-25 09:52:16 -0700139 s.oerr = err
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700140 s.err = fmt.Errorf("%s:%d: %s", filepath.Base(file), line, err)
141 s.ReportError()
142 return s.err
143}
144
145type reader func(r *bufio.Reader) (string, error)
146
147func readAll(r *bufio.Reader) (string, error) {
148 all := ""
149 for {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700150 l, err := r.ReadString('\n')
151 all += l
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700152 if err != nil {
153 if err == io.EOF {
154 return all, nil
155 }
156 return all, err
157 }
158 }
159}
160
161func readLine(r *bufio.Reader) (string, error) {
162 return r.ReadString('\n')
163}
164
165func (s *Session) read(f reader) (string, error) {
166 ch := make(chan string, 1)
167 ech := make(chan error, 1)
168 go func(fn reader, io *bufio.Reader) {
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700169 str, err := fn(io)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700170 if err != nil {
171 ech <- err
172 return
173 }
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700174 ch <- str
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700175 }(f, s.input)
176 select {
177 case err := <-ech:
178 return "", err
179 case m := <-ch:
180 return m, nil
181 case <-time.After(s.timeout):
182 return "", Timeout
183 }
184}
185
186// Expect asserts that the next line in the input matches the supplied string.
187func (s *Session) Expect(expected string) {
188 if s.Failed() {
189 return
190 }
191 line, err := s.read(readLine)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700192 s.log(err, "Expect: %s", line)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700193 if err != nil {
194 s.error(err)
195 return
196 }
197 line = strings.TrimRight(line, "\n")
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700198
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700199 if line != expected {
200 s.error(fmt.Errorf("got %q, want %q", line, expected))
201 }
202 return
203}
204
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700205// Expectf asserts that the next line in the input matches the result of
206// formatting the supplied arguments. It's equivalent to
207// Expect(fmt.Sprintf(args))
208func (s *Session) Expectf(format string, args ...interface{}) {
209 if s.Failed() {
210 return
211 }
212 line, err := s.read(readLine)
213 s.log(err, "Expect: %s", line)
214 if err != nil {
215 s.error(err)
216 return
217 }
218 line = strings.TrimRight(line, "\n")
219 expected := fmt.Sprintf(format, args...)
220 if line != expected {
221 s.error(fmt.Errorf("got %q, want %q", line, expected))
222 }
223 return
224}
225
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700226func (s *Session) expectRE(pattern string, n int) (string, [][]string, error) {
227 if s.Failed() {
228 return "", nil, s.err
229 }
230 re, err := regexp.Compile(pattern)
231 if err != nil {
232 return "", nil, err
233 }
234 line, err := s.read(readLine)
235 if err != nil {
236 return "", nil, err
237 }
238 line = strings.TrimRight(line, "\n")
239 return line, re.FindAllStringSubmatch(line, n), err
240}
241
242// ExpectRE asserts that the next line in the input matches the pattern using
James Ringc0264952015-02-14 09:55:32 -0800243// regexp.MustCompile(pattern).FindAllStringSubmatch(..., n).
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700244func (s *Session) ExpectRE(pattern string, n int) [][]string {
245 if s.Failed() {
246 return [][]string{}
247 }
248 l, m, err := s.expectRE(pattern, n)
Bogdan Caprita490a4512014-11-20 21:12:19 -0800249 s.log(err, "ExpectRE: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700250 if err != nil {
251 s.error(err)
252 return [][]string{}
253 }
254 if len(m) == 0 {
255 s.error(fmt.Errorf("%q found no match in %q", pattern, l))
256 }
257 return m
258}
259
260// ExpectVar asserts that the next line in the input matches the pattern
261// <name>=<value> and returns <value>.
262func (s *Session) ExpectVar(name string) string {
263 if s.Failed() {
264 return ""
265 }
266 l, m, err := s.expectRE(name+"=(.*)", 1)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700267 s.log(err, "ExpectVar: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700268 if err != nil {
269 s.error(err)
270 return ""
271 }
272 if len(m) != 1 || len(m[0]) != 2 {
273 s.error(fmt.Errorf("failed to find value for %q in %q", name, l))
274 return ""
275 }
276 return m[0][1]
277}
278
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700279// ExpectSetRE verifies whether the supplied set of regular expression
280// parameters matches the next n (where n is the number of parameters)
281// lines of input. Each line is read and matched against the supplied
282// patterns in the order that they are supplied as parameters. Consequently
283// the set may contain repetitions if the same pattern is expected multiple
James Ring3c8316a2015-02-04 10:48:04 -0800284// times. The value returned is either:
James Ringc0264952015-02-14 09:55:32 -0800285// * nil in the case of an error, or
286// * nil if n lines are read or EOF is encountered before all expressions are
287// matched, or
James Ring3c8316a2015-02-04 10:48:04 -0800288// * an array of length len(expected), whose ith element contains the result
289// of FindStringSubmatch of expected[i] on the matching string (never
290// nil). If there are no capturing groups in expected[i], the return
291// value's [i][0] element will be the entire matching string
292func (s *Session) ExpectSetRE(expected ...string) [][]string {
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700293 if s.Failed() {
James Ring3c8316a2015-02-04 10:48:04 -0800294 return nil
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700295 }
James Ring3c8316a2015-02-04 10:48:04 -0800296 if match, err := s.expectSetRE(len(expected), expected...); err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800297 s.error(err)
James Ring3c8316a2015-02-04 10:48:04 -0800298 return nil
299 } else {
300 return match
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800301 }
302}
303
James Ringc0264952015-02-14 09:55:32 -0800304// ExpectSetEventuallyRE is like ExpectSetRE except that it reads as much
305// output as required rather than just the next n lines. The value returned is
306// either:
307// * nil in the case of an error, or
308// * nil if EOF is encountered before all expressions are matched, or
James Ring3c8316a2015-02-04 10:48:04 -0800309// * an array of length len(expected), whose ith element contains the result
310// of FindStringSubmatch of expected[i] on the matching string (never
311// nil). If there are no capturing groups in expected[i], the return
312// value's [i][0] will contain the entire matching string
James Ringc0264952015-02-14 09:55:32 -0800313// This function stops consuming output as soon as all regular expressions are
314// matched.
James Ring3c8316a2015-02-04 10:48:04 -0800315func (s *Session) ExpectSetEventuallyRE(expected ...string) [][]string {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800316 if s.Failed() {
James Ring3c8316a2015-02-04 10:48:04 -0800317 return nil
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800318 }
James Ring3c8316a2015-02-04 10:48:04 -0800319 if matches, err := s.expectSetRE(-1, expected...); err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800320 s.error(err)
James Ring3c8316a2015-02-04 10:48:04 -0800321 return nil
322 } else {
323 return matches
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800324 }
325}
326
327// expectSetRE will look for the expected set of patterns in the next
James Ringc0264952015-02-14 09:55:32 -0800328// numLines of output or in all remaining output. If all expressions are
329// matched, no more output is consumed.
James Ring3c8316a2015-02-04 10:48:04 -0800330func (s *Session) expectSetRE(numLines int, expected ...string) ([][]string, error) {
James Ringc0264952015-02-14 09:55:32 -0800331 matches := make([][]string, len(expected))
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700332 regexps := make([]*regexp.Regexp, len(expected))
333 for i, expRE := range expected {
334 re, err := regexp.Compile(expRE)
335 if err != nil {
James Ring3c8316a2015-02-04 10:48:04 -0800336 return nil, err
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700337 }
338 regexps[i] = re
339 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800340 i := 0
James Ringc0264952015-02-14 09:55:32 -0800341 matchCount := 0
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800342 for {
James Ringc0264952015-02-14 09:55:32 -0800343 if matchCount == len(expected) {
344 break
345 }
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700346 line, err := s.read(readLine)
347 line = strings.TrimRight(line, "\n")
348 s.log(err, "ExpectSetRE: %s", line)
349 if err != nil {
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800350 if numLines >= 0 {
James Ring3c8316a2015-02-04 10:48:04 -0800351 return nil, err
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800352 }
353 break
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700354 }
James Ringc0264952015-02-14 09:55:32 -0800355
356 // Match the line against all regexp's and remove each regexp
357 // that matches.
358 for i, re := range regexps {
359 if re == nil {
360 continue
361 }
362 match := re.FindStringSubmatch(line)
363 if match != nil {
364 matchCount++
365 regexps[i] = nil
366 matches[i] = match
367 // Don't allow this line to be matched by more than one re.
368 break
369 }
370 }
371
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800372 i++
373 if numLines > 0 && i >= numLines {
374 break
375 }
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700376 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800377
James Ringc0264952015-02-14 09:55:32 -0800378 // It's an error if there are any unmatched regexps.
379 unmatchedRes := make([]string, 0)
380 for i, re := range regexps {
381 if re != nil {
382 unmatchedRes = append(unmatchedRes, expected[i])
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700383 }
Cosmos Nicolaoua429eff2014-11-19 14:02:34 -0800384 }
James Ringc0264952015-02-14 09:55:32 -0800385 if len(unmatchedRes) > 0 {
386 return nil, fmt.Errorf("found no match for [%v]", strings.Join(unmatchedRes, ","))
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700387 }
James Ring3c8316a2015-02-04 10:48:04 -0800388 return matches, nil
Cosmos Nicolaou5cda14f2014-10-27 09:58:20 -0700389}
390
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700391// ReadLine reads the next line, if any, from the input stream. It will set
392// the error state to io.EOF if it has read past the end of the stream.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700393// ReadLine has no effect if an error has already occurred.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700394func (s *Session) ReadLine() string {
395 if s.Failed() {
396 return ""
397 }
398 l, err := s.read(readLine)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700399 s.log(err, "Readline: %s", l)
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700400 if err != nil {
401 s.error(err)
402 }
403 return strings.TrimRight(l, "\n")
404}
405
406// ReadAll reads all remaining input on the stream. Unlike all of the other
407// methods it does not strip newlines from the input.
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700408// ReadAll has no effect if an error has already occurred.
Cosmos Nicolaoue925f2a2014-09-05 14:47:18 -0700409func (s *Session) ReadAll() (string, error) {
410 if s.Failed() {
411 return "", s.err
412 }
413 return s.read(readAll)
414}
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700415
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700416func (s *Session) ExpectEOF() error {
417 if s.Failed() {
418 return s.err
419 }
420 buf := [1024]byte{}
421 n, err := s.input.Read(buf[:])
422 if n != 0 || err == nil {
423 s.error(fmt.Errorf("unexpected input %d bytes: %q", n, string(buf[:n])))
424 return s.err
425 }
426 if err != io.EOF {
427 s.error(err)
428 return s.err
429 }
430 return nil
431}
432
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700433// Finish reads all remaining input on the stream regardless of any
434// prior errors and writes it to the supplied io.Writer parameter if non-nil.
435// It returns both the data read and the prior error, if any, otherwise it
436// returns any error that occurred reading the rest of the input.
437func (s *Session) Finish(w io.Writer) (string, error) {
438 a, err := s.read(readAll)
439 if w != nil {
440 fmt.Fprint(w, a)
441 }
442 if s.Failed() {
443 return a, s.err
444 }
445 return a, err
446}