2 Package gexec provides support for testing external processes.
14 . "github.com/onsi/gomega"
15 "github.com/onsi/gomega/gbytes"
18 const INVALID_EXIT_CODE = 254
24 //A *gbytes.Buffer connected to the command's stdout
27 //A *gbytes.Buffer connected to the command's stderr
30 //A channel that will close when the command exits
31 Exited <-chan struct{}
38 Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session.
40 The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err.
41 These buffers can be used with the gbytes.Say matcher to match against unread output:
43 Ω(session.Out).Should(gbytes.Say("foo-out"))
44 Ω(session.Err).Should(gbytes.Say("foo-err"))
46 In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with:
48 Ω(session).Should(gbytes.Say("foo-out"))
50 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.
51 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:
53 session, err := Start(command, GinkgoWriter, GinkgoWriter)
55 This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails.
57 The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself.
58 Instead, to assert that the command has exited you can use the gexec.Exit matcher:
60 Ω(session).Should(gexec.Exit())
62 When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any
63 Eventuallys waiting for the buffers to Say something.
65 func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) {
66 exited := make(chan struct{})
70 Out: gbytes.NewBuffer(),
71 Err: gbytes.NewBuffer(),
77 var commandOut, commandErr io.Writer
79 commandOut, commandErr = session.Out, session.Err
81 if outWriter != nil && !reflect.ValueOf(outWriter).IsNil() {
82 commandOut = io.MultiWriter(commandOut, outWriter)
85 if errWriter != nil && !reflect.ValueOf(errWriter).IsNil() {
86 commandErr = io.MultiWriter(commandErr, errWriter)
89 command.Stdout = commandOut
90 command.Stderr = commandErr
92 err := command.Start()
94 go session.monitorForExit(exited)
95 trackedSessionsMutex.Lock()
96 defer trackedSessionsMutex.Unlock()
97 trackedSessions = append(trackedSessions, session)
104 Buffer implements the gbytes.BufferProvider interface and returns s.Out
105 This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out:
107 Eventually(session).Should(gbytes.Say("foo"))
109 func (s *Session) Buffer() *gbytes.Buffer {
114 ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1.
116 To assert that the command has exited it is more convenient to use the Exit matcher:
118 Eventually(s).Should(gexec.Exit())
120 When the process exits because it has received a particular signal, the exit code will be 128+signal-value
121 (See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html)
124 func (s *Session) ExitCode() int {
126 defer s.lock.Unlock()
131 Wait waits until the wrapped command exits. It can be passed an optional timeout.
132 If the command does not exit within the timeout, Wait will trigger a test failure.
134 Wait returns the session, making it possible to chain:
136 session.Wait().Out.Contents()
138 will wait for the command to exit then return the entirety of Out's contents.
140 Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does.
142 func (s *Session) Wait(timeout ...interface{}) *Session {
143 EventuallyWithOffset(1, s, timeout...).Should(Exit())
148 Kill sends the running command a SIGKILL signal. It does not wait for the process to exit.
150 If the command has already exited, Kill returns silently.
152 The session is returned to enable chaining.
154 func (s *Session) Kill() *Session {
155 if s.ExitCode() != -1 {
158 s.Command.Process.Kill()
163 Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit.
165 If the command has already exited, Interrupt returns silently.
167 The session is returned to enable chaining.
169 func (s *Session) Interrupt() *Session {
170 return s.Signal(syscall.SIGINT)
174 Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit.
176 If the command has already exited, Terminate returns silently.
178 The session is returned to enable chaining.
180 func (s *Session) Terminate() *Session {
181 return s.Signal(syscall.SIGTERM)
185 Signal sends the running command the passed in signal. It does not wait for the process to exit.
187 If the command has already exited, Signal returns silently.
189 The session is returned to enable chaining.
191 func (s *Session) Signal(signal os.Signal) *Session {
192 if s.ExitCode() != -1 {
195 s.Command.Process.Signal(signal)
199 func (s *Session) monitorForExit(exited chan<- struct{}) {
200 err := s.Command.Wait()
204 status := s.Command.ProcessState.Sys().(syscall.WaitStatus)
205 if status.Signaled() {
206 s.exitCode = 128 + int(status.Signal())
208 exitStatus := status.ExitStatus()
209 if exitStatus == -1 && err != nil {
210 s.exitCode = INVALID_EXIT_CODE
212 s.exitCode = exitStatus
219 var trackedSessions = []*Session{}
220 var trackedSessionsMutex = &sync.Mutex{}
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.
226 If any of the processes already exited, KillAndWait returns silently.
228 func KillAndWait(timeout ...interface{}) {
229 trackedSessionsMutex.Lock()
230 defer trackedSessionsMutex.Unlock()
231 for _, session := range trackedSessions {
232 session.Kill().Wait(timeout...)
234 trackedSessions = []*Session{}
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.
241 If any of the processes already exited, TerminateAndWait returns silently.
243 func TerminateAndWait(timeout ...interface{}) {
244 trackedSessionsMutex.Lock()
245 defer trackedSessionsMutex.Unlock()
246 for _, session := range trackedSessions {
247 session.Terminate().Wait(timeout...)
252 Kill sends a SIGKILL signal to all the processes started by Run.
253 It does not wait for the processes to exit.
255 If any of the processes already exited, Kill returns silently.
258 trackedSessionsMutex.Lock()
259 defer trackedSessionsMutex.Unlock()
260 for _, session := range trackedSessions {
266 Terminate sends a SIGTERM signal to all the processes started by Run.
267 It does not wait for the processes to exit.
269 If any of the processes already exited, Terminate returns silently.
272 trackedSessionsMutex.Lock()
273 defer trackedSessionsMutex.Unlock()
274 for _, session := range trackedSessions {
280 Signal sends the passed in signal to all the processes started by Run.
281 It does not wait for the processes to exit.
283 If any of the processes already exited, Signal returns silently.
285 func Signal(signal os.Signal) {
286 trackedSessionsMutex.Lock()
287 defer trackedSessionsMutex.Unlock()
288 for _, session := range trackedSessions {
289 session.Signal(signal)
294 Interrupt sends the SIGINT signal to all the processes started by Run.
295 It does not wait for the processes to exit.
297 If any of the processes already exited, Interrupt returns silently.
300 trackedSessionsMutex.Lock()
301 defer trackedSessionsMutex.Unlock()
302 for _, session := range trackedSessions {