hesper

package module
v0.0.0-...-c00219f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

README

Hesper

Hesper manages Apple Container lifecycle for Go integration tests. Run real services like PostgreSQL in lightweight containers during tests.

Requirements

Install

go get codeberg.org/graphei/hesper

Quick start

func TestDatabase(t *testing.T) {
    ctx := context.Background()

    pg, err := postgres.Run(ctx, "postgres:16-alpine",
        postgres.WithHostPort(15432),
        postgres.WithDatabase("testdb"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer pg.Stop(ctx)

    pool, err := pgxpool.New(ctx, pg.MustConnectionString())
    // ... run queries
}

Core API

container, err := hesper.Run(ctx, "alpine:latest",
    hesper.WithPort(18080, 8080),
    hesper.WithEnv("KEY", "value"),
    hesper.WithCommand("sleep", "60"),
)
defer container.Stop(ctx)

host, _ := container.Host(8080) // "127.0.0.1:18080"
logs, _ := container.Logs(ctx)
Options
Option Description
WithPort(hostPort, containerPort) Publish a container port to the host
WithName(name) Set container name
WithEnv(key, value) Set an environment variable
WithLabel(key, value) Add a label
WithCommand(args...) Override the image CMD
WithEntrypoint(ep) Override the image ENTRYPOINT
WithVolume(host, container, ro) Mount a volume
WithWorkDir(path) Set working directory
WithAutoRemove() Remove container on exit
WithWaitStrategy(s) Wait for container readiness
Wait strategies
hesper.NewWaitForLog("Server started")                        // match a log line
hesper.WaitForLogOccurrence("ready", 2)                       // match N occurrences
hesper.NewWaitForExec("pg_isready")                           // run command inside container
hesper.NewWaitForPort(8080)                                   // wait for TCP port
hesper.NewWaitForAll(logWait, portWait)                       // combine strategies
hesper.NoWait{}                                               // skip waiting

PostgreSQL module

import "codeberg.org/graphei/hesper/modules/postgres"

pg, err := postgres.Run(ctx, "postgres:16-alpine",
    postgres.WithHostPort(15432),
    postgres.WithDatabase("mydb"),
    postgres.WithUsername("user"),
    postgres.WithPassword("pass"),
)

dsn := pg.MustConnectionString()
// postgres://user:pass@127.0.0.1:15432/mydb?sslmode=disable

The postgres module waits for readiness via pg_isready by default. Each test should use a unique host port to avoid conflicts when running in parallel.

Errors

Use errors.Is() to check for specific failures:

Error Description
ErrImageNotFound Image doesn't exist or can't be pulled
ErrContainerNotFound Container doesn't exist
ErrContainerNameInUse Name already taken
ErrUnknownCliError Unrecognized CLI error

How it works

Hesper calls container run to start containers with published ports, inspects for port mappings, waits for readiness, then returns a *Container. Access services via Host(containerPort) which returns the 127.0.0.1:hostPort address.

Cleanup happens via Stop() or WithAutoRemove(). A reaper automatically cleans up orphaned containers from previous test runs on startup.

Running tests

# Unit tests (no containers needed)
go test ./...

# Integration tests (requires running container CLI)
go test -tags integration -parallel 7 -timeout 120s ./...

License

AGPL-3.0

Documentation

Overview

Package hesper provides a Go library for managing Apple Containers in tests.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrImageNotFound      = errors.New("image not found")
	ErrContainerNameInUse = errors.New("container name already in use")
	ErrContainerNotFound  = errors.New("container not found")
	ErrUnknownCliError    = errors.New("an unknown CLI error occurred")
)

Functions

This section is empty.

Types

type Config

type Config struct {
	// contains filtered or unexported fields
}

Config is exported to allow external packages to implement Option.

type Container

type Container struct {
	// contains filtered or unexported fields
}

Container represents a running container managed by Hesper.

func MustRun

func MustRun(ctx context.Context, image string, opts ...Option) *Container

MustRun is a convenience wrapper around Run that panics on error. This is useful for test setup code where you want to fail fast.

Example:

container := hesper.MustRun(ctx, "postgres:16-alpine",
	hesper.WithEnv("POSTGRES_PASSWORD", "secret"),
)
defer container.Stop(ctx)

func Run

func Run(ctx context.Context, image string, opts ...Option) (*Container, error)

Run starts a new container with the specified image and options. It automatically adds the hesper.managed=true label for cleanup tracking.

The Run function performs these operations in order:

  1. Cleans up any orphaned containers from previous runs (once per process)
  2. Applies all provided options to configure the container
  3. Starts the container using the container CLI
  4. Inspects the container to retrieve published port mappings
  5. Waits for the container to be ready (if a wait strategy is provided)
  6. Returns a *Container with the container ID and published ports

Use WithPort to publish container ports to the host. Use Container.Host to get the host address for a published port.

Example:

ctx := context.Background()
container, err := hesper.Run(ctx, "postgres:16-alpine",
	hesper.WithEnv("POSTGRES_PASSWORD", "secret"),
	hesper.WithPort(15432, 5432),
	hesper.WithWaitStrategy(hesper.NewWaitForLog("ready to accept connections")),
)
if err != nil {
	log.Fatal(err)
}
defer container.Stop(ctx)

func RunWithInput

func RunWithInput(ctx context.Context, input RunInput) (*Container, error)

RunWithInput is an alternative to Run that uses a struct for parameters. This is useful when you need to pass many options or want to build the configuration programmatically.

func (*Container) Host

func (c *Container) Host(containerPort int) (string, error)

Host returns the host address and port for a given container port, formatted as "address:port" (e.g. "127.0.0.1:15432"). The container port must have been published via WithPort.

func (*Container) ID

func (c *Container) ID() string

func (*Container) Logs

func (c *Container) Logs(ctx context.Context) (string, error)

func (*Container) Stop

func (c *Container) Stop(ctx context.Context) error

Stop is idempotent: it returns nil if the container is already removed.

type HesperError

type HesperError struct {
	Type   error
	Stderr string
}

func (*HesperError) Error

func (e *HesperError) Error() string

func (*HesperError) Unwrap

func (e *HesperError) Unwrap() error

type NoWait

type NoWait struct{}

func (NoWait) WaitUntilReady

func (NoWait) WaitUntilReady(_ context.Context, _ *Container) error

type Option

type Option interface {
	Apply(*Config) error
}

Option configures a container.

func WithAutoRemove

func WithAutoRemove() Option

func WithCommand

func WithCommand(command ...string) Option

func WithEntrypoint

func WithEntrypoint(entrypoint ...string) Option

func WithEnv

func WithEnv(key, value string) Option

func WithLabel

func WithLabel(key, value string) Option

func WithName

func WithName(name string) Option

func WithPort

func WithPort(hostPort, containerPort int) Option

func WithVolume

func WithVolume(hostPath, containerPath string, readonly bool) Option

func WithWaitStrategy

func WithWaitStrategy(strategy WaitStrategy) Option

func WithWorkDir

func WithWorkDir(workDir string) Option

type RunInput

type RunInput struct {
	Image   string
	Options []Option
}

type WaitForAll

type WaitForAll struct {
	// contains filtered or unexported fields
}

WaitForAll combines multiple wait strategies, running them in sequence.

func NewWaitForAll

func NewWaitForAll(strategies ...WaitStrategy) *WaitForAll

func (*WaitForAll) WaitUntilReady

func (w *WaitForAll) WaitUntilReady(ctx context.Context, container *Container) error

type WaitForExec

type WaitForExec struct {
	// contains filtered or unexported fields
}

WaitForExec waits until a command executed inside the container exits successfully.

func NewWaitForExec

func NewWaitForExec(command ...string) *WaitForExec

func (*WaitForExec) WaitUntilReady

func (w *WaitForExec) WaitUntilReady(ctx context.Context, container *Container) error

type WaitForLog

type WaitForLog struct {
	// contains filtered or unexported fields
}

func NewWaitForLog

func NewWaitForLog(message string) *WaitForLog

func WaitForLogOccurrence

func WaitForLogOccurrence(message string, count int) *WaitForLog

func (*WaitForLog) WaitUntilReady

func (w *WaitForLog) WaitUntilReady(ctx context.Context, container *Container) error

func (*WaitForLog) WithPollInterval

func (w *WaitForLog) WithPollInterval(interval time.Duration) *WaitForLog

type WaitForPort

type WaitForPort struct {
	// contains filtered or unexported fields
}

WaitForPort waits until a published container port accepts TCP connections.

func NewWaitForPort

func NewWaitForPort(containerPort int) *WaitForPort

func (*WaitForPort) WaitUntilReady

func (w *WaitForPort) WaitUntilReady(ctx context.Context, container *Container) error

type WaitStrategy

type WaitStrategy interface {
	WaitUntilReady(ctx context.Context, container *Container) error
}

Directories

Path Synopsis
internal
exec
Package exec provides internal utilities for executing container CLI commands.
Package exec provides internal utilities for executing container CLI commands.
reaper
Package reaper provides functionality to clean up orphaned containers.
Package reaper provides functionality to clean up orphaned containers.
modules
postgres
Package postgres provides a Postgres-specific wrapper around the hesper container library.
Package postgres provides a Postgres-specific wrapper around the hesper container library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL