...

Source file src/github.com/onsi/gomega/gexec/session.go

Documentation: github.com/onsi/gomega/gexec

     1  /*
     2  Package gexec provides support for testing external processes.
     3  */
     4  
     5  // untested sections: 1
     6  
     7  package gexec
     8  
     9  import (
    10  	"io"
    11  	"os"
    12  	"os/exec"
    13  	"sync"
    14  	"syscall"
    15  
    16  	. "github.com/onsi/gomega"
    17  	"github.com/onsi/gomega/gbytes"
    18  )
    19  
    20  const INVALID_EXIT_CODE = 254
    21  
    22  type Session struct {
    23  	//The wrapped command
    24  	Command *exec.Cmd
    25  
    26  	//A *gbytes.Buffer connected to the command's stdout
    27  	Out *gbytes.Buffer
    28  
    29  	//A *gbytes.Buffer connected to the command's stderr
    30  	Err *gbytes.Buffer
    31  
    32  	//A channel that will close when the command exits
    33  	Exited <-chan struct{}
    34  
    35  	lock     *sync.Mutex
    36  	exitCode int
    37  }
    38  
    39  /*
    40  Start starts the passed-in *exec.Cmd command.  It wraps the command in a *gexec.Session.
    41  
    42  The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err.
    43  These buffers can be used with the gbytes.Say matcher to match against unread output:
    44  
    45  	Expect(session.Out).Should(gbytes.Say("foo-out"))
    46  	Expect(session.Err).Should(gbytes.Say("foo-err"))
    47  
    48  In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer.  This allows you to replace the first line, above, with:
    49  
    50  	Expect(session).Should(gbytes.Say("foo-out"))
    51  
    52  When outWriter and/or errWriter are non-nil, the session will pipe stdout and/or stderr output both into the session *gybtes.Buffers and to the passed-in outWriter/errWriter.
    53  This is useful for capturing the process's output or logging it to screen.  In particular, when using Ginkgo it can be convenient to direct output to the GinkgoWriter:
    54  
    55  	session, err := Start(command, GinkgoWriter, GinkgoWriter)
    56  
    57  This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails.
    58  
    59  The session wrapper is responsible for waiting on the *exec.Cmd command.  You *should not* call command.Wait() yourself.
    60  Instead, to assert that the command has exited you can use the gexec.Exit matcher:
    61  
    62  	Expect(session).Should(gexec.Exit())
    63  
    64  When the session exits it closes the stdout and stderr gbytes buffers.  This will short circuit any
    65  Eventuallys waiting for the buffers to Say something.
    66  */
    67  func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) {
    68  	exited := make(chan struct{})
    69  
    70  	session := &Session{
    71  		Command:  command,
    72  		Out:      gbytes.NewBuffer(),
    73  		Err:      gbytes.NewBuffer(),
    74  		Exited:   exited,
    75  		lock:     &sync.Mutex{},
    76  		exitCode: -1,
    77  	}
    78  
    79  	var commandOut, commandErr io.Writer
    80  
    81  	commandOut, commandErr = session.Out, session.Err
    82  
    83  	if outWriter != nil {
    84  		commandOut = io.MultiWriter(commandOut, outWriter)
    85  	}
    86  
    87  	if errWriter != nil {
    88  		commandErr = io.MultiWriter(commandErr, errWriter)
    89  	}
    90  
    91  	command.Stdout = commandOut
    92  	command.Stderr = commandErr
    93  
    94  	err := command.Start()
    95  	if err == nil {
    96  		go session.monitorForExit(exited)
    97  		trackedSessionsMutex.Lock()
    98  		defer trackedSessionsMutex.Unlock()
    99  		trackedSessions = append(trackedSessions, session)
   100  	}
   101  
   102  	return session, err
   103  }
   104  
   105  /*
   106  Buffer implements the gbytes.BufferProvider interface and returns s.Out
   107  This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out:
   108  
   109  	Eventually(session).Should(gbytes.Say("foo"))
   110  */
   111  func (s *Session) Buffer() *gbytes.Buffer {
   112  	return s.Out
   113  }
   114  
   115  /*
   116  ExitCode returns the wrapped command's exit code.  If the command hasn't exited yet, ExitCode returns -1.
   117  
   118  To assert that the command has exited it is more convenient to use the Exit matcher:
   119  
   120  	Eventually(s).Should(gexec.Exit())
   121  
   122  When the process exits because it has received a particular signal, the exit code will be 128+signal-value
   123  (See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html)
   124  */
   125  func (s *Session) ExitCode() int {
   126  	s.lock.Lock()
   127  	defer s.lock.Unlock()
   128  	return s.exitCode
   129  }
   130  
   131  /*
   132  Wait waits until the wrapped command exits.  It can be passed an optional timeout.
   133  If the command does not exit within the timeout, Wait will trigger a test failure.
   134  
   135  Wait returns the session, making it possible to chain:
   136  
   137  	session.Wait().Out.Contents()
   138  
   139  will wait for the command to exit then return the entirety of Out's contents.
   140  
   141  Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does.
   142  */
   143  func (s *Session) Wait(timeout ...interface{}) *Session {
   144  	EventuallyWithOffset(1, s, timeout...).Should(Exit())
   145  	return s
   146  }
   147  
   148  /*
   149  Kill sends the running command a SIGKILL signal.  It does not wait for the process to exit.
   150  
   151  If the command has already exited, Kill returns silently.
   152  
   153  The session is returned to enable chaining.
   154  */
   155  func (s *Session) Kill() *Session {
   156  	return s.Signal(syscall.SIGKILL)
   157  }
   158  
   159  /*
   160  Interrupt sends the running command a SIGINT signal.  It does not wait for the process to exit.
   161  
   162  If the command has already exited, Interrupt returns silently.
   163  
   164  The session is returned to enable chaining.
   165  */
   166  func (s *Session) Interrupt() *Session {
   167  	return s.Signal(syscall.SIGINT)
   168  }
   169  
   170  /*
   171  Terminate sends the running command a SIGTERM signal.  It does not wait for the process to exit.
   172  
   173  If the command has already exited, Terminate returns silently.
   174  
   175  The session is returned to enable chaining.
   176  */
   177  func (s *Session) Terminate() *Session {
   178  	return s.Signal(syscall.SIGTERM)
   179  }
   180  
   181  /*
   182  Signal sends the running command the passed in signal.  It does not wait for the process to exit.
   183  
   184  If the command has already exited, Signal returns silently.
   185  
   186  The session is returned to enable chaining.
   187  */
   188  func (s *Session) Signal(signal os.Signal) *Session {
   189  	if s.processIsAlive() {
   190  		s.Command.Process.Signal(signal)
   191  	}
   192  	return s
   193  }
   194  
   195  func (s *Session) monitorForExit(exited chan<- struct{}) {
   196  	err := s.Command.Wait()
   197  	s.lock.Lock()
   198  	s.Out.Close()
   199  	s.Err.Close()
   200  	status := s.Command.ProcessState.Sys().(syscall.WaitStatus)
   201  	if status.Signaled() {
   202  		s.exitCode = 128 + int(status.Signal())
   203  	} else {
   204  		exitStatus := status.ExitStatus()
   205  		if exitStatus == -1 && err != nil {
   206  			s.exitCode = INVALID_EXIT_CODE
   207  		}
   208  		s.exitCode = exitStatus
   209  	}
   210  	s.lock.Unlock()
   211  
   212  	close(exited)
   213  }
   214  
   215  func (s *Session) processIsAlive() bool {
   216  	return s.ExitCode() == -1 && s.Command.Process != nil
   217  }
   218  
   219  var trackedSessions = []*Session{}
   220  var trackedSessionsMutex = &sync.Mutex{}
   221  
   222  /*
   223  Kill sends a SIGKILL signal to all the processes started by Run, and waits for them to exit.
   224  The timeout specified is applied to each process killed.
   225  
   226  If any of the processes already exited, KillAndWait returns silently.
   227  */
   228  func KillAndWait(timeout ...interface{}) {
   229  	trackedSessionsMutex.Lock()
   230  	defer trackedSessionsMutex.Unlock()
   231  	for _, session := range trackedSessions {
   232  		session.Kill().Wait(timeout...)
   233  	}
   234  	trackedSessions = []*Session{}
   235  }
   236  
   237  /*
   238  Kill sends a SIGTERM signal to all the processes started by Run, and waits for them to exit.
   239  The timeout specified is applied to each process killed.
   240  
   241  If any of the processes already exited, TerminateAndWait returns silently.
   242  */
   243  func TerminateAndWait(timeout ...interface{}) {
   244  	trackedSessionsMutex.Lock()
   245  	defer trackedSessionsMutex.Unlock()
   246  	for _, session := range trackedSessions {
   247  		session.Terminate().Wait(timeout...)
   248  	}
   249  }
   250  
   251  /*
   252  Kill sends a SIGKILL signal to all the processes started by Run.
   253  It does not wait for the processes to exit.
   254  
   255  If any of the processes already exited, Kill returns silently.
   256  */
   257  func Kill() {
   258  	trackedSessionsMutex.Lock()
   259  	defer trackedSessionsMutex.Unlock()
   260  	for _, session := range trackedSessions {
   261  		session.Kill()
   262  	}
   263  }
   264  
   265  /*
   266  Terminate sends a SIGTERM signal to all the processes started by Run.
   267  It does not wait for the processes to exit.
   268  
   269  If any of the processes already exited, Terminate returns silently.
   270  */
   271  func Terminate() {
   272  	trackedSessionsMutex.Lock()
   273  	defer trackedSessionsMutex.Unlock()
   274  	for _, session := range trackedSessions {
   275  		session.Terminate()
   276  	}
   277  }
   278  
   279  /*
   280  Signal sends the passed in signal to all the processes started by Run.
   281  It does not wait for the processes to exit.
   282  
   283  If any of the processes already exited, Signal returns silently.
   284  */
   285  func Signal(signal os.Signal) {
   286  	trackedSessionsMutex.Lock()
   287  	defer trackedSessionsMutex.Unlock()
   288  	for _, session := range trackedSessions {
   289  		session.Signal(signal)
   290  	}
   291  }
   292  
   293  /*
   294  Interrupt sends the SIGINT signal to all the processes started by Run.
   295  It does not wait for the processes to exit.
   296  
   297  If any of the processes already exited, Interrupt returns silently.
   298  */
   299  func Interrupt() {
   300  	trackedSessionsMutex.Lock()
   301  	defer trackedSessionsMutex.Unlock()
   302  	for _, session := range trackedSessions {
   303  		session.Interrupt()
   304  	}
   305  }
   306  

View as plain text