or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-assertions.mdformat.mdgbytes.mdgcustom.mdgexec.mdghttp.mdgleak.mdgmeasure.mdgstruct.mdindex.mdmatchers.mdtypes-interfaces.md
tile.json

gexec.mddocs/

GExec Package

Package: github.com/onsi/gomega/gexec

The gexec package provides support for testing external processes in Go. It wraps os/exec.Cmd commands in a Session type that provides convenient methods for managing process lifecycle, capturing output, and making assertions about exit codes.

Import

import "github.com/onsi/gomega/gexec"

Overview

The gexec package enables:

  • Starting and managing external processes
  • Capturing stdout and stderr to testable buffers
  • Sending signals (SIGKILL, SIGTERM, SIGINT, custom)
  • Waiting for process exit with timeouts
  • Building Go binaries for testing
  • Making assertions about process exit codes

Session Type

The Session type wraps an exec.Cmd and provides process management capabilities.

Session Struct

type Session struct {
    Command *exec.Cmd          // The underlying command
    Out     *gbytes.Buffer     // stdout buffer
    Err     *gbytes.Buffer     // stderr buffer
    Exited  <-chan struct{}    // Closed when process exits
}

{ .api }

Fields:

  • Command - The wrapped *exec.Cmd
  • Out - A *gbytes.Buffer connected to the command's stdout
  • Err - A *gbytes.Buffer connected to the command's stderr
  • Exited - A channel that closes when the command exits

Session Methods

ExitCode

func (s *Session) ExitCode() int

{ .api }

Returns the wrapped command's exit code. Returns -1 if the command hasn't exited yet.

When a process exits due to a signal, the exit code will be 128 + signal-value (per Unix conventions).

Example:

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

Eventually(session).Should(gexec.Exit(0))
code := session.ExitCode()
Expect(code).To(Equal(0))

Wait

func (s *Session) Wait(timeout ...any) *Session

{ .api }

Waits until the wrapped command exits. Accepts an optional timeout (uses Eventually under the hood, so accepts the same timeout/polling interval arguments).

If the command does not exit within the timeout, Wait will trigger a test failure.

Returns the session to enable chaining.

Example:

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Wait with default timeout
session.Wait()

// Wait with custom timeout
session.Wait(5 * time.Second)

// Chain to access output
output := session.Wait().Out.Contents()

Kill

func (s *Session) Kill() *Session

{ .api }

Sends a SIGKILL signal to the running command. Does not wait for the process to exit.

If the command has already exited, Kill returns silently.

Returns the session to enable chaining.

Example:

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Kill and wait
session.Kill().Wait()

Interrupt

func (s *Session) Interrupt() *Session

{ .api }

Sends a SIGINT signal to the running command. Does not wait for the process to exit.

If the command has already exited, Interrupt returns silently.

Returns the session to enable chaining.

Example:

session.Interrupt().Wait(time.Second)

Terminate

func (s *Session) Terminate() *Session

{ .api }

Sends a SIGTERM signal to the running command. Does not wait for the process to exit.

If the command has already exited, Terminate returns silently.

Returns the session to enable chaining.

Example:

session.Terminate().Wait(3 * time.Second)

Signal

func (s *Session) Signal(signal os.Signal) *Session

{ .api }

Sends a custom signal to the running command. Does not wait for the process to exit.

If the command has already exited, Signal returns silently.

Returns the session to enable chaining.

Example:

import "syscall"

session.Signal(syscall.SIGUSR1)

Buffer

func (s *Session) Buffer() *gbytes.Buffer

{ .api }

Returns the stdout buffer (Out). This implements the gbytes.BufferProvider interface, allowing you to use gbytes.Say matcher directly on the session without referencing .Out:

Example:

// These are equivalent:
Eventually(session.Out).Should(gbytes.Say("foo"))
Eventually(session).Should(gbytes.Say("foo"))

Session Management Functions

Start

func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error)

{ .api }

Starts the passed-in *exec.Cmd command and wraps it in a *gexec.Session.

The session pipes the command's stdout and stderr to two *gbytes.Buffer objects available as session.Out and session.Err. These buffers can be used with the gbytes.Say matcher.

When outWriter and/or errWriter are non-nil, the session will pipe output both to the session buffers and to the provided writers. This is useful for logging output to screen. With Ginkgo, it's convenient to use GinkgoWriter:

