or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

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

gexec.mddocs/

gexec Package - Testing External Processes

The gexec package provides powerful utilities for testing external processes in Go. It simplifies building Go binaries, executing them, and making assertions about their behavior including exit codes, stdout, and stderr output.

Overview

Package: github.com/onsi/gomega/gexec

The gexec package is designed for testing command-line applications and external processes. It provides:

  • Building Go binaries for testing
  • Starting and managing external processes
  • Capturing stdout and stderr in thread-safe buffers
  • Matchers for verifying process exit behavior
  • Automatic cleanup of build artifacts

Core Types

Session

{ .api }

type Session struct {
    Command *exec.Cmd
    Out     *Buffer
    Err     *Buffer
    Exited  <-chan struct{}
    // Contains filtered or unexported fields
}

Represents a running external process with captured output.

Fields:

  • Command - The underlying *exec.Cmd for the process
  • Out - Thread-safe buffer containing stdout output (from gbytes package)
  • Err - Thread-safe buffer containing stderr output (from gbytes package)
  • Exited - Channel that closes when the command exits

Session Methods

Wait

{ .api }

func (s *Session) Wait(timeout ...time.Duration) *Session

Waits for the session to exit. If timeout is provided, waits up to that duration.

Parameters:

  • timeout - Optional timeout duration. If omitted, waits indefinitely

Returns: The Session (for chaining)

Usage:

session.Wait(5 * time.Second)

Kill

{ .api }

func (s *Session) Kill() *Session

Sends SIGKILL to the process, forcing immediate termination.

Returns: The Session (for chaining)

Terminate

{ .api }

func (s *Session) Terminate() *Session

Sends SIGTERM to the process, requesting graceful termination.

Returns: The Session (for chaining)

Signal

{ .api }

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

Sends a custom signal to the process.

Parameters:

  • signal - The OS signal to send (e.g., syscall.SIGUSR1)

Returns: The Session (for chaining)

Interrupt

{ .api }

func (s *Session) Interrupt() *Session

Sends SIGINT to the process (equivalent to Ctrl+C).

Returns: The Session (for chaining)

ExitCode

{ .api }

func (s *Session) ExitCode() int

Returns the exit code of the process. Returns -1 if the process hasn't exited yet.

Returns: Exit code as integer

Buffer

{ .api }

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

Returns the Out buffer, implementing the gbytes.BufferProvider interface.

Returns: *gbytes.Buffer - The stdout buffer

Usage:

session, _ := gexec.Start(command, nil, nil)
// Can use session directly with gbytes matchers
Eventually(session).Should(gbytes.Say("pattern"))

Starting Processes

Start

{ .api }

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

Starts an external command and returns a Session for managing it.

Parameters:

  • command - The *exec.Cmd to execute (created with exec.Command())
  • outWriter - Writer for stdout. Pass nil to use default Buffer, or pass GinkgoWriter to see output in Ginkgo
  • errWriter - Writer for stderr. Pass nil to use default Buffer, or pass GinkgoWriter to see output in Ginkgo

Returns:

  • *Session - Session managing the running process
  • error - Error if the command fails to start

Example:

command := exec.Command("myapp", "--flag", "value")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

Eventually(session).Should(gexec.Exit(0))

Building Go Binaries

Build

{ .api }

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

Builds a Go package and returns the path to the compiled binary. The binary is built in a temporary directory and automatically tracked for cleanup.

Parameters:

  • packagePath - Go package path to build (e.g., "github.com/user/myapp" or relative path like "./cmd/server")
  • args - Optional additional arguments passed to go build (e.g., "-race", "-tags=integration")

Returns:

  • string - Absolute path to the compiled binary
  • error - Error if compilation fails

Example:

binPath, err := gexec.Build("github.com/myorg/myapp")
Expect(err).NotTo(HaveOccurred())

command := exec.Command(binPath, "run")
session, err := gexec.Start(command, nil, nil)
Expect(err).NotTo(HaveOccurred())

BuildWithEnvironment

{ .api }

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

Builds a Go package with custom environment variables. Useful for cross-compilation or setting custom build flags.

Parameters:

  • packagePath - Go package path to build
  • env - Environment variables for the build process (e.g., []string{"GOOS=linux", "GOARCH=amd64"})
  • args - Optional additional build arguments

Returns:

  • string - Absolute path to the compiled binary
  • error - Error if compilation fails

Example:

// Cross-compile for Linux
env := []string{
    "GOOS=linux",
    "GOARCH=amd64",
    "CGO_ENABLED=0",
}
binPath, err := gexec.BuildWithEnvironment("./cmd/server", env, "-tags", "production")
Expect(err).NotTo(HaveOccurred())

BuildIn

{ .api }

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

Builds a Go package with a custom GOPATH. Useful for legacy projects or specific build setups.

