blob: e82df22b9b9e80b33bd69179924ef48a1a78b481 [file] [edit]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package expect
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"testing"
"time"
stdlog "log"
"google.golang.org/grpc/codes"
log "github.com/golang/glog"
term "github.com/google/goterm/term"
"golang.org/x/crypto/ssh"
)
var (
// eReg identifies the expect commands in the shell files.
eReg = regexp.MustCompile(`^# e: (.*)$`)
// sReg identifies the send commands in the shell files.
sReg = regexp.MustCompile(`^# s: (.*)$`)
// sReg identifies the sw/case commands in the shell files.
cReg = regexp.MustCompile(`^# c: '(.*)' (.*)$`)
)
const expTestData = "./testdata/"
var privateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAnUKOpnWUyxCGy9lyTLK3Crd5BXGG718wIgHzpCvKbopVbYu4
xa9Fk1cjPHp2F/pPwXmIKJuTVpkm/VXcFfABH6cMeszaXqVBhqm6AA0E7y0K0oYZ
GUMMm3sBLPV3ydUHECI2NnEXOCLGysKM6Ht2ZuGxCKdXpquRE1HdLUUIJep31gSO
J4dyQRk12VYHrpTjz1Tzv9prf76vJqYmr6+axeH7I3/9KGnPe1vD0z8NhOwqQONz
DMSjpbSYFiVyRbDVgSi9xq+BeFrASZuwkoHut3tzmIvQLcGIR+LoOeN9mDpXbseQ
y84PXkduF8udrBCIBETekmK7kqi6hwLRCn9/twIDAQABAoIBAQCTeEOvQ5oJpvDR
HpN56ymNCiqZ+TERLhFEAtKIRGxrppufw6O89bToC5HGeAxgReIey6nscqADWFFg
xfBCPjO/i/Y+/fVVReEht+3teEgFRhbc/tVwhBjBgOLEV1hC09rwvTRbb0fX43zJ
zRE4Pfb1WXWbaNngOQkttdoURqTyb+n8zgwx0AUsueSrYxTk1UTF+Jet7g23jRjd
YCCx9qhHez5yif+1LZGIqJD0OKGHr9q+bbOZpy5dqjamuf9ulBvnZkKzcuHf0m/W
Vhf9YI8kOQhPXfztTnZrN5Jg64gGuvJ0sEZucp5hOR4hYkLagOCaUuNIeHG7SsYU
hChWCDphAoGBAM7nr4t714etJnuRE39FG+rylV5K3T2osr2Hwp7wSZfXHZUcnk2N
KSDA9tzeFYX7QxUlc7qNwsLC2WkK97x1WbrNdO8Zn5lmBHfyIS7jxEGQY9htWrgr
sjAaUg/JfHMu/lxNAigXAaGU2VzsTySgB3eWbfaAd/sImaxnVPBajOARAoGBAMKT
NykNYl3zg1pIXGQlu3a0pTv2gRcBnnW7bUAM6b6tDdQZ+5cbu43MfAwhsGsMZ/HL
gKQRJI952olrPa4dEiirxUKqfVVPLnDcUSu6uhvJFpzN2YVdMyPNWc3V9lfIztbu
UvCvupnmeViG9qRoJgbMLIBLBN1oS5MKOL55oqtHAoGBAMX21XZe4qRVHlniQEZo
aELPIe1bMf3Z2FMRfzw1aiSW1R4jiK9o3a4SEuDWuL893jxwXh9jnbJdXklsDgbK
PTVHeZd/672I58Of7vH/SXr13SJp1wAaBt6RgGzMen92uja0E9kp0gy475RCIaNI
XnykeMf+uU1+OBLFt3ZVHS8RAoGBAJr/BpvPK6LHzsTmi6LDY/gFovKHRQH8qiwC
595z6ueXl0J0iDQxRVCJqe9IDu7XbR3yDEGl3kfku69oHDRMuCBp5LNceIaykr4Y
4xhAoOxtXXP/jt1sBsboWDddz+TR8+LG6o8MjUr3i4Z3zJXe2RvlHTX9jJyK7ljt
dZJV9r0VAoGBAI+yBh2oa5D66XztQ2pfKm/B03RYARR0iKvho6Ass1nM1YSvhflt
CkGNYNP5Mwr/VpfTppl0JTl9++gtoAmDgVm19JUYiABhYNmbTq62STJb+LkEL+xK
Jf5UkUJOE8Rf+A1vmI1igjVffSIRLTJC6zOX0JCZMIFKZhyTZsPOuFcm
-----END RSA PRIVATE KEY-----`)
// SSHServer represents one local SSH server.
type SSHServer struct {
batch []Batcher
cfg *ssh.ServerConfig
l net.Listener
sync.Mutex
opts []Option
term *term.Termios
}
// New returns a new SSHServer configured for username/password.
func NewSSHServer(user, pass string, b []Batcher, t *term.Termios, opts ...Option) *SSHServer {
srv := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
if c.User() == user && string(password) == pass {
return nil, nil
}
return nil, errors.New("password rejected for:" + c.User())
},
}
k, _ := ssh.ParsePrivateKey(privateKey)
srv.AddHostKey(k)
if t == nil {
t = &term.Termios{
Wz: term.Winsize{
WsCol: 132,
WsRow: 43,
},
}
}
return &SSHServer{cfg: srv, batch: b, term: t, opts: opts}
}
// Batcher replaces the batcher.
func (s *SSHServer) Batcher(b []Batcher) {
s.Lock()
s.batch = b
s.Unlock()
}
// Termios replaces the termios.
func (s *SSHServer) Termios(t *term.Termios) {
if t == nil {
t = &term.Termios{
Wz: term.Winsize{
WsCol: 132,
WsRow: 43,
},
}
}
s.term = t
}
// Serve spins up the SSH server and returns the port used.
func (s *SSHServer) Serve() (uint16, error) {
l, err := net.Listen("tcp", "")
if err != nil {
return 0, err
}
s.l = l
go func() {
for {
c, err := l.Accept()
if err != nil {
log.Errorf("Accept failed: %v", err)
return
}
go s.runBatch(c)
}
}()
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
return 0, err
}
p, err := strconv.Atoi(port)
if err != nil {
return 0, err
}
return uint16(p), nil
}
// Close closes the SSH server write pipe.
func (s *SSHServer) Close() error {
return s.l.Close()
}
const testTimeout = 20 * time.Second
// RFC 4254 Section 6.2.
type ptyRequestMsg struct {
Term string
Columns uint32
Rows uint32
Width uint32
Height uint32
Modelist string
}
func (s *SSHServer) runBatch(conn net.Conn) {
defer conn.Close()
_, chs, rq, err := ssh.NewServerConn(conn, s.cfg)
if err != nil {
log.Errorf("ssh.NewServerConn failed: %v", err)
return
}
go ssh.DiscardRequests(rq)
for ch := range chs {
switch ch.ChannelType() {
case "session":
sch, in, err := ch.Accept()
if err != nil {
log.Errorf("ch.Accept failed: %v", err)
return
}
for sess := range in {
switch sess.Type {
case "dummy":
if err := sess.Reply(true, nil); err != nil {
log.Errorf("sess.Reply(%t,nil) failed: %v", true, err)
}
case "pty-req":
ptyReq := ptyRequestMsg{}
if err := ssh.Unmarshal(sess.Payload, &ptyReq); err != nil {
if err := sess.Reply(false, nil); err != nil {
log.Errorf("sess.Reply(%t,nil) failed: %v", false, err)
}
log.Errorf("ssh.Unmarshal of PTYRequest failed: %v", err)
continue
}
if ptyReq.Columns != uint32(s.term.Wz.WsCol) || ptyReq.Rows != uint32(s.term.Wz.WsRow) {
log.Errorf("PTY cols/rows: %d/%d want: %d/%d", ptyReq.Columns, ptyReq.Rows, s.term.Wz.WsCol, s.term.Wz.WsRow)
if err := sess.Reply(false, nil); err != nil {
log.Errorf("sess.Reply(%t,nil) failed: %v", false, err)
}
continue
}
if err := sess.Reply(true, nil); err != nil {
log.Errorf("sess.Reply(%t,nil) failed: %v", true, err)
}
case "shell":
log.Infof("Shell request coming in")
resCh := make(chan error)
defer close(resCh)
if err := sess.Reply(true, nil); err != nil {
log.Errorf("sess.Reply(%t,nil) failed: %v", true, err)
}
rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
go io.Copy(sch, rIn)
go io.Copy(wOut, sch)
go func() {
exp, _, err := SpawnGeneric(&GenOptions{
In: wIn,
Out: rOut,
Wait: func() error {
return <-resCh
},
Close: func() error { return wIn.Close() },
Check: func() bool { return true },
}, testTimeout*2, s.opts...)
s.Lock()
out, err := exp.ExpectBatch(s.batch, testTimeout*2)
if err != nil {
log.Errorf("exp.ExpectBatch(%v) failed: %v, res: %v", s.batch, err, out)
}
s.Unlock()
}()
default:
sess.Reply(false, []byte(fmt.Sprint("session type not supported")))
}
}
default:
ch.Reject(ssh.UnknownChannelType, "channel type not supported")
}
}
}
// Tc is an example of implementing custom tag functions.
type Tc uint32
func NewTc() (t Tc) {
return
}
// Count works like ContinueLog with a counter.
func (t Tc) Count(msg string, s *Status) func() (Tag, *Status) {
return func() (Tag, *Status) {
t++
log.Infof("%d: %s", t, msg)
return ContinueTag, s
}
}
// NextLog adds loggin and counting to the Next tag.
func (t Tc) NextLog(msg string) func() (Tag, *Status) {
return func() (Tag, *Status) {
t++
log.Infof("Next %d: %s", t, msg)
return NextTag, NewStatus(codes.Unimplemented, "Should not matter")
}
}
func TestBatcher(t *testing.T) {
tests := []struct {
name string
clt, srv []Batcher
fail bool
}{
{
name: "Config mode",
clt: []Batcher{
&BExp{`router1>`},
&BSnd{"conf t\n"},
&BExp{`\(configure\) router1>`},
},
srv: []Batcher{
&BSnd{`router1> `},
&BExp{"conf t\n"},
&BSnd{`(configure) router1> `},
}}, {
name: "Login caser",
clt: []Batcher{
&BCas{[]Caser{
&Case{R: regexp.MustCompile(`Login: `), S: "TestUser\n", T: LogContinue("at login prompt", NewStatus(codes.PermissionDenied, "wrong username")), Rt: 1},
&Case{R: regexp.MustCompile(`Password: `), S: "TestPass\n", T: LogContinue("at password prompt", NewStatus(codes.PermissionDenied, "wrong pass")), Rt: 1},
&Case{R: regexp.MustCompile(`Permission denied`), T: Fail(NewStatus(codes.PermissionDenied, "login failed"))},
&Case{R: regexp.MustCompile(`router 1>`), T: OK()},
}},
},
srv: []Batcher{
&BSnd{"Login: "},
&BCas{[]Caser{
&Case{R: regexp.MustCompile("TestUser\n"), S: `Password: `, T: Continue(NewStatus(codes.PermissionDenied, "permission denied")), Rt: 3},
&Case{R: regexp.MustCompile("TestPass\n"), S: `router 1> `, T: OK()},
},
},
}}, {
name: "100 Hello World",
clt: []Batcher{
&BSnd{`Hello `},
&BCas{[]Caser{
&Case{R: regexp.MustCompile("Done"), T: OK()},
&Case{R: regexp.MustCompile("World"), S: "Hello ", T: NewTc().Count("Hello", NewStatus(codes.OutOfRange, "too many worlds")), Rt: 100},
}},
},
srv: []Batcher{
&BCas{[]Caser{
&Case{R: regexp.MustCompile("Hello"), S: "World\n", T: NewTc().Count("World", NewStatus(codes.OK, "too many hellos")), Rt: 99},
}},
&BSnd{"Done"},
},
}, {
name: "100 Hello World using Next tag",
clt: []Batcher{
&BSnd{`Hello `},
&BCas{[]Caser{
&Case{R: regexp.MustCompile("Done"), T: OK()},
&Case{R: regexp.MustCompile("World"), S: "Hello ", T: NewTc().Count("Hello", NewStatus(codes.OutOfRange, "too many worlds")), Rt: 100},
}},
},
srv: []Batcher{
&BCas{[]Caser{
&Case{R: regexp.MustCompile("Hello"), S: "World\n", T: NewTc().NextLog("World"), Rt: 99},
&Case{R: regexp.MustCompile("Hello"), S: "Done\n", T: LogContinue("Done sent", NewStatus(codes.OK, "Done sent"))},
}},
},
},
}
srv := NewSSHServer("test", "test", nil, nil, CheckDuration(40*time.Millisecond))
port, err := srv.Serve()
if err != nil {
t.Fatalf("srv.Serve failed: %v", err)
}
defer srv.Close()
for _, tst := range tests {
srv.Batcher(tst.srv)
clt, err := ssh.Dial("tcp", net.JoinHostPort("localhost", strconv.Itoa(int(port))),
&ssh.ClientConfig{
User: "test",
Auth: []ssh.AuthMethod{ssh.Password("test")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
t.Errorf("%s: ssh.Dial failed: %v", tst.name, err)
continue
}
e, _, err := SpawnSSH(clt, testTimeout*2)
if err != nil {
t.Errorf("%s: SpawnSSH failed: %v", tst.name, err)
}
res, err := e.ExpectBatch(tst.clt, testTimeout*2)
if got, want := err != nil, tst.fail; got != want {
t.Errorf("%s: e.ExpectBatch(%v,_) = %v want: %v, res: %q", tst.name, tst.clt, err, want, res)
}
if err := clt.Close(); err != nil {
t.Errorf("%s: clt.Close failed: %v", tst.name, err)
}
}
}
var (
cliMap = map[string]string{
"show system uptime": `Current time: 1998-10-13 19:45:47 UTC
Time Source: NTP CLOCK
System booted: 1998-10-12 20:51:41 UTC (22:54:06 ago)
Protocols started: 1998-10-13 19:33:45 UTC (00:12:02 ago)
Last configured: 1998-10-13 19:33:45 UTC (00:12:02 ago) by abc
12:45PM up 22:54, 2 users, load averages: 0.07, 0.02, 0.01
testuser@testrouter# `,
"show version": `Cisco IOS Software, 3600 Software (C3660-I-M), Version 12.3(4)T
TAC Support: http://www.cisco.com/tac
Copyright (c) 1986-2003 by Cisco Systems, Inc.
Compiled Thu 18-Sep-03 15:37 by ccai
ROM: System Bootstrap, Version 12.0(6r)T, RELEASE SOFTWARE (fc1)
ROM:
C3660-1 uptime is 1 week, 3 days, 6 hours, 41 minutes
System returned to ROM by power-on
System image file is "slot0:tftpboot/c3660-i-mz.123-4.T"
Cisco 3660 (R527x) processor (revision 1.0) with 57344K/8192K bytes of memory.
Processor board ID JAB055180FF
R527x CPU at 225Mhz, Implementation 40, Rev 10.0, 2048KB L2 Cache
3660 Chassis type: ENTERPRISE
2 FastEthernet interfaces
4 Serial interfaces
DRAM configuration is 64 bits wide with parity disabled.
125K bytes of NVRAM.
16384K bytes of processor board System flash (Read/Write)
Flash card inserted. Reading filesystem...done.
20480K bytes of processor board PCMCIA Slot0 flash (Read/Write)
Configuration register is 0x2102
testrouter# `,
"show system users": `7:30PM up 4 days, 2:26, 2 users, load averages: 0.07, 0.02, 0.01
USER TTY FROM LOGIN@ IDLE WHAT
root d0 - Fri05PM 4days -csh (csh)
blue p0 level5.company.net 7:30PM - cli
testuser@testrouter# `,
}
)
func fakeCli(tMap map[string]string, in io.Reader, out io.Writer) {
scn := bufio.NewScanner(in)
for scn.Scan() {
tst, ok := tMap[scn.Text()]
if !ok {
out.Write([]byte(fmt.Sprintf("command: %q not found", scn.Text())))
continue
}
_, err := out.Write([]byte(tst))
if err != nil {
log.Warningf("Write of %q failed: %v", tst, err)
return
}
}
}
// ExampleDebugCheck toggles the DebugCheck option.
func ExampleDebugCheck() {
rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
rLog, wLog := io.Pipe()
waitCh := make(chan error)
defer rIn.Close()
defer wOut.Close()
defer wLog.Close()
go fakeCli(cliMap, rIn, wOut)
exp, r, err := SpawnGeneric(&GenOptions{
In: wIn,
Out: rOut,
Wait: func() error { return <-waitCh },
Close: func() error { return wIn.Close() },
Check: func() bool {
return true
}}, -1)
if err != nil {
log.Errorf("SpawnGeneric failed: %v", err)
return
}
re := regexp.MustCompile("testrouter# ")
interact := func() {
for cmd := range cliMap {
if err := exp.Send(cmd + "\n"); err != nil {
log.Errorf("exp.Send(%q) failed: %v\n", cmd+"\n", err)
return
}
out, _, err := exp.Expect(re, -1)
if err != nil {
log.Errorf("exp.Expect(%v) failed: %v out: %v", re, err, out)
return
}
}
}
go func() {
var last string
scn := bufio.NewScanner(rLog)
for scn.Scan() {
ws := strings.Split(scn.Text(), " ")
if ws[0] == last {
continue
}
last = ws[0]
fmt.Println(ws[0])
}
}()
fmt.Println("First round")
interact()
fmt.Println("Second round - Debugging enabled")
prev := exp.Options(DebugCheck(stdlog.New(wLog, "DebugExample ", 0)))
interact()
exp.Options(prev)
fmt.Println("Last round - Previous Check put back")
interact()
waitCh <- nil
exp.Close()
wOut.Close()
<-r
// Output:
// First round
// Second round - Debugging enabled
// DebugExample
// Last round - Previous Check put back
}
// ExampleChangeCheck changes the check function runtime for an Expect session.
func ExampleChangeCheck() {
rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
waitCh := make(chan error)
outCh := make(chan string)
defer close(outCh)
go fakeCli(cliMap, rIn, wOut)
go func() {
var last string
for s := range outCh {
if s == last {
continue
}
fmt.Println(s)
last = s
}
}()
exp, r, err := SpawnGeneric(&GenOptions{
In: wIn,
Out: rOut,
Wait: func() error { return <-waitCh },
Close: func() error { return wIn.Close() },
Check: func() bool {
outCh <- "Original check"
return true
}}, -1)
if err != nil {
fmt.Printf("SpawnGeneric failed: %v\n", err)
return
}
re := regexp.MustCompile("testrouter# ")
interact := func() {
for cmd := range cliMap {
if err := exp.Send(cmd + "\n"); err != nil {
fmt.Printf("exp.Send(%q) failed: %v\n", cmd+"\n", err)
return
}
out, _, err := exp.Expect(re, -1)
if err != nil {
fmt.Printf("exp.Expect(%v) failed: %v out: %v", re, err, out)
return
}
}
}
interact()
prev := exp.Options(ChangeCheck(func() bool {
outCh <- "Replaced check"
return true
}))
interact()
exp.Options(prev)
interact()
waitCh <- nil
exp.Close()
wOut.Close()
<-r
// Output:
// Original check
// Replaced check
// Original check
}
// TestSpawnGeneric tests out the generic spawn function.
func TestSpawnGeneric(t *testing.T) {
fr, fw := io.Pipe()
tests := []struct {
name string
opt *GenOptions
check func() bool
cli map[string]string
re *regexp.Regexp
fail bool
}{{
name: "Clean test",
check: func() bool { return true },
cli: cliMap,
re: regexp.MustCompile("testrouter# "),
fail: false,
}, {
name: "Fail check",
check: func() bool {
return false
},
cli: cliMap,
re: regexp.MustCompile("testrouter# "),
fail: true,
}, {
name: "In nil",
opt: &GenOptions{},
fail: true,
}, {
name: "Out nil",
opt: &GenOptions{
In: fw,
},
fail: true,
}, {
name: "Wait nil",
opt: &GenOptions{
In: fw,
Out: fr,
},
fail: true,
}}
for _, tst := range tests {
t.Logf("Running test: %v", tst.name)
waitCh := make(chan error)
rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
if tst.opt == nil {
tst.opt = &GenOptions{
In: wIn,
Out: rOut,
Wait: func() error { return <-waitCh },
Close: func() error { return wIn.Close() },
Check: tst.check}
}
go fakeCli(tst.cli, rIn, wOut)
exp, r, err := SpawnGeneric(tst.opt, -1)
if err != nil {
if !tst.fail {
t.Errorf("test: %v , SpawnGeneric failed: %v", tst.name, err)
}
continue
}
gotFail := false
for cmd := range tst.cli {
err := exp.Send(cmd + "\n")
if err != nil {
if tst.fail {
gotFail = true
break
}
t.Errorf("Send(%q) failed: %v", cmd, err)
break
}
out, _, err := exp.Expect(tst.re, -1)
if err != nil {
if tst.fail {
gotFail = true
break
}
t.Errorf("Expect(%q) failed: %v, out: %q", tst.re, err, out)
break
}
}
if gotFail != tst.fail {
t.Errorf("test: %v , failed status mismatch, got: %v want: %v", tst.name, gotFail, tst.fail)
}
waitCh <- nil
exp.Close()
wOut.Close()
<-r
}
}
// TestSpawnSSHPTY tests the SSHPTY spawner.
func TestSpawnSSHPTY(t *testing.T) {
tests := []struct {
name string
fail bool
srv []Batcher
clt []Batcher
sshNil bool
srvTerm *term.Termios
cltTerm term.Termios
}{{
name: "sshClient broken",
fail: true,
sshNil: true,
}, {
name: "Empty Termios",
clt: []Batcher{
&BSnd{"Hello"},
&BExp{"World"},
},
srv: []Batcher{
&BExp{"Hello"},
&BSnd{"World"},
},
}, {
name: "Termios mismatch",
fail: true,
srvTerm: &term.Termios{
Wz: term.Winsize{
WsCol: 120,
WsRow: 40,
}},
cltTerm: term.Termios{
Wz: term.Winsize{
WsCol: 240,
WsRow: 22,
},
},
}}
srv := NewSSHServer("test", "test", nil, nil)
port, err := srv.Serve()
if err != nil {
t.Fatalf("srv.Serve failed: %v", err)
}
defer srv.Close()
for _, tst := range tests {
srv.Batcher(tst.srv)
srv.Termios(tst.srvTerm)
var sshClt *ssh.Client
if !tst.sshNil {
clt, err := ssh.Dial("tcp", net.JoinHostPort("localhost", strconv.Itoa(int(port))),
&ssh.ClientConfig{
User: "test",
Auth: []ssh.AuthMethod{ssh.Password("test")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
t.Errorf("%s: net.Dial(%q) failed: %v", tst.name, net.JoinHostPort("localhost", strconv.Itoa(int(port))), err)
continue
}
sshClt = clt
}
e, _, err := SpawnSSHPTY(sshClt, testTimeout*2, tst.cltTerm)
if got, want := err != nil, tst.fail; got != want {
t.Errorf("%s: SpawnSSH = %t want: %t, err: %v", tst.name, got, want, err)
continue
}
if err != nil {
continue
}
res, err := e.ExpectBatch(tst.clt, testTimeout*2)
if err != nil {
t.Errorf("%s: e.ExpectBatch failed: %v, out: %v", tst.name, err, res)
continue
}
if err := sshClt.Close(); err != nil {
t.Errorf("%s: clt.Close failed: %v", tst.name, err)
}
}
}
// TestOptions tests manipulating options.
func TestOptions(t *testing.T) {
tests := []struct {
name string
check func() bool
opts []Option
re *regexp.Regexp
fail bool
}{{
name: "No options",
check: func() bool { return true },
}, {
name: "No check option",
opts: []Option{NoCheck()},
check: func() bool { return false },
},
}
for _, tst := range tests {
rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
go fakeCli(cliMap, rIn, wOut)
waitCh := make(chan error)
exp, r, err := SpawnGeneric(&GenOptions{
In: wIn,
Out: rOut,
Wait: func() error { return <-waitCh },
Close: func() error { return wIn.Close() },
Check: tst.check}, -1, tst.opts...)
if err != nil {
t.Errorf("%s: SpawnGeneric failed: %v", tst.name, err)
continue
}
if got, want := exp.Send("\n\n") != nil, tst.fail; got != want {
t.Errorf("%s: exp.Send(\"\\n\\n\") = %t want: %t", tst.name, got, want)
}
waitCh <- nil
exp.Close()
wOut.Close()
<-r
}
}
// TestSpawn tests out the Spawn function
func TestSpawn(t *testing.T) {
tests := []struct {
name string
fail bool
cmd string
cmdErr bool
}{{
name: "Spawn non executable fail",
fail: true,
cmd: "/etc/hosts",
}, {
name: "Nil return code",
cmd: "/bin/true",
}, {
name: "Non nil return code",
cmd: "/bin/false",
cmdErr: true,
}, {
name: "Spawn cat",
cmd: "/bin/cat",
cmdErr: true,
}}
for _, tst := range tests {
e, errCh, err := Spawn(tst.cmd, 8*time.Second)
if got, want := err != nil, tst.fail; got != want {
t.Errorf("%s: Spawn(%q) = %t want: %t, err: %v", tst.name, tst.cmd, got, want, err)
continue
}
if err != nil {
continue
}
<-time.After(2 * time.Second)
if err := e.Close(); err != nil {
t.Logf("e.Close failed: %v", err)
}
res := <-errCh
if got, want := res != nil, tst.cmdErr; got != want {
t.Errorf("%s: errCh = %t want: %t, err: %v", tst.name, got, want, err)
continue
}
}
}
// TestExpect tests the Expect function.
func TestExpect(t *testing.T) {
tests := []struct {
name string
fail bool
srv []Batcher
timeout time.Duration
re *regexp.Regexp
}{{
name: "Match prompt",
srv: []Batcher{
&BSnd{`
Pretty please don't hack my chassis
router1> `},
},
re: regexp.MustCompile("router1>"),
timeout: 2 * time.Second,
}, {
name: "Match fail",
fail: true,
re: regexp.MustCompile("router1>"),
srv: []Batcher{
&BSnd{`
Welcome
Router42>`},
},
timeout: 1 * time.Second,
}}
for _, tst := range tests {
exp, _, err := SpawnFake(tst.srv, tst.timeout)
if err != nil {
if !tst.fail {
t.Errorf("%s: SpawnFake failed: %v", tst.name, err)
}
continue
}
out, _, err := exp.Expect(tst.re, tst.timeout)
if got, want := err != nil, tst.fail; got != want {
t.Errorf("%s: Expect(%q,%v) = %t want: %t , err: %v, out: %q", tst.name, tst.re.String(), tst.timeout, got, want, err, out)
continue
}
}
}
// TestScenarios reads and executes the expect/*.sh test scenarios.
func TestScenarios(t *testing.T) {
//path := runfiles.Path(expTestData)
files, err := filepath.Glob(expTestData + "/*.sh")
if err != nil || len(files) == 0 {
t.Fatalf("filepath.Glob(%q) failed: %v, not testfile found", expTestData+"/*.sh", err)
}
L1:
for _, f := range files {
_, file := filepath.Split(f)
tst, err := buildTest(file)
if err != nil {
t.Errorf("%s: buildTest(%q) failed: %v", file, file, err)
continue
}
// Spawn the testfile
exp, r, err := Spawn(f, 0)
if err != nil {
t.Errorf("%s: Spawn(%q,0) failed: %v", file, file, err)
continue
}
t.Log("Testing scenariofile:", file)
for _, ts := range tst {
switch ts.Cmd() {
case BatchExpect:
re := regexp.MustCompile(ts.Arg())
to := ts.Timeout()
if to == 0 {
to = 30 * time.Second
}
o, _, err := exp.Expect(re, to)
if err != nil {
t.Errorf("%s: Expect(%q,%v) failed: %v, out: %q", file, ts.Arg, to, err, o)
continue L1
}
t.Log("Scenario:", file, "expect:", ts.Arg(), " found")
if !re.MatchString(o) {
t.Fatalf("%s: Doublecheck failed re: %q output: %q", file, ts.Arg(), o)
continue L1
}
case BatchSend:
if err := exp.Send(ts.Arg()); err != nil {
t.Fatalf("%s: Send(%q) failed: %v", file, ts.Arg(), err)
continue L1
}
case BatchSwitchCase:
to := ts.Timeout()
if to == 0 {
to = 30 * time.Second
}
o, _, _, err := exp.ExpectSwitchCase(ts.Cases(), to)
if err != nil {
if err.Error() == "process not running" {
t.Logf("%s: exp.ExpectSwitchCase(%v,%v) failed: %v, process returned: %v", file, ts.Cases(), to, err, <-r)
}
t.Errorf("%s: ExpectSwitchCase failed: %v case: %v output: %q", file, err, ts.Cases(), o)
continue L1
}
}
}
exp.Close()
}
}
// TestBatchScenarios runs through the scenarios again , this time as Batchjobs.
func TestBatchScenarios(t *testing.T) {
//path := runfiles.Path(expTestData)
files, err := filepath.Glob(expTestData + "/*.sh")
if err != nil || len(files) == 0 {
t.Fatalf("filepath.Glob(%q) failed: %v, not testfile found", expTestData+"/*.sh", err)
}
for _, f := range files {
_, file := filepath.Split(f)
tsts, err := buildTest(file)
if err != nil {
t.Errorf("%s: buildTest(%q) failed: %v", f, f, err)
continue
}
batch := []Batcher{}
for _, tst := range tsts {
switch tst.Cmd() {
case BatchExpect:
batch = append(batch, &BExp{tst.Arg()})
case BatchSend:
batch = append(batch, &BSnd{tst.Arg()})
case BatchSwitchCase:
batch = append(batch, &BCas{tst.Cases()})
}
}
exp, r, err := Spawn(f, 30*time.Second)
if err != nil {
t.Errorf("%s: Spawn(%q) failed: %v", file, file, err)
continue
}
res, err := exp.ExpectBatch(batch, 30*time.Second)
if err != nil {
t.Errorf("%s: ExpectBatch failed: %v, res: %v", file, err, res)
continue
}
exp.Close()
<-r
}
}
var tMap map[string][]Batcher
// buildTest Reads the sends and expected outputs from the testfiles eg.
func buildTest(fstring string) ([]Batcher, error) {
if tMap == nil {
tMap = make(map[string][]Batcher)
}
if tst, ok := tMap[fstring]; ok {
return tst, nil
}
//path := runfiles.Path(expTestData)
f, err := os.Open(expTestData + "/" + fstring)
if err != nil {
return []Batcher{}, err
}
defer f.Close()
scn := bufio.NewScanner(f)
var (
etst []Batcher
// tcases temporary []Caser slice
tcases []Caser
// incases toggle to tell if we're currently building a []Caser slice for BatchSwitchCase
incases bool
)
for scn.Scan() {
ln := scn.Text()
if err := scn.Err(); err != nil {
return []Batcher{}, err
}
if res := cReg.FindStringSubmatch(ln); res != nil {
incases = true
res[2] = strings.Replace(res[2], `\n`, "\n", -1)
tcases = append(tcases, &Case{regexp.MustCompile(res[1]), res[2], nil, 0})
continue
}
if res := eReg.FindStringSubmatch(ln); res != nil {
if incases {
incases = false
etst = append(etst, &BCas{tcases})
tcases = []Caser{}
}
res[1] = strings.Replace(res[1], `\n`, "\n", -1)
etst = append(etst, &BExp{res[1]})
continue
}
if res := sReg.FindStringSubmatch(ln); res != nil {
if incases {
incases = false
etst = append(etst, &BCas{tcases})
tcases = []Caser{}
}
res[1] = strings.Replace(res[1], `\n`, "\n", -1)
etst = append(etst, &BSnd{res[1]})
continue
}
}
// If c: is the last thing we have we need to tie it up
if incases {
etst = append(etst, &BCas{tcases})
}
tMap[fstring] = etst
return etst, nil
}