session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)

This logs output when running tests in verbose mode, or only when a test fails.

The session is tracked globally and can be managed with package-level functions like KillAndWait().

Note: Do not call command.Wait() yourself. The session handles waiting internally.

Example:

cmd := exec.Command("my-app", "--port", "8080")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

Eventually(session.Out).Should(gbytes.Say("Server started"))
Eventually(session.Out).Should(gbytes.Say("Listening on :8080"))

session.Terminate().Wait()

Global Session Control Functions

These functions operate on all sessions started by Start(). They're useful for cleanup in test suites.

KillAndWait

func KillAndWait(timeout ...any)

{ .api }

Sends SIGKILL to all tracked sessions and waits for them to exit. The timeout is applied to each process individually.

If any processes have already exited, KillAndWait returns silently.

Example:

var _ = AfterSuite(func() {
    gexec.KillAndWait(5 * time.Second)
    gexec.CleanupBuildArtifacts()
})

TerminateAndWait

func TerminateAndWait(timeout ...any)

{ .api }

Sends SIGTERM to all tracked sessions and waits for them to exit. The timeout is applied to each process individually.

If any processes have already exited, TerminateAndWait returns silently.

Kill

func Kill()

{ .api }

Sends SIGKILL to all tracked sessions. Does not wait for processes to exit.

If any processes have already exited, Kill returns silently.

Terminate

func Terminate()

{ .api }

Sends SIGTERM to all tracked sessions. Does not wait for processes to exit.

If any processes have already exited, Terminate returns silently.

Signal

func Signal(signal os.Signal)

{ .api }

Sends the specified signal to all tracked sessions. Does not wait for processes to exit.

If any processes have already exited, Signal returns silently.

Example:

import "syscall"

gexec.Signal(syscall.SIGHUP) // Reload configuration

Interrupt

func Interrupt()

{ .api }

Sends SIGINT to all tracked sessions. Does not wait for processes to exit.

If any processes have already exited, Interrupt returns silently.

Build Functions

The gexec package provides functions to compile Go packages and test binaries, useful for integration testing.

Build

func Build(packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Compiles the Go package at packagePath using go build. The resulting binary is saved in a temporary directory, and its path is returned.

Uses the $GOPATH environment variable (or the default GOPATH if not set, on Go 1.8+). Additional arguments are passed to go build.

Example:

var serverPath string

var _ = BeforeSuite(func() {
    var err error
    serverPath, err = gexec.Build("github.com/myorg/myapp/cmd/server")
    Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
    gexec.CleanupBuildArtifacts()
})

It("runs the server", func() {
    cmd := exec.Command(serverPath, "--port", "8080")
    session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session.Out).Should(gbytes.Say("Server started"))
    session.Terminate().Wait()
})

BuildWithEnvironment

func BuildWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error)

{ .api }

Identical to Build but allows you to specify environment variables to be set during compilation.

Example:

serverPath, err := gexec.BuildWithEnvironment(
    "github.com/myorg/myapp",
    []string{"CGO_ENABLED=0", "GOOS=linux"},
    "-ldflags", "-s -w",
)

BuildIn

func BuildIn(gopath string, packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Identical to Build but allows you to specify a custom $GOPATH as the first argument.

Example:

serverPath, err := gexec.BuildIn("/custom/gopath", "mypackage")

CompileTest

func CompileTest(packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Compiles a test binary for the package at packagePath using go test -c. The resulting binary is saved in a temporary directory, and its path is returned.

Deprecated: CompileTest makes GOPATH assumptions that don't translate well to the Go modules world. Consider using Build with the -c flag instead.

Example:

testPath, err := gexec.CompileTest("github.com/myorg/mypackage")
Expect(err).NotTo(HaveOccurred())

cmd := exec.Command(testPath, "-test.v")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

CompileTestWithEnvironment

func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error)

{ .api }

Identical to CompileTest but allows you to specify environment variables to be set during test binary compilation.

Example:

testPath, err := gexec.CompileTestWithEnvironment(
    "github.com/myorg/mypackage",
    []string{"CGO_ENABLED=0", "GOOS=linux"},
    "-tags", "integration",
)

CompileTestIn

func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Identical to CompileTest but allows you to specify a custom $GOPATH as the first argument.

Example:

testPath, err := gexec.CompileTestIn("/custom/gopath", "mypackage")

GetAndCompileTest

func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Downloads the package at packagePath (using go get) and then compiles its test binary using go test -c. The resulting binary is saved in a temporary directory, and its path is returned.

This is useful for compiling test binaries of external packages.

Example:

testPath, err := gexec.GetAndCompileTest("github.com/external/package")
Expect(err).NotTo(HaveOccurred())

GetAndCompileTestWithEnvironment

func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error)

