2016-04-08 01:13:13 +00:00
|
|
|
/*
|
|
|
|
Copyright 2016 Gravitational, Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package integration
|
|
|
|
|
|
|
|
import (
|
2016-04-14 18:43:08 +00:00
|
|
|
"bytes"
|
2016-04-14 20:30:13 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2016-04-14 18:43:08 +00:00
|
|
|
"os"
|
2016-04-11 23:30:58 +00:00
|
|
|
"os/user"
|
2016-04-08 17:38:19 +00:00
|
|
|
"strconv"
|
2016-05-04 23:49:59 +00:00
|
|
|
"strings"
|
2016-04-08 01:13:13 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2016-04-11 23:30:58 +00:00
|
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
2016-05-01 08:36:21 +00:00
|
|
|
"github.com/gravitational/teleport/lib/events"
|
|
|
|
"github.com/gravitational/teleport/lib/session"
|
2016-04-08 02:01:31 +00:00
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
|
|
|
2016-04-08 01:13:13 +00:00
|
|
|
"gopkg.in/check.v1"
|
|
|
|
)
|
|
|
|
|
2016-04-13 01:44:09 +00:00
|
|
|
const (
|
|
|
|
Host = "127.0.0.1"
|
|
|
|
Site = "local-site"
|
2016-04-14 20:30:13 +00:00
|
|
|
|
2016-05-04 23:49:59 +00:00
|
|
|
AllocatePortsNum = 100
|
2016-04-13 01:44:09 +00:00
|
|
|
)
|
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
type IntSuite struct {
|
2016-04-14 20:30:13 +00:00
|
|
|
ports utils.PortList
|
2016-04-11 23:30:58 +00:00
|
|
|
me *user.User
|
2016-04-13 01:44:09 +00:00
|
|
|
// priv/pub pair to avoid re-generating it
|
|
|
|
priv []byte
|
|
|
|
pub []byte
|
2016-04-08 17:38:19 +00:00
|
|
|
}
|
2016-04-08 01:13:13 +00:00
|
|
|
|
|
|
|
// bootstrap check
|
|
|
|
func TestIntegrations(t *testing.T) { check.TestingT(t) }
|
|
|
|
|
2016-05-08 18:53:57 +00:00
|
|
|
var _ = check.Suite(&IntSuite{})
|
2016-04-08 01:13:13 +00:00
|
|
|
|
2016-04-14 18:43:08 +00:00
|
|
|
func (s *IntSuite) TearDownSuite(c *check.C) {
|
|
|
|
var err error
|
|
|
|
// restore os.Stdin to its original condition: connected to /dev/null
|
|
|
|
os.Stdin.Close()
|
|
|
|
os.Stdin, err = os.Open("/dev/null")
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
}
|
|
|
|
|
2016-04-08 01:13:13 +00:00
|
|
|
func (s *IntSuite) SetUpSuite(c *check.C) {
|
2016-04-14 20:30:13 +00:00
|
|
|
var err error
|
2016-04-08 02:01:31 +00:00
|
|
|
utils.InitLoggerForTests()
|
2016-04-11 23:30:58 +00:00
|
|
|
SetTestTimeouts(100)
|
2016-04-08 17:38:19 +00:00
|
|
|
|
2016-05-08 18:53:57 +00:00
|
|
|
s.priv, s.pub, err = testauthority.New().GenerateKeyPair("")
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
// find 10 free litening ports to use
|
2016-04-14 20:30:13 +00:00
|
|
|
s.ports, err = utils.GetFreeTCPPorts(AllocatePortsNum)
|
2016-04-08 17:38:19 +00:00
|
|
|
if err != nil {
|
|
|
|
c.Fatal(err)
|
|
|
|
}
|
2016-04-11 23:30:58 +00:00
|
|
|
s.me, _ = user.Current()
|
2016-04-14 18:43:08 +00:00
|
|
|
|
|
|
|
// close & re-open stdin because 'go test' runs with os.stdin connected to /dev/null
|
2016-04-14 21:42:10 +00:00
|
|
|
stdin, err := os.Open("/dev/tty")
|
|
|
|
if err != nil {
|
|
|
|
os.Stdin.Close()
|
|
|
|
os.Stdin = stdin
|
|
|
|
}
|
2016-04-08 01:13:13 +00:00
|
|
|
}
|
|
|
|
|
2016-04-13 01:44:09 +00:00
|
|
|
// newTeleport helper returns a running Teleport instance pre-configured
|
|
|
|
// with the current user os.user.Current()
|
2016-05-04 23:49:59 +00:00
|
|
|
func (s *IntSuite) newTeleport(c *check.C, logins []string, enableSSH bool) *TeleInstance {
|
2016-04-14 20:30:13 +00:00
|
|
|
t := NewInstance(Site, Host, s.getPorts(5), s.priv, s.pub)
|
2016-05-04 23:49:59 +00:00
|
|
|
// use passed logins, but use suite's default login if nothing was passed
|
|
|
|
if logins == nil || len(logins) == 0 {
|
|
|
|
logins = []string{s.me.Username}
|
|
|
|
}
|
|
|
|
for _, login := range logins {
|
|
|
|
t.AddUser(login, []string{login})
|
|
|
|
}
|
|
|
|
|
2016-04-14 18:43:08 +00:00
|
|
|
if t.Create(nil, enableSSH, nil) != nil {
|
2016-04-13 01:44:09 +00:00
|
|
|
c.FailNow()
|
|
|
|
}
|
|
|
|
if t.Start() != nil {
|
|
|
|
c.FailNow()
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
2016-05-04 23:49:59 +00:00
|
|
|
// TestAudit creates a live session, records a bunch of data through it (>5MB) and
|
|
|
|
// then reads it back and compares against simulated reality
|
|
|
|
func (s *IntSuite) TestAudit(c *check.C) {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
// create server and get the reference to the site API:
|
|
|
|
t := s.newTeleport(c, nil, true)
|
|
|
|
site := t.GetSiteAPI(Site)
|
|
|
|
c.Assert(site, check.NotNil)
|
|
|
|
defer t.Stop()
|
|
|
|
|
|
|
|
// should have no sessions:
|
|
|
|
sessions, _ := site.GetSessions()
|
|
|
|
c.Assert(len(sessions), check.Equals, 0)
|
|
|
|
|
|
|
|
// create interactive session (this goroutine is this user's terminal time)
|
|
|
|
endC := make(chan error, 0)
|
|
|
|
myTerm := NewTerminal(250)
|
|
|
|
go func() {
|
|
|
|
cl, err := t.NewClient(s.me.Username, Site, Host, t.GetPortSSHInt())
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
cl.Output = &myTerm
|
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
err = cl.SSH([]string{}, false, &myTerm)
|
|
|
|
endC <- err
|
2016-05-04 23:49:59 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
// wait until there's a session in there:
|
|
|
|
for len(sessions) == 0 {
|
|
|
|
time.Sleep(time.Millisecond * 5)
|
|
|
|
sessions, _ = site.GetSessions()
|
|
|
|
}
|
|
|
|
session := &sessions[0]
|
|
|
|
// wait for the user to join this session:
|
|
|
|
for len(session.Parties) == 0 {
|
|
|
|
time.Sleep(time.Millisecond * 5)
|
|
|
|
session, err = site.GetSession(sessions[0].ID)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
}
|
|
|
|
// make sure it's us who joined! :)
|
|
|
|
c.Assert(session.Parties[0].User, check.Equals, s.me.Username)
|
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
// lets add something to the session streaam:
|
|
|
|
// write 1MB chunk
|
|
|
|
bigChunk := make([]byte, 1024*1024)
|
|
|
|
err = site.PostSessionChunk(session.ID, bytes.NewReader(bigChunk))
|
|
|
|
c.Assert(err, check.Equals, nil)
|
2016-05-06 23:10:50 +00:00
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
// then add small prefix:
|
|
|
|
err = site.PostSessionChunk(session.ID, bytes.NewBufferString("\nsuffix"))
|
|
|
|
c.Assert(err, check.Equals, nil)
|
|
|
|
|
2016-05-04 23:49:59 +00:00
|
|
|
// lets type "echo hi" followed by "enter" and then "exit" + "enter":
|
|
|
|
myTerm.Type("\aecho hi\n\r\aexit\n\r\a")
|
|
|
|
|
|
|
|
// wait for session to end:
|
|
|
|
<-endC
|
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
// read back the entire session (we have to try several times until we get back
|
|
|
|
// everything because the session is closing)
|
2016-05-08 04:17:28 +00:00
|
|
|
const expectedLen = 1048600
|
2016-05-06 06:51:56 +00:00
|
|
|
var sessionStream []byte
|
|
|
|
for i := 0; len(sessionStream) < expectedLen; i++ {
|
|
|
|
sessionStream, err = site.GetSessionChunk(session.ID, 0, events.MaxChunkBytes)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
time.Sleep(time.Millisecond * 250)
|
|
|
|
if i > 10 {
|
|
|
|
// session stream keeps coming back short
|
|
|
|
c.Fatalf("stream is too short: <%d", expectedLen)
|
|
|
|
}
|
|
|
|
}
|
2016-05-04 23:49:59 +00:00
|
|
|
|
|
|
|
// see what we got. It looks different based on bash settings, but here it is
|
|
|
|
// on Ev's machine (hostname is 'edsger'):
|
|
|
|
//
|
|
|
|
// edsger ~: echo hi
|
|
|
|
// hi
|
|
|
|
// edsger ~: exit
|
|
|
|
// logout
|
2016-05-06 06:51:56 +00:00
|
|
|
// <1MB of zeros here>
|
2016-05-04 23:49:59 +00:00
|
|
|
// suffix
|
|
|
|
//
|
|
|
|
c.Assert(strings.Contains(string(sessionStream), "echo hi"), check.Equals, true)
|
2016-05-06 06:51:56 +00:00
|
|
|
c.Assert(strings.Contains(string(sessionStream), "\nsuffix"), check.Equals, true)
|
2016-05-04 23:49:59 +00:00
|
|
|
|
|
|
|
// now lets look at session events:
|
|
|
|
history, err := site.GetSessionEvents(session.ID, 0)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
|
2016-05-06 23:10:50 +00:00
|
|
|
getChunk := func(e events.EventFields, maxlen int) string {
|
2016-05-04 23:49:59 +00:00
|
|
|
offset := e.GetInt("offset")
|
|
|
|
length := e.GetInt("bytes")
|
|
|
|
if length == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
2016-05-06 23:10:50 +00:00
|
|
|
if length > maxlen {
|
|
|
|
length = maxlen
|
|
|
|
}
|
2016-05-04 23:49:59 +00:00
|
|
|
return string(sessionStream[offset : offset+length])
|
|
|
|
}
|
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
findByType := func(et string) events.EventFields {
|
|
|
|
for _, e := range history {
|
|
|
|
if e.GetType() == et {
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2016-05-04 23:49:59 +00:00
|
|
|
|
2016-05-06 06:51:56 +00:00
|
|
|
// there should alwys be 'session.start' event (and it must be first)
|
|
|
|
first := history[0]
|
|
|
|
start := findByType(events.SessionStartEvent)
|
|
|
|
c.Assert(start, check.DeepEquals, first)
|
|
|
|
c.Assert(start.GetInt("bytes"), check.Equals, 0)
|
|
|
|
c.Assert(start.GetString(events.SessionEventID) != "", check.Equals, true)
|
|
|
|
c.Assert(start.GetString(events.TerminalSize) != "", check.Equals, true)
|
|
|
|
|
2016-05-06 23:10:50 +00:00
|
|
|
// find "\nsuffix" write and find our huge 1MB chunk
|
|
|
|
prefixFound, hugeChunkFound := false, false
|
|
|
|
for _, e := range history {
|
|
|
|
if getChunk(e, 10) == "\nsuffix" {
|
|
|
|
prefixFound = true
|
|
|
|
}
|
|
|
|
if e.GetInt("bytes") == 1048576 {
|
|
|
|
hugeChunkFound = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.Assert(prefixFound, check.Equals, true)
|
|
|
|
c.Assert(hugeChunkFound, check.Equals, true)
|
2016-05-06 06:51:56 +00:00
|
|
|
|
|
|
|
// there should alwys be 'session.end' event
|
|
|
|
end := findByType(events.SessionEndEvent)
|
|
|
|
c.Assert(end, check.NotNil)
|
|
|
|
c.Assert(end.GetInt("bytes"), check.Equals, 0)
|
|
|
|
c.Assert(end.GetString(events.SessionEventID) != "", check.Equals, true)
|
|
|
|
|
|
|
|
// there should alwys be 'session.leave' event
|
|
|
|
leave := findByType(events.SessionLeaveEvent)
|
|
|
|
c.Assert(leave, check.NotNil)
|
|
|
|
c.Assert(leave.GetInt("bytes"), check.Equals, 0)
|
|
|
|
c.Assert(leave.GetString(events.SessionEventID) != "", check.Equals, true)
|
2016-05-04 23:49:59 +00:00
|
|
|
|
|
|
|
// all of them should have a proper time:
|
|
|
|
for _, e := range history {
|
|
|
|
c.Assert(e.GetTime("time").IsZero(), check.Equals, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-14 20:30:13 +00:00
|
|
|
// TestInteractive covers SSH into shell and joining the same session from another client
|
|
|
|
func (s *IntSuite) TestInteractive(c *check.C) {
|
2016-05-04 23:49:59 +00:00
|
|
|
t := s.newTeleport(c, nil, true)
|
2016-04-13 01:44:09 +00:00
|
|
|
defer t.Stop()
|
|
|
|
|
2016-04-14 20:30:13 +00:00
|
|
|
sessionEndC := make(chan interface{}, 0)
|
|
|
|
|
|
|
|
// get a reference to site obj:
|
2016-05-04 23:49:59 +00:00
|
|
|
site := t.GetSiteAPI(Site)
|
2016-04-14 20:30:13 +00:00
|
|
|
c.Assert(site, check.NotNil)
|
|
|
|
|
|
|
|
personA := NewTerminal(250)
|
|
|
|
personB := NewTerminal(250)
|
|
|
|
|
2016-05-04 23:49:59 +00:00
|
|
|
// PersonA: SSH into the server, wait one second, then type some commands on stdin:
|
|
|
|
openSession := func() {
|
|
|
|
cl, err := t.NewClient(s.me.Username, Site, Host, t.GetPortSSHInt())
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
cl.Output = &personA
|
|
|
|
// Person A types something into the terminal (including "exit")
|
|
|
|
personA.Type("\aecho hi\n\r\aexit\n\r\a")
|
|
|
|
err = cl.SSH([]string{}, false, &personA)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
sessionEndC <- true
|
|
|
|
}
|
|
|
|
|
2016-04-14 20:30:13 +00:00
|
|
|
// PersonB: wait for a session to become available, then join:
|
|
|
|
joinSession := func() {
|
|
|
|
var sessionID string
|
|
|
|
for {
|
2016-04-27 05:55:06 +00:00
|
|
|
time.Sleep(time.Millisecond)
|
2016-04-14 20:30:13 +00:00
|
|
|
sessions, _ := site.GetSessions()
|
|
|
|
if len(sessions) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
sessionID = string(sessions[0].ID)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
cl, err := t.NewClient(s.me.Username, Site, Host, t.GetPortSSHInt())
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
cl.Output = &personB
|
2016-04-14 21:42:10 +00:00
|
|
|
for i := 0; i < 10; i++ {
|
2016-05-01 08:36:21 +00:00
|
|
|
err = cl.Join(session.ID(sessionID), &personB)
|
2016-04-14 20:30:13 +00:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
}
|
|
|
|
|
|
|
|
go openSession()
|
|
|
|
go joinSession()
|
|
|
|
|
|
|
|
// wait for the session to end
|
|
|
|
waitFor(sessionEndC, time.Second*10)
|
|
|
|
|
|
|
|
// make sure both parites saw the same output:
|
2016-05-06 23:10:50 +00:00
|
|
|
c.Assert(string(personA.Output(100)), check.DeepEquals, string(personB.Output(100)))
|
2016-04-13 01:44:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TestInvalidLogins validates that you can't login with invalid login or
|
|
|
|
// with invalid 'site' parameter
|
2016-04-14 18:43:08 +00:00
|
|
|
func (s *IntSuite) TestInvalidLogins(c *check.C) {
|
2016-05-04 23:49:59 +00:00
|
|
|
t := s.newTeleport(c, nil, true)
|
2016-04-13 01:44:09 +00:00
|
|
|
defer t.Stop()
|
|
|
|
|
|
|
|
cmd := []string{"echo", "success"}
|
|
|
|
|
|
|
|
// try the wrong site:
|
2016-04-14 18:43:08 +00:00
|
|
|
tc, err := t.NewClient(s.me.Username, "wrong-site", Host, t.GetPortSSHInt())
|
|
|
|
c.Assert(err, check.IsNil)
|
2016-04-14 20:30:13 +00:00
|
|
|
err = tc.SSH(cmd, false, nil)
|
2016-04-13 01:44:09 +00:00
|
|
|
c.Assert(err, check.ErrorMatches, "site wrong-site not found")
|
|
|
|
}
|
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
// TestTwoSites creates two teleport sites: "a" and "b" and
|
|
|
|
// creates a tunnel from A to B.
|
|
|
|
//
|
|
|
|
// Then it executes an SSH command on A by connecting directly
|
|
|
|
// to A and by connecting to B via B<->A tunnel
|
2016-04-14 18:43:08 +00:00
|
|
|
func (s *IntSuite) TestTwoSites(c *check.C) {
|
2016-04-11 23:30:58 +00:00
|
|
|
username := s.me.Username
|
|
|
|
|
2016-04-14 20:30:13 +00:00
|
|
|
a := NewInstance("site-A", Host, s.getPorts(5), s.priv, s.pub)
|
|
|
|
b := NewInstance("site-B", Host, s.getPorts(5), s.priv, s.pub)
|
2016-04-11 23:30:58 +00:00
|
|
|
|
|
|
|
a.AddUser(username, []string{username})
|
|
|
|
b.AddUser(username, []string{username})
|
2016-04-08 02:01:31 +00:00
|
|
|
|
2016-04-14 18:43:08 +00:00
|
|
|
c.Assert(b.Create(a.Secrets.AsSlice(), false, nil), check.IsNil)
|
|
|
|
c.Assert(a.Create(b.Secrets.AsSlice(), true, nil), check.IsNil)
|
2016-04-08 01:13:13 +00:00
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
c.Assert(b.Start(), check.IsNil)
|
|
|
|
c.Assert(a.Start(), check.IsNil)
|
2016-04-08 01:13:13 +00:00
|
|
|
|
2016-04-11 23:30:58 +00:00
|
|
|
// wait for both sites to see each other via their reverse tunnels (for up to 10 seconds)
|
|
|
|
abortTime := time.Now().Add(time.Second * 10)
|
|
|
|
for len(b.Tunnel.GetSites()) < 2 && len(b.Tunnel.GetSites()) < 2 {
|
|
|
|
time.Sleep(time.Millisecond * 200)
|
|
|
|
if time.Now().After(abortTime) {
|
|
|
|
c.Fatalf("two sites do not see each other: tunnels are not working")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-14 18:43:08 +00:00
|
|
|
var (
|
|
|
|
outputA bytes.Buffer
|
|
|
|
outputB bytes.Buffer
|
|
|
|
)
|
|
|
|
|
2016-04-11 23:30:58 +00:00
|
|
|
// if we got here, it means two sites are cross-connected. lets execute SSH commands
|
|
|
|
sshPort := a.GetPortSSHInt()
|
|
|
|
cmd := []string{"echo", "hello world"}
|
2016-04-08 01:13:13 +00:00
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
// directly:
|
2016-04-14 18:43:08 +00:00
|
|
|
tc, err := a.NewClient(username, "site-A", Host, sshPort)
|
|
|
|
tc.Output = &outputA
|
2016-04-14 20:30:13 +00:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
err = tc.SSH(cmd, false, nil)
|
2016-04-14 18:43:08 +00:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
c.Assert(outputA.String(), check.Equals, "hello world\n")
|
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
// via tunnel b->a:
|
2016-04-14 18:43:08 +00:00
|
|
|
tc, err = b.NewClient(username, "site-A", Host, sshPort)
|
|
|
|
tc.Output = &outputB
|
2016-04-14 20:30:13 +00:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
err = tc.SSH(cmd, false, nil)
|
2016-04-08 17:38:19 +00:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
c.Assert(outputA, check.DeepEquals, outputB)
|
2016-04-08 01:13:13 +00:00
|
|
|
|
2016-04-08 17:38:19 +00:00
|
|
|
c.Assert(b.Stop(), check.IsNil)
|
|
|
|
c.Assert(a.Stop(), check.IsNil)
|
2016-04-08 01:13:13 +00:00
|
|
|
}
|
2016-04-14 20:30:13 +00:00
|
|
|
|
|
|
|
// getPorts helper returns a range of unallocated ports available for litening on
|
|
|
|
func (s *IntSuite) getPorts(num int) []int {
|
|
|
|
if len(s.ports) < num {
|
|
|
|
panic("do not have enough ports! increase AllocatePortsNum constant")
|
|
|
|
}
|
|
|
|
ports := make([]int, num)
|
|
|
|
for i := range ports {
|
|
|
|
p, _ := strconv.Atoi(s.ports.Pop())
|
|
|
|
ports[i] = p
|
|
|
|
}
|
|
|
|
return ports
|
|
|
|
}
|
|
|
|
|
|
|
|
// Terminal emulates stdin+stdout for integration testing
|
|
|
|
type Terminal struct {
|
|
|
|
io.Writer
|
|
|
|
io.Reader
|
|
|
|
|
|
|
|
written *bytes.Buffer
|
|
|
|
typed chan byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewTerminal(capacity int) Terminal {
|
|
|
|
return Terminal{
|
|
|
|
typed: make(chan byte, capacity),
|
|
|
|
written: bytes.NewBuffer([]byte{}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Terminal) Type(data string) {
|
|
|
|
for _, b := range []byte(data) {
|
|
|
|
t.typed <- b
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-06 23:10:50 +00:00
|
|
|
// Output returns a number of first 'limit' bytes printed into this fake terminal
|
|
|
|
func (t *Terminal) Output(limit int) string {
|
|
|
|
buff := t.written.Bytes()
|
|
|
|
if len(buff) > limit {
|
|
|
|
buff = buff[:limit]
|
2016-04-14 20:30:13 +00:00
|
|
|
}
|
2016-05-06 23:10:50 +00:00
|
|
|
// clean up white space for easier comparison:
|
|
|
|
return strings.TrimSpace(string(buff))
|
2016-04-14 20:30:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Terminal) Write(data []byte) (n int, err error) {
|
|
|
|
return t.written.Write(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Terminal) Read(p []byte) (n int, err error) {
|
|
|
|
for n = 0; n < len(p); n++ {
|
|
|
|
p[n] = <-t.typed
|
|
|
|
if p[n] == '\r' {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if p[n] == '\a' { // 'alert' used for debugging, means 'pause for 1 second'
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
n -= 1
|
|
|
|
}
|
|
|
|
time.Sleep(time.Millisecond * 10)
|
|
|
|
}
|
|
|
|
return n, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// waitFor helper waits on a challen for up to the given timeout
|
|
|
|
func waitFor(c chan interface{}, timeout time.Duration) error {
|
|
|
|
tick := time.Tick(timeout)
|
|
|
|
select {
|
|
|
|
case <-c:
|
|
|
|
return nil
|
|
|
|
case <-tick:
|
|
|
|
return fmt.Errorf("timeout waiting for event")
|
|
|
|
}
|
|
|
|
}
|