Parameters:

  • gopath - Custom GOPATH to use for the build
  • packagePath - Go package path to build
  • args - Optional additional build arguments

Returns:

  • string - Absolute path to the compiled binary
  • error - Error if compilation fails

Example:

binPath, err := gexec.BuildIn("/custom/gopath", "myapp", "-v")
Expect(err).NotTo(HaveOccurred())

CleanupBuildArtifacts

{ .api }

func CleanupBuildArtifacts()

Removes all compiled binaries created by Build, BuildWithEnvironment, and BuildIn functions. Typically called in test cleanup hooks.

Example:

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

Global Process Control

These functions operate on all sessions started with gexec.

Kill

{ .api }

func Kill()

Sends SIGKILL to all tracked sessions without waiting for them to exit.

Usage:

// Emergency cleanup
gexec.Kill()

KillAndWait

{ .api }

func KillAndWait(timeout ...any)

Sends SIGKILL to all tracked sessions and waits for them to exit.

Parameters:

  • timeout - Optional timeout duration (time.Duration or string like "5s")

Usage:

gexec.KillAndWait(5 * time.Second)

Terminate

{ .api }

func Terminate()

Sends SIGTERM to all tracked sessions without waiting for them to exit.

Usage:

gexec.Terminate()

TerminateAndWait

{ .api }

func TerminateAndWait(timeout ...any)

Sends SIGTERM to all tracked sessions and waits for them to exit.

Parameters:

  • timeout - Optional timeout duration (time.Duration or string like "5s")

Usage:

gexec.TerminateAndWait(10 * time.Second)

Signal

{ .api }

func Signal(signal os.Signal)

Sends the specified signal to all tracked sessions without waiting.

Parameters:

  • signal - OS signal to send

Usage:

gexec.Signal(syscall.SIGUSR1)

Interrupt

{ .api }

func Interrupt()

Sends SIGINT to all tracked sessions without waiting for them to exit.

Usage:

gexec.Interrupt()

Additional Types

PrefixedWriter

{ .api }

type PrefixedWriter struct {
    // Contains filtered or unexported fields
}

An io.Writer that prefixes each line with a specified string.

Constructor:

{ .api }

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

Parameters:

  • prefix - String to prepend to each line
  • writer - Underlying writer

Returns: *PrefixedWriter

Usage:

prefixed := gexec.NewPrefixedWriter("[APP] ", GinkgoWriter)
session, _ := gexec.Start(command, prefixed, prefixed)
// Output will be prefixed: "[APP] log message"

Exiter Interface

{ .api }

type Exiter interface {
    ExitCode() int
}

Interface for types that have an exit code. Session implements this interface.

Constants

{ .api }

const INVALID_EXIT_CODE = 254

Special exit code indicating the process exit code couldn't be determined.

Matchers

Exit

{ .api }

func Exit(optionalExitCode ...int) types.GomegaMatcher

Matcher that succeeds when a Session has exited. Optionally verifies the exit code.

Parameters:

  • optionalExitCode - Expected exit code. If omitted, matches any exit (even non-zero)

Returns: GomegaMatcher for use with Should(), ShouldNot(), etc.

Examples:

// Wait for any exit
Eventually(session).Should(gexec.Exit())

// Verify successful exit (code 0)
Eventually(session).Should(gexec.Exit(0))

// Verify specific exit code
Eventually(session).Should(gexec.Exit(137))

// Verify non-zero exit
Eventually(session).ShouldNot(gexec.Exit(0))

Usage Examples

Basic Process Testing

package myapp_test

import (
    "os/exec"
    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "github.com/onsi/gomega/gexec"
)

var _ = Describe("MyApp", func() {
    var pathToApp string

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

    AfterSuite(func() {
        gexec.CleanupBuildArtifacts()
    })

    It("should exit successfully", func() {
        command := exec.Command(pathToApp)
        session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
        Expect(err).NotTo(HaveOccurred())

        Eventually(session).Should(gexec.Exit(0))
    })
})

Testing Command Output

It("should output the correct message", func() {
    command := exec.Command(pathToApp, "greet", "World")
    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session).Should(gexec.Exit(0))

    // Check stdout
    Expect(session.Out.Contents()).To(ContainSubstring("Hello, World!"))

    // Check stderr is empty
    Expect(session.Err.Contents()).To(BeEmpty())
})

Testing Error Conditions

It("should fail with invalid arguments", func() {
    command := exec.Command(pathToApp, "--invalid-flag")
    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session).ShouldNot(gexec.Exit(0))
    Expect(session.Err.Contents()).To(ContainSubstring("unknown flag"))
})

Using gbytes Matchers with Output

It("should stream output progressively", func() {
    command := exec.Command(pathToApp, "stream")
    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    // Use gbytes.Say to wait for specific output
    Eventually(session.Out).Should(gbytes.Say("Starting process"))
    Eventually(session.Out).Should(gbytes.Say("Processing complete"))

    Eventually(session).Should(gexec.Exit(0))
})