{ .api }

Identical to GetAndCompileTest but allows you to specify environment variables to be set during download and test binary compilation.

Example:

testPath, err := gexec.GetAndCompileTestWithEnvironment(
    "github.com/external/package",
    []string{"GOPRIVATE=github.com/myorg/*"},
)

GetAndCompileTestIn

func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error)

{ .api }

Identical to GetAndCompileTest but allows you to specify a custom $GOPATH as the first argument.

Example:

testPath, err := gexec.GetAndCompileTestIn("/custom/gopath", "github.com/external/package")

CleanupBuildArtifacts

func CleanupBuildArtifacts()

{ .api }

Removes all compiled binaries created by the Build functions. Should be called before your test suite ends, typically in an AfterSuite callback.

Example:

var _ = AfterSuite(func() {
    gexec.KillAndWait()
    gexec.CleanupBuildArtifacts()
})

Matchers

Exit

func Exit(optionalExitCode ...int) *exitMatcher

{ .api }

The Exit matcher operates on a session and passes if the session has exited.

If no status code is provided, Exit succeeds if the session has exited regardless of exit code. If a status code is provided, Exit only succeeds if the process exited with that specific code.

Note: The process must have already exited. To wait for a process to exit, use Eventually:

Examples:

// Check that process has exited (any exit code)
Eventually(session).Should(gexec.Exit())

// Check for specific exit code
Eventually(session, 3*time.Second).Should(gexec.Exit(0))

// Check for non-zero exit
Eventually(session).Should(gexec.Exit(1))

// Negative assertion
Consistently(session, 100*time.Millisecond).ShouldNot(gexec.Exit())

The Exit matcher implements OracleMatcher, so it works correctly with Eventually and Consistently - it signals that the match result may change in the future while the process is still running.

Utilities

NewPrefixedWriter

func NewPrefixedWriter(prefix string, writer io.Writer) *PrefixedWriter

{ .api }

Creates an io.Writer that wraps another writer and prefixes each line with the specified string.

This is useful when running multiple sessions concurrently - you can prefix the output of each session to distinguish between them.

Example:

cmd1 := exec.Command("server1")
session1, err := gexec.Start(cmd1,
    gexec.NewPrefixedWriter("[server1] ", GinkgoWriter),
    gexec.NewPrefixedWriter("[server1] ", GinkgoWriter),
)

cmd2 := exec.Command("server2")
session2, err := gexec.Start(cmd2,
    gexec.NewPrefixedWriter("[server2] ", GinkgoWriter),
    gexec.NewPrefixedWriter("[server2] ", GinkgoWriter),
)

PrefixedWriter Type

type PrefixedWriter struct {
    // unexported fields
}

{ .api }

A writer that prefixes each line with a specified string. Created by NewPrefixedWriter.

Method:

func (w *PrefixedWriter) Write(b []byte) (int, error)

{ .api }

Implements io.Writer. Writes data to the underlying writer, prefixing each new line with the configured prefix.

Interfaces

Exiter

type Exiter interface {
    ExitCode() int
}

{ .api }

The Exiter interface represents types that have an exit code. The Session type implements this interface, making it compatible with the Exit matcher.

Any custom type implementing ExitCode() int can be used with the Exit matcher.

Constants

INVALID_EXIT_CODE

const INVALID_EXIT_CODE = 254

{ .api }

The exit code value used when a process exits in an unexpected way and the actual exit code cannot be determined. This is distinct from -1, which indicates the process hasn't exited yet.

Complete Example

Here's a comprehensive example showing common gexec usage patterns:

package myapp_test

import (
    "os/exec"
    "testing"
    "time"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "github.com/onsi/gomega/gbytes"
    "github.com/onsi/gomega/gexec"
)

var serverPath string

