| /* |
| Package gexec provides support for testing external processes. |
| */ |
| package gexec |
| |
| import ( |
| "io" |
| "os" |
| "os/exec" |
| "reflect" |
| "sync" |
| "syscall" |
| |
| . "github.com/onsi/gomega" |
| "github.com/onsi/gomega/gbytes" |
| ) |
| |
| const INVALID_EXIT_CODE = 254 |
| |
| type Session struct { |
| //The wrapped command |
| Command *exec.Cmd |
| |
| //A *gbytes.Buffer connected to the command's stdout |
| Out *gbytes.Buffer |
| |
| //A *gbytes.Buffer connected to the command's stderr |
| Err *gbytes.Buffer |
| |
| //A channel that will close when the command exits |
| Exited <-chan struct{} |
| |
| lock *sync.Mutex |
| exitCode int |
| } |
| |
| /* |
| Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session. |
| |
| The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err. |
| These buffers can be used with the gbytes.Say matcher to match against unread output: |
| |
| Ω(session.Out).Should(gbytes.Say("foo-out")) |
| Ω(session.Err).Should(gbytes.Say("foo-err")) |
| |
| In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with: |
| |
| Ω(session).Should(gbytes.Say("foo-out")) |
| |
| 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. |
| 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: |
| |
| session, err := Start(command, GinkgoWriter, GinkgoWriter) |
| |
| This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails. |
| |
| The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself. |
| Instead, to assert that the command has exited you can use the gexec.Exit matcher: |
| |
| Ω(session).Should(gexec.Exit()) |
| |
| When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any |
| Eventuallys waiting fo the buffers to Say something. |
| */ |
| func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) { |
| exited := make(chan struct{}) |
| |
| session := &Session{ |
| Command: command, |
| Out: gbytes.NewBuffer(), |
| Err: gbytes.NewBuffer(), |
| Exited: exited, |
| lock: &sync.Mutex{}, |
| exitCode: -1, |
| } |
| |
| var commandOut, commandErr io.Writer |
| |
| commandOut, commandErr = session.Out, session.Err |
| |
| if outWriter != nil && !reflect.ValueOf(outWriter).IsNil() { |
| commandOut = io.MultiWriter(commandOut, outWriter) |
| } |
| |
| if errWriter != nil && !reflect.ValueOf(errWriter).IsNil() { |
| commandErr = io.MultiWriter(commandErr, errWriter) |
| } |
| |
| command.Stdout = commandOut |
| command.Stderr = commandErr |
| |
| err := command.Start() |
| if err == nil { |
| go session.monitorForExit(exited) |
| } |
| |
| return session, err |
| } |
| |
| /* |
| Buffer implements the gbytes.BufferProvider interface and returns s.Out |
| This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out: |
| |
| Eventually(session).Should(gbytes.Say("foo")) |
| */ |
| func (s *Session) Buffer() *gbytes.Buffer { |
| return s.Out |
| } |
| |
| /* |
| ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1. |
| |
| To assert that the command has exited it is more convenient to use the Exit matcher: |
| |
| Eventually(s).Should(gexec.Exit()) |
| |
| When the process exits because it has received a particular signal, the exit code will be 128+signal-value |
| (See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html) |
| |
| */ |
| func (s *Session) ExitCode() int { |
| s.lock.Lock() |
| defer s.lock.Unlock() |
| return s.exitCode |
| } |
| |
| /* |
| Wait waits until the wrapped command exits. It can be passed an optional timeout. |
| If the command does not exit within the timeout, Wait will trigger a test failure. |
| |
| Wait returns the session, making it possible to chain: |
| |
| session.Wait().Out.Contents() |
| |
| will wait for the command to exit then return the entirety of Out's contents. |
| |
| Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does. |
| */ |
| func (s *Session) Wait(timeout ...interface{}) *Session { |
| EventuallyWithOffset(1, s, timeout...).Should(Exit()) |
| return s |
| } |
| |
| /* |
| Kill sends the running command a SIGKILL signal. It does not wait for the process to exit. |
| |
| If the command has already exited, Kill returns silently. |
| |
| The session is returned to enable chaining. |
| */ |
| func (s *Session) Kill() *Session { |
| if s.ExitCode() != -1 { |
| return s |
| } |
| s.Command.Process.Kill() |
| return s |
| } |
| |
| /* |
| Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit. |
| |
| If the command has already exited, Interrupt returns silently. |
| |
| The session is returned to enable chaining. |
| */ |
| func (s *Session) Interrupt() *Session { |
| return s.Signal(syscall.SIGINT) |
| } |
| |
| /* |
| Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit. |
| |
| If the command has already exited, Terminate returns silently. |
| |
| The session is returned to enable chaining. |
| */ |
| func (s *Session) Terminate() *Session { |
| return s.Signal(syscall.SIGTERM) |
| } |
| |
| /* |
| Terminate sends the running command the passed in signal. It does not wait for the process to exit. |
| |
| If the command has already exited, Signal returns silently. |
| |
| The session is returned to enable chaining. |
| */ |
| func (s *Session) Signal(signal os.Signal) *Session { |
| if s.ExitCode() != -1 { |
| return s |
| } |
| s.Command.Process.Signal(signal) |
| return s |
| } |
| |
| func (s *Session) monitorForExit(exited chan<- struct{}) { |
| err := s.Command.Wait() |
| s.lock.Lock() |
| s.Out.Close() |
| s.Err.Close() |
| status := s.Command.ProcessState.Sys().(syscall.WaitStatus) |
| if status.Signaled() { |
| s.exitCode = 128 + int(status.Signal()) |
| } else { |
| exitStatus := status.ExitStatus() |
| if exitStatus == -1 && err != nil { |
| s.exitCode = INVALID_EXIT_CODE |
| } |
| s.exitCode = exitStatus |
| } |
| s.lock.Unlock() |
| |
| close(exited) |
| } |