Testing Long-Running Processes

It("should run a server that can be terminated", func() {
    command := exec.Command(pathToApp, "serve", "--port", "8080")
    session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
    Expect(err).NotTo(HaveOccurred())

    // Wait for server to start
    Eventually(session.Out).Should(gbytes.Say("Server listening"))

    // Do some testing...

    // Terminate gracefully
    session.Terminate()
    Eventually(session, "5s").Should(gexec.Exit())
})

Killing Unresponsive Processes

It("should handle unresponsive processes", func() {
    command := exec.Command(pathToApp, "hang")
    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    // Try graceful termination
    session.Terminate()

    // If not exited after 2 seconds, force kill
    Eventually(session, "2s").Should(gexec.Exit())
    if session.ExitCode() == -1 {
        session.Kill()
        Eventually(session, "1s").Should(gexec.Exit())
    }
})

Building with Race Detector

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

It("should be race-free", func() {
    command := exec.Command(pathToApp, "concurrent-operation")
    session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session).Should(gexec.Exit(0))
    Expect(session.Err.Contents()).NotTo(ContainSubstring("DATA RACE"))
})

Testing with Custom Environment

It("should respect environment variables", func() {
    command := exec.Command(pathToApp)
    command.Env = append(os.Environ(), "APP_MODE=test", "LOG_LEVEL=debug")

    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session).Should(gexec.Exit(0))
    Expect(session.Out.Contents()).To(ContainSubstring("DEBUG"))
})

Testing with Stdin Input

It("should process stdin input", func() {
    command := exec.Command(pathToApp, "process")
    stdin, err := command.StdinPipe()
    Expect(err).NotTo(HaveOccurred())

    session, err := gexec.Start(command, nil, nil)
    Expect(err).NotTo(HaveOccurred())

    // Write to stdin
    _, err = stdin.Write([]byte("test input\n"))
    Expect(err).NotTo(HaveOccurred())
    stdin.Close()

    Eventually(session).Should(gexec.Exit(0))
    Expect(session.Out.Contents()).To(ContainSubstring("Processed: test input"))
})

Testing Signal Handling

It("should handle SIGUSR1 gracefully", func() {
    command := exec.Command(pathToApp, "daemon")
    session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
    Expect(err).NotTo(HaveOccurred())

    Eventually(session.Out).Should(gbytes.Say("Daemon started"))

    // Send custom signal
    session.Signal(syscall.SIGUSR1)

    Eventually(session.Out).Should(gbytes.Say("Received SIGUSR1"))

    session.Terminate()
    Eventually(session).Should(gexec.Exit(0))
})

Best Practices

  1. Always Build Once Per Suite: Build binaries in BeforeSuite() and reuse them across tests for better performance.

  2. Clean Up Artifacts: Always call CleanupBuildArtifacts() in AfterSuite() to remove temporary binaries.

  3. Use GinkgoWriter for Debugging: Pass GinkgoWriter as the output writer when you need to see process output during test failures.

  4. Set Reasonable Timeouts: Use Eventually(session, "5s") with appropriate timeouts to avoid hanging tests.

  5. Check Both Exit Code and Output: Verify both that the process exits correctly and produces expected output.

  6. Handle Cleanup in AfterEach: Ensure processes are terminated in AfterEach() to prevent leaked processes:

    AfterEach(func() {
        if session != nil {
            session.Kill()
        }
    })
  7. Use Race Detector in CI: Build with -race flag in CI environments to catch concurrency issues.

  8. Test Both Success and Failure Cases: Verify both successful operations and error handling.

Integration with Gomega Matchers

The Session's Out and Err buffers are from the gbytes package and support all gbytes matchers:

// Wait for output
Eventually(session.Out).Should(gbytes.Say("pattern"))

// Check final contents
Expect(session.Out.Contents()).To(MatchRegexp("regex.*pattern"))

// Use with timeout
Eventually(session.Out, "3s").Should(gbytes.Say("startup complete"))

Common Patterns

CLI Application Testing Suite

var _ = Describe("CLI Application", func() {
    var binPath string

    BeforeSuite(func() {
        var err error
        binPath, err = gexec.Build("./cmd/mycli")
        Expect(err).NotTo(HaveOccurred())
    })

    AfterSuite(func() {
        gexec.CleanupBuildArtifacts()
    })

    Context("version command", func() {
        It("should print version", func() {
            cmd := exec.Command(binPath, "version")
            session, err := gexec.Start(cmd, nil, nil)
            Expect(err).NotTo(HaveOccurred())

            Eventually(session).Should(gexec.Exit(0))
            Expect(session.Out.Contents()).To(MatchRegexp(`v\d+\.\d+\.\d+`))
        })
    })
})

This comprehensive approach ensures robust testing of command-line applications and external processes.