var _ = BeforeSuite(func() {
    var err error
    // Build the server binary once for all tests
    serverPath, err = gexec.Build("github.com/myorg/myapp/cmd/server")
    Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
    // Cleanup any running sessions and build artifacts
    gexec.KillAndWait(5 * time.Second)
    gexec.CleanupBuildArtifacts()
})

var _ = Describe("Server", func() {
    var session *gexec.Session

    AfterEach(func() {
        if session != nil {
            session.Terminate().Wait(time.Second)
        }
    })

    It("starts and responds to requests", func() {
        cmd := exec.Command(serverPath, "--port", "8080")
        var err error
        session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
        Expect(err).NotTo(HaveOccurred())

        // Wait for server to start
        Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Server listening"))

        // Make requests to the server...

        // Gracefully terminate
        session.Terminate()
        Eventually(session, 3*time.Second).Should(gexec.Exit(0))
        Expect(session.Out).To(gbytes.Say("Shutdown complete"))
    })

    It("handles SIGINT gracefully", func() {
        cmd := exec.Command(serverPath)
        var err error
        session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
        Expect(err).NotTo(HaveOccurred())

        Eventually(session.Out).Should(gbytes.Say("Server listening"))

        session.Interrupt()
        Eventually(session, 2*time.Second).Should(gexec.Exit(0))
    })

    It("reports errors to stderr", func() {
        cmd := exec.Command(serverPath, "--invalid-flag")
        var err error
        session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
        Expect(err).NotTo(HaveOccurred())

        Eventually(session, 2*time.Second).Should(gexec.Exit(1))
        Expect(session.Err).To(gbytes.Say("unknown flag"))
    })

    Context("multiple servers", func() {
        var session1, session2 *gexec.Session

        AfterEach(func() {
            if session1 != nil {
                session1.Kill().Wait()
            }
            if session2 != nil {
                session2.Kill().Wait()
            }
        })

        It("runs multiple servers concurrently with prefixed output", func() {
            cmd1 := exec.Command(serverPath, "--port", "8081")
            var err error
            session1, err = gexec.Start(cmd1,
                gexec.NewPrefixedWriter("[8081] ", GinkgoWriter),
                gexec.NewPrefixedWriter("[8081] ", GinkgoWriter),
            )
            Expect(err).NotTo(HaveOccurred())

            cmd2 := exec.Command(serverPath, "--port", "8082")
            session2, err = gexec.Start(cmd2,
                gexec.NewPrefixedWriter("[8082] ", GinkgoWriter),
                gexec.NewPrefixedWriter("[8082] ", GinkgoWriter),
            )
            Expect(err).NotTo(HaveOccurred())

            Eventually(session1.Out).Should(gbytes.Say("Server listening"))
            Eventually(session2.Out).Should(gbytes.Say("Server listening"))
        })
    })
})

func TestMyApp(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "MyApp Suite")
}

Signal Handling Notes

Unix Signal Exit Codes

When a process is terminated by a signal, the exit code follows the convention:

exit_code = 128 + signal_number

Common signal exit codes:

  • 130 = 128 + 2 (SIGINT)
  • 137 = 128 + 9 (SIGKILL)
  • 143 = 128 + 15 (SIGTERM)

Example:

session.Kill()
Eventually(session).Should(gexec.Exit(137)) // 128 + SIGKILL(9)

Platform Considerations

Signal handling is Unix-specific. On Windows, only Kill() is fully supported (via os.Process.Kill()). Other signal methods may not behave as expected on Windows.

Best Practices

  1. Always call CleanupBuildArtifacts() in AfterSuite to remove temporary binaries
  2. Use Eventually with Exit matcher to wait for processes to exit rather than calling Wait() directly
  3. Prefer Terminate() over Kill() for graceful shutdown when possible
  4. Use GinkgoWriter for output writers to get conditional logging (only on failure unless verbose)
  5. Use NewPrefixedWriter when running multiple sessions concurrently to distinguish output
  6. Track sessions properly - either manage them manually or rely on global KillAndWait() cleanup
  7. Build once in BeforeSuite rather than rebuilding for each test
  8. Check both stdout and stderr - use session.Out and session.Err to capture all output

Related Packages

  • gbytes (github.com/onsi/gomega/gbytes) - Buffer testing utilities, provides the Say matcher used with session output
  • gomega (github.com/onsi/gomega) - Core assertion library, provides Eventually, Consistently, Expect