spanemuboost

package module
v0.4.5 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 35 Imported by: 1

README

SPANner EMUlator BOOtSTrapper

Go Reference

spanemuboost bootstraps Cloud Spanner Emulator and, experimentally, Spanner Omni for tests using testcontainers-go. Review the Spanner Omni software requirements before enabling the Omni path in local development or CI.

It inspired by autoConfigEmulator of:

This package doesn't have functionality of splitting statements and stripping comments. Consider to use memefish or helper packages.

Examples

Quick start
func TestFoo(t *testing.T) {
    env := spanemuboost.SetupEmulatorWithClients(t,
        spanemuboost.WithSetupDDLs(ddls),
    )
    // env.Client, env.DatabaseClient, env.InstanceClient available
    // cleanup is automatic via t.Cleanup
}

For non-test usage (e.g. embedding the emulator in an application where the testing package is unavailable), see runnable examples on pkg.go.dev.

Shared runtime, database-per-case

For many independent test or validation cases, prefer one shared backend runtime and one database per case. That is the recommended shape for the emulator, and it is even more important for Omni because each Omni runtime owns a memory-heavy container.

Use NewLazyRuntime plus SetupClients or OpenClients when cases should share a runtime lazily. Use WithRandomDatabaseID() for per-case isolation. See ExampleNewLazyRuntime_validationCases on pkg.go.dev for a validation-harness shape that composes shared and case-specific setup, and separates setup failures from candidate statement results.

Random database IDs do not enable schema teardown on Clients.Close() by default. The databases disappear when the runtime container is closed. For long-lived shared runtimes, use ForceSchemaTeardown() or explicit cleanup if database accumulation matters.

Need Entry point Starts a new runtime container?
One test owns runtime and clients SetupWithClients(t, backend, ...) Yes
Non-test code owns runtime and clients RunWithClients(ctx, backend, ...) Yes
Many tests share one runtime with testing.TB cleanup NewLazyRuntime(backend, ...) + SetupClients Once, on first use
Many cases need explicit context.Context or manual client cleanup NewLazyRuntime(backend, ...) + OpenClients Once, on first use
Eager runtime startup with multiple databases Run(ctx, backend, ...) + OpenClients Once, when Run is called
Spanner Omni (experimental)

Setup, Run, RunWithClients, and SetupWithClients with BackendOmni start a Spanner Omni single-server container and use the public Spanner gRPC API on port 15000 for database creation, DDL application, DML setup, and managed client creation. This path is intended for integration tests that want a real Omni runtime without depending on the emulator.

func TestOmni(t *testing.T) {
    env := spanemuboost.SetupWithClients(t, spanemuboost.BackendOmni,
        spanemuboost.WithRandomDatabaseID(),
        spanemuboost.WithSetupDDLs([]string{
            "CREATE TABLE tbl (pk STRING(MAX), col INT64) PRIMARY KEY (pk)",
        }),
        spanemuboost.WithSetupRawDMLs([]string{
            "INSERT INTO tbl (pk, col) VALUES ('foo', 1)",
        }),
    )

    err := env.Client.Single().Query(t.Context(), spanner.NewStatement(
        "SELECT col FROM tbl WHERE pk = 'foo'",
    )).Do(func(r *spanner.Row) error {
        var col int64
        return r.Column(0, &col)
    })
    if err != nil { t.Fatal(err) }
}

For Omni, use databases as the normal isolation unit. Do not use random project or instance IDs for ordinary test isolation; the single-server deployment uses fixed project and instance IDs. Use WithRandomDatabaseID() and share the runtime with NewLazyRuntime(BackendOmni, ...) when many cases are involved.

Omni caveat Detail
Experimental runtime Omni support is newer than the emulator path and should be treated as integration-test-oriented
Primary endpoint The main Spanner gRPC endpoint is 15000; the console remains separate
Resource use Each started Omni runtime owns one Spanner Omni container; plan for roughly 4 GiB of memory per concurrently running Omni container
Recommended client config Managed Omni clients force the RecommendedOmniClientConfig() transport defaults (DisableNativeMetrics and IsExperimentalHost) unless guardrails are disabled; the same helper remains the recommended base for external Go clients
Host and container prerequisites Review the Spanner Omni software requirements before enabling Omni in local development or CI; see Omni runtime environments for local Colima and Podman notes
Guardrails Known-invalid single-server Omni settings fail fast with human-readable errors; use DisableBackendGuardrails() only when testing a newer backend whose constraints may have changed

The repository's Omni integration tests are gated by SPANEMUBOOST_ENABLE_OMNI_TESTS=1 so default test runs stay hermetic unless the environment is explicitly prepared for Omni. Keep tests that start Omni runtimes serial unless the host has enough spare memory for multiple Omni containers. spanemuboost does not impose a global runtime lock; use go test -p=1 -parallel=1 or share a runtime with NewLazyRuntime(BackendOmni, ...) when memory is tight.

When running through Podman and Testcontainers-Go does not auto-detect Podman from DOCKER_HOST, set SPANEMUBOOST_TESTCONTAINERS_PROVIDER=podman for that command or pass WithContainerProvider(testcontainers.ProviderPodman). The environment variable affects all spanemuboost runtime containers, including the default emulator backend. For rootful Podman machine with Ryuk enabled, the relevant environment is:

env DOCKER_HOST=unix://<host Podman API socket> \
  TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/run/podman/podman.sock \
  TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true \
  SPANEMUBOOST_TESTCONTAINERS_PROVIDER=podman \
  SPANEMUBOOST_ENABLE_OMNI_TESTS=1 \
  go test -p=1 -parallel=1 ./...

Once a runtime is started, the shared client helpers are backend-neutral:

func TestSharedHelpers(t *testing.T) {
    runtime := spanemuboost.Setup(t, spanemuboost.BackendOmni)
    clients := spanemuboost.SetupClients(t, runtime,
        spanemuboost.WithRandomDatabaseID(),
        spanemuboost.WithSetupDDLs([]string{
            "CREATE TABLE tbl (pk STRING(MAX)) PRIMARY KEY (pk)",
        }),
    )
    _ = clients
}
Reusing a long-lived Omni runtime

Starting Spanner Omni through testcontainers is slow because each runtime pulls and boots a memory-heavy container. For local development and repeated test runs, start Omni once with the spanemuboost CLI and attach clients to the published endpoint:

spanemuboost serve omni --endpoint-file /tmp/omni-endpoint.json
spanemuboost stop --endpoint-file /tmp/omni-endpoint.json

By default, serve omni skips instance and database bootstrap so only the built-in spanner-info database exists until clients create their own. Pass --with-default-database to create the default emulator-database at serve time (legacy behavior).

The endpoint file is owned by serve: it is written on startup (including the serve process PID for lifecycle management) and removed on exit. Unset SPANEMUBOOST_ENDPOINT_FILE after stopping the lifecycle manager.

In another shell:

export SPANEMUBOOST_ENDPOINT_FILE=/tmp/omni-endpoint.json
export SPANEMUBOOST_ENABLE_OMNI_TESTS=1
go test -p=1 -parallel=1 ./...

Client code can use [NewLazyRuntimeFromEnvOrStart] to keep the existing testcontainers path while automatically attaching when endpoint env vars are set, or [NewAttachedRuntimeFromEnv] for explicit attachment:

runtime, err := spanemuboost.NewLazyRuntimeFromEnvOrStart(spanemuboost.BackendOmni)
if err != nil {
    log.Fatal(err)
}
clients, err := spanemuboost.OpenClients(ctx, runtime,
    spanemuboost.WithRandomDatabaseID(),
    spanemuboost.WithSetupDDLs(ddls),
)
Variable Purpose
SPANEMUBOOST_ENDPOINT_FILE JSON file written by spanemuboost serve
SPANEMUBOOST_OMNI_URI Direct Omni gRPC endpoint (host:port)
SPANEMUBOOST_EMULATOR_URI Direct emulator gRPC endpoint (host:port)

When attaching to Omni via SPANEMUBOOST_OMNI_URI, project and instance IDs default to default. Non-default IDs require the running Omni instance to match and are rejected by Omni guardrails unless callers use DisableBackendGuardrails() on a programmatic runtime constructor.

[AttachedRuntime.Close] is a no-op because the lifecycle manager owns the container. Stop the shared runtime with spanemuboost stop or by stopping the serve process directly.

Run, RunWithClients, Setup, SetupWithClients, OpenClients, SetupClients, RuntimePlatform, and NewLazyRuntime work across emulator and Omni. This backend-neutral API surface is the primary stable entry point; only the BackendOmni backend and its specific behaviors are considered experimental. Omni does not add separate exported startup or client-opening helpers.

Use RuntimePlatform(ctx, runtime) when you want to surface the actual resolved container platform for a package-provided runtime handle without downcasting back to *Emulator. Depending on what metadata the underlying runtime exposes, that may be an os/arch string such as linux/amd64, a variant-qualified string such as linux/arm64/v8, or an OS-only value such as linux.

Shared emulator patterns

As recommended by the Cloud Spanner Emulator FAQ:

What is the recommended test setup? Use a single emulator process and create a Cloud Spanner instance within it. Since creating databases is cheap in the emulator, we recommend that each test bring up and tear down its own database. This ensures hermetic testing and allows the test suite to run tests in parallel if needed.

Pattern Emulator lifetime Best for
Lazy (NewLazyRuntime(BackendEmulator, ...) + SetupClients) First SetupClients call → TestMain cleanup Packages mixing emulator and non-emulator tests; skips startup when unused
Eager (RunEmulator + SetupClients) TestMain start → TestMain cleanup All tests need the emulator; fail fast on startup errors
Subtests (SetupEmulator + SetupClients) Parent test → t.Cleanup Related tests grouped under one function; supports t.Parallel() in subtests

The emulator starts only when the first test calls SetupClients with the LazyRuntime. If go test -run TestUnit matches only tests that never use it, the container is never started.

var lazyRuntime = spanemuboost.NewLazyRuntime(
    spanemuboost.BackendEmulator,
    spanemuboost.EnableInstanceAutoConfigOnly(),
)

func TestMain(m *testing.M) { lazyRuntime.TestMain(m) }

func TestCreate(t *testing.T) {
    clients := spanemuboost.SetupClients(t, lazyRuntime,
        spanemuboost.WithRandomDatabaseID(),
        spanemuboost.WithSetupDDLs(ddls),
    )
    // use clients.Client...
}

func TestUnit(t *testing.T) {
    // Does NOT use lazyRuntime — emulator never starts
}

NewLazyEmulator remains available as a backward-compatible wrapper around the emulator backend, but NewLazyRuntime(BackendEmulator, ...) can cover the same shared-runtime patterns while also extending naturally to Omni.

When you need emulator-specific helpers such as Container(), call runtime := lazyRuntime.Setup(t) (or Get(ctx)) and type assert the result back to *spanemuboost.Emulator for the BackendEmulator case.

Eager shared emulator

testing.M does NOT implement testing.TB, so use RunEmulator directly in TestMain.

var emulator *spanemuboost.Emulator

func TestMain(m *testing.M) {
    var err error
    emulator, err = spanemuboost.RunEmulator(context.Background(),
        spanemuboost.EnableInstanceAutoConfigOnly(),
    )
    if err != nil { log.Fatal(err) }
    emulator.TestMain(m)
}

func TestCreate(t *testing.T) {
    clients := spanemuboost.SetupClients(t, emulator,
        spanemuboost.WithRandomDatabaseID(),
        spanemuboost.WithSetupDDLs(ddls),
    )
    // use clients.Client...
}
Shared emulator with subtests

When tests are naturally related and don't need TestMain, you can share an emulator within subtests of a single parent test. Since each subtest creates its own database via WithRandomDatabaseID(), subtests can safely run in parallel with t.Parallel().

func TestSuite(t *testing.T) {
    lazy := spanemuboost.NewLazyRuntime(
        spanemuboost.BackendEmulator,
        spanemuboost.EnableInstanceAutoConfigOnly(),
    )
    t.Cleanup(func() { _ = lazy.Close() })
    runtime := lazy.Setup(t)

    t.Run("test1", func(t *testing.T) {
        t.Parallel()
        clients := spanemuboost.SetupClients(t, runtime,
            spanemuboost.WithRandomDatabaseID(),
            spanemuboost.WithSetupDDLs(ddls),
        )
        // use clients.Client...
    })
}
SPANNER_EMULATOR_HOST environment variable

For serial tests with code that reads SPANNER_EMULATOR_HOST directly:

func TestWithEnvVar(t *testing.T) {
    lazy := spanemuboost.NewLazyRuntime(
        spanemuboost.BackendEmulator,
        spanemuboost.EnableInstanceAutoConfigOnly(),
    )
    t.Cleanup(func() { _ = lazy.Close() })
    runtime := lazy.Setup(t)
    t.Setenv("SPANNER_EMULATOR_HOST", runtime.URI())
    // Code under test that reads SPANNER_EMULATOR_HOST directly
}
Caveat Detail
No t.Parallel() t.Setenv panics if the test or an ancestor called t.Parallel()
Process-global The env var doesn't scale to concurrent tests
Prefer ClientOptions() Pass runtime.ClientOptions() or clients directly when possible

Documentation

Overview

Package spanemuboost starts Cloud Spanner Emulator and experimental Spanner Omni runtimes for tests, then bootstraps instances, databases, schema, and Spanner clients.

The emulator path is the default and stable path. Use SetupEmulatorWithClients for simple tests. For many independent cases against either backend, share one runtime with NewLazyRuntime and create one database per case with WithRandomDatabaseID.

The Omni path uses BackendOmni through the backend-neutral APIs such as Setup, Run, SetupWithClients, RunWithClients, OpenClients, SetupClients, and NewLazyRuntime. Omni support is experimental. The shared-runtime pattern is especially important for Omni because each started Omni runtime owns one Spanner Omni container.

Index

Examples

Constants

View Source
const (
	DefaultEmulatorImage = "gcr.io/cloud-spanner-emulator/emulator:1.5.54"
	DefaultProjectID     = "emulator-project"
	DefaultInstanceID    = "emulator-instance"
	DefaultDatabaseID    = "emulator-database"
)

Variables

This section is empty.

Functions

func EndpointConfigured added in v0.4.4

func EndpointConfigured() bool

EndpointConfigured reports whether external endpoint env vars are set in the current process environment. It does not validate that LoadEndpoint succeeds.

func EndpointConfiguredForBackend added in v0.4.4

func EndpointConfiguredForBackend(backend Backend) bool

EndpointConfiguredForBackend reports whether an endpoint for the requested backend is configured. Unlike EndpointConfigured, this ignores unrelated backend URI env vars so callers can distinguish Omni from emulator endpoints.

func NewEmulator deprecated

func NewEmulator(ctx context.Context, options ...Option) (emulator *tcspanner.Container, teardown func(), err error)

Deprecated: Use SetupEmulator (for tests) or RunEmulator instead.

NewEmulator initializes Cloud Spanner Emulator. The emulator will be closed when teardown is called. You should call it.

func RecommendedOmniClientConfig added in v0.4.0

func RecommendedOmniClientConfig() spanner.ClientConfig

RecommendedOmniClientConfig returns the recommended spanner.ClientConfig for a Go Spanner data client connecting to the experimental Omni backend. The helper remains part of the backend-neutral API surface, but its Omni-specific recommendations may evolve before v1.

Example (ExternalClient)
package main

import (
	"context"
	"log"
	"os"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	if os.Getenv("SPANEMUBOOST_ENABLE_OMNI_TESTS") != "1" {
		return
	}

	ctx := context.Background()
	runtime, err := spanemuboost.Run(ctx, spanemuboost.BackendOmni,
		spanemuboost.WithRandomDatabaseID(),
	)
	if err != nil {
		log.Printf("failed to start Omni runtime: %v", err)
		return
	}
	defer runtime.Close() //nolint:errcheck

	client, err := spanner.NewClientWithConfig(
		ctx,
		runtime.DatabasePath(),
		spanemuboost.RecommendedOmniClientConfig(),
		runtime.ClientOptions()...,
	)
	if err != nil {
		log.Printf("failed to create Omni client: %v", err)
		return
	}
	defer client.Close()

	_ = client
}

func RuntimePlatform added in v0.4.0

func RuntimePlatform(ctx context.Context, runtime RuntimeHandle) (string, error)

RuntimePlatform returns the actual resolved container platform (for example, "linux/amd64", "linux/arm64", or, when available, a variant-qualified value such as "linux/arm64/v8") for a package-provided runtime handle. When the underlying runtime only exposes partial metadata, it may return an OS-only value such as "linux". AttachedRuntime values return "attached" because they do not own a container.

It accepts the same started and lazy handles as OpenClients and SetupClients, and resolves lazy handles by starting them on first use.

func SaveEndpoint added in v0.4.4

func SaveEndpoint(path string, endpoint Endpoint) error

SaveEndpoint writes endpoint metadata as JSON with mode 0600.

func Serve added in v0.4.4

func Serve(ctx context.Context, backend Backend, endpointPath string, options ...Option) error

Serve starts a backend runtime, writes its Endpoint metadata when endpointPath is non-empty, and blocks until ctx is canceled. The runtime is closed before Serve returns. When an endpoint file was written, it is removed on exit so stale metadata is not left behind.

func ServeFromConfig added in v0.4.4

func ServeFromConfig(ctx context.Context, cfg ServeConfig) error

ServeFromConfig starts a backend and blocks until interrupted.

func StopFromConfig added in v0.4.4

func StopFromConfig(ctx context.Context, cfg StopConfig) error

StopFromConfig sends SIGTERM to a spanemuboost serve process and waits for it to exit. The process is identified by StopConfig.PIDFile or lifecycle metadata in StopConfig.EndpointFile. Remote or manually started endpoints without a PID cannot be stopped through this API.

Types

type AttachedRuntime added in v0.4.4

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

AttachedRuntime is a Runtime connected to an already-running backend. AttachedRuntime.Close does not stop the remote process or container.

func NewAttachedRuntime added in v0.4.4

func NewAttachedRuntime(endpoint Endpoint, options ...Option) (*AttachedRuntime, error)

NewAttachedRuntime connects to endpoint without starting a container.

func NewAttachedRuntimeFromEnv added in v0.4.4

func NewAttachedRuntimeFromEnv(options ...Option) (*AttachedRuntime, error)

NewAttachedRuntimeFromEnv is a convenience wrapper around LoadEndpoint and NewAttachedRuntime.

func (*AttachedRuntime) ClientOptions added in v0.4.4

func (a *AttachedRuntime) ClientOptions() []option.ClientOption

ClientOptions returns transport options for the attached backend. Options passed via WithClientOptionsForClient are applied in OpenClients and SetupClients, not here.

func (*AttachedRuntime) Close added in v0.4.4

func (a *AttachedRuntime) Close() error

Close is a no-op for attached runtimes because this handle does not own the remote backend lifecycle.

func (*AttachedRuntime) DatabaseID added in v0.4.4

func (a *AttachedRuntime) DatabaseID() string

func (*AttachedRuntime) DatabasePath added in v0.4.4

func (a *AttachedRuntime) DatabasePath() string

func (*AttachedRuntime) InstanceID added in v0.4.4

func (a *AttachedRuntime) InstanceID() string

func (*AttachedRuntime) InstancePath added in v0.4.4

func (a *AttachedRuntime) InstancePath() string

func (*AttachedRuntime) ProjectID added in v0.4.4

func (a *AttachedRuntime) ProjectID() string

func (*AttachedRuntime) ProjectPath added in v0.4.4

func (a *AttachedRuntime) ProjectPath() string

func (*AttachedRuntime) URI added in v0.4.4

func (a *AttachedRuntime) URI() string

type Backend added in v0.4.0

type Backend string

Backend identifies the runtime implementation to start. Callers should use the exported Backend* constants; other values are rejected.

const (
	// BackendEmulator starts the Cloud Spanner Emulator backend.
	BackendEmulator Backend = "emulator"
	// BackendOmni starts the experimental Spanner Omni backend.
	// Backend-specific behavior for Omni may change before v1.
	//
	// Each started Omni runtime owns one Spanner Omni container. Plan for roughly
	// 4 GiB of memory per concurrently running Omni container, and keep tests
	// that start Omni runtimes serial unless the host has enough spare memory.
	// spanemuboost does not serialize Omni runtime lifetimes globally because
	// such a lock would be process-local and surprising for callers that
	// intentionally provision multiple independent runtimes.
	//
	// Use [RecommendedOmniClientConfig] for external Go clients.
	BackendOmni Backend = "omni"
)

type Clients

type Clients struct {
	// InstanceClient enables instance-level administrative operations.
	// For Omni, instance lifecycle operations are backend-limited and may fail.
	InstanceClient *instance.InstanceAdminClient
	DatabaseClient *database.DatabaseAdminClient
	Client         *spanner.Client

	ProjectID, InstanceID, DatabaseID string
	// contains filtered or unexported fields
}

Clients holds Spanner clients and manages the lifecycle of schema resources (instances and databases) auto-created during bootstrap.

By default, auto-created resources with fixed IDs are dropped on Clients.Close, while resources with random IDs are not (since they never collide). Use ForceSchemaTeardown or SkipSchemaTeardown to override.

For RunEmulatorWithClients/SetupEmulatorWithClients, teardown is disabled because the emulator container owns the resource lifecycle; use ForceSchemaTeardown to override.

func NewClients deprecated added in v0.2.0

func NewClients(ctx context.Context, emulator *tcspanner.Container, options ...Option) (clients *Clients, teardown func(), err error)

Deprecated: Use SetupClients (for tests) or OpenClients instead.

NewClients setup existing Cloud Spanner Emulator with Spanner clients. The clients will be closed when teardown is called. You should call it.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	emulator, emulatorTeardown, err := spanemuboost.NewEmulator(ctx,
		spanemuboost.EnableInstanceAutoConfigOnly(),
	)
	if err != nil {
		log.Fatalln(err)
		return
	}

	defer emulatorTeardown()

	var pks []int64
	for i := range 10 {
		func() {
			clients, clientsTeardown, err := spanemuboost.NewClients(ctx, emulator,
				spanemuboost.EnableDatabaseAutoConfigOnly(),
				spanemuboost.WithRandomDatabaseID(),
				spanemuboost.WithSetupDDLs([]string{"CREATE TABLE tbl (PK INT64 PRIMARY KEY)"}),
				spanemuboost.WithSetupDMLs([]spanner.Statement{
					{SQL: "INSERT INTO tbl(PK) VALUES(@i)", Params: map[string]any{"i": i}},
				}),
			)
			if err != nil {
				log.Fatalln(err)
				return
			}

			defer clientsTeardown()

			err = clients.Client.Single().Query(ctx, spanner.NewStatement("SELECT PK FROM tbl")).Do(func(r *spanner.Row) error {
				var pk int64
				if err := r.ColumnByName("PK", &pk); err != nil {
					return err
				}
				pks = append(pks, pk)
				return nil
			})
			if err != nil {
				log.Fatalln(err)
			}
		}()
	}

	fmt.Println(pks)
}
Output:
[0 1 2 3 4 5 6 7 8 9]

func NewEmulatorWithClients deprecated

func NewEmulatorWithClients(ctx context.Context, options ...Option) (emulator *tcspanner.Container, clients *Clients, teardown func(), err error)

Deprecated: Use SetupEmulatorWithClients (for tests) or RunEmulatorWithClients instead.

NewEmulatorWithClients initializes Cloud Spanner Emulator with Spanner clients. The emulator and clients will be closed when teardown is called. You should call it.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	_, clients, teardown, err := spanemuboost.NewEmulatorWithClients(ctx)
	if err != nil {
		log.Fatalln(err)
		return
	}

	defer teardown()

	err = clients.Client.Single().Query(ctx, spanner.NewStatement("SELECT 1")).Do(func(r *spanner.Row) error {
		fmt.Println(r)
		return nil
	})
	if err != nil {
		log.Fatalln(err)
	}
}
Output:
{fields: [type:{code:INT64}], values: [string_value:"1"]}

func OpenClients added in v0.3.0

func OpenClients(ctx context.Context, runtime RuntimeHandle, options ...Option) (*Clients, error)

OpenClients connects to an existing RuntimeHandle and opens Spanner clients. Supported handles are *Emulator, *LazyRuntime, *LazyEmulator, and the Runtime returned by Run or Setup. When a lazy runtime is passed, it is started automatically on first use. The parameter type is intentionally limited to package-provided runtime values so callers can use lazy runtime handles without adding another startup method to the public Runtime interface. Options inherit the runtime's projectID, instanceID, and databaseID. When reopening against an existing runtime, automatic create and teardown behavior is disabled by default, so clients target the existing instance and database unless explicitly overridden where supported (for example via EnableAutoConfig). Call Clients.Close to close the clients when done. In tests, prefer SetupClients which handles cleanup automatically.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	emu, err := spanemuboost.RunEmulator(ctx,
		spanemuboost.EnableInstanceAutoConfigOnly(),
	)
	if err != nil {
		log.Fatalln(err)
		return
	}

	defer emu.Close() //nolint:errcheck

	var pks []int64
	for i := range 10 {
		func() {
			clients, err := spanemuboost.OpenClients(ctx, emu,
				spanemuboost.WithRandomDatabaseID(),
				spanemuboost.WithSetupDDLs([]string{"CREATE TABLE tbl (PK INT64 PRIMARY KEY)"}),
				spanemuboost.WithSetupDMLs([]spanner.Statement{
					{SQL: "INSERT INTO tbl(PK) VALUES(@i)", Params: map[string]any{"i": i}},
				}),
			)
			if err != nil {
				log.Fatalln(err)
				return
			}

			defer clients.Close() //nolint:errcheck

			err = clients.Client.Single().Query(ctx, spanner.NewStatement("SELECT PK FROM tbl")).Do(func(r *spanner.Row) error {
				var pk int64
				if err := r.ColumnByName("PK", &pk); err != nil {
					return err
				}
				pks = append(pks, pk)
				return nil
			})
			if err != nil {
				log.Fatalln(err)
			}
		}()
	}

	fmt.Println(pks)
}
Output:
[0 1 2 3 4 5 6 7 8 9]

func SetupClients added in v0.3.0

func SetupClients(tb testing.TB, runtime RuntimeHandle, options ...Option) *Clients

SetupClients opens Spanner clients against an existing RuntimeHandle and registers cleanup via testing.TB.Cleanup. It calls testing.TB.Fatal on setup error. Supported handles are *Emulator, *LazyRuntime, *LazyEmulator, and the Runtime returned by Run or Setup. When a lazy runtime is passed, it is started automatically on first use. The parameter type is intentionally limited to package-provided runtime values so callers can use lazy runtime handles without adding another startup method to the public Runtime interface. Options inherit the runtime's projectID, instanceID, and databaseID. Use OpenClients if you need a context.Context or are not in a test.

func (*Clients) ClientOptions added in v0.3.3

func (c *Clients) ClientOptions() []option.ClientOption

ClientOptions returns the option.ClientOption values used to connect to the emulator. This is useful when callers need to create additional gRPC clients (e.g., with custom interceptors) against the same emulator without holding a separate *Emulator reference.

func (*Clients) Close added in v0.3.0

func (c *Clients) Close() error

Close closes all Spanner clients. By default, auto-created resources with fixed IDs are dropped during Close after the data client is closed and before the admin clients are closed. See ForceSchemaTeardown and SkipSchemaTeardown. spanner.Client.Close does not return an error, so only admin client and resource cleanup errors are returned. Close is nil-safe and idempotent. After the first call, subsequent calls return the result of that first call.

func (*Clients) DatabasePath added in v0.2.2

func (c *Clients) DatabasePath() string

func (*Clients) InstancePath added in v0.2.2

func (c *Clients) InstancePath() string

func (*Clients) ProjectPath added in v0.2.2

func (c *Clients) ProjectPath() string

func (*Clients) URI added in v0.3.3

func (c *Clients) URI() string

URI returns the gRPC endpoint (host:port) of the emulator this Clients is connected to, suitable for use as SPANNER_EMULATOR_HOST.

type Emulator added in v0.3.0

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

Emulator wraps a Cloud Spanner Emulator container. Use RunEmulator or SetupEmulator to create one.

func RunEmulator added in v0.3.0

func RunEmulator(ctx context.Context, options ...Option) (*Emulator, error)

RunEmulator starts a Cloud Spanner Emulator container and performs any configured bootstrap (instance/database creation, DDL, DML). Call Emulator.Close to terminate the container when done. In tests, prefer SetupEmulator which handles cleanup automatically. In TestMain, use this function since testing.M does not implement testing.TB.

func SetupEmulator added in v0.3.0

func SetupEmulator(tb testing.TB, options ...Option) *Emulator

SetupEmulator starts a Cloud Spanner Emulator and registers cleanup via testing.TB.Cleanup. It calls testing.TB.Fatal on setup error. Use RunEmulator if you need a context.Context or are not in a test. Note that testing.M does not implement testing.TB, so use RunEmulator in TestMain.

func (*Emulator) ClientOptions added in v0.3.0

func (e *Emulator) ClientOptions() []option.ClientOption

ClientOptions returns option.ClientOption values configured for connecting to this emulator (endpoint, insecure credentials, no authentication).

The endpoint uses the passthrough:/// scheme to bypass gRPC name resolution and avoid the slow authentication code path that would otherwise be triggered when grpc.NewClient (dns resolver by default) is used by the auth layer. This mirrors the approach used by the Spanner client library's SPANNER_EMULATOR_HOST handling (googleapis/google-cloud-go#10947), as well as the Bigtable and Datastore SDKs for their emulator paths.

Currently the auth layer uses grpc.DialContext (passthrough by default), so this is a defensive measure for the planned migration to grpc.NewClient.

func (*Emulator) Close added in v0.3.0

func (e *Emulator) Close() error

Close terminates the emulator container. Close is nil-safe and idempotent. After the first call, subsequent calls return the result of that first call.

func (*Emulator) Container added in v0.3.0

func (e *Emulator) Container() *tcspanner.Container

Container returns the underlying *tcspanner.Container for direct access. Most users should use Emulator.URI or Emulator.ClientOptions instead.

func (*Emulator) DatabaseID added in v0.3.0

func (e *Emulator) DatabaseID() string

DatabaseID returns the database ID configured for this emulator.

func (*Emulator) DatabasePath added in v0.3.0

func (e *Emulator) DatabasePath() string

DatabasePath returns the database resource path.

func (*Emulator) InstanceID added in v0.3.0

func (e *Emulator) InstanceID() string

InstanceID returns the instance ID configured for this emulator.

func (*Emulator) InstancePath added in v0.3.0

func (e *Emulator) InstancePath() string

InstancePath returns the instance resource path.

func (*Emulator) ProjectID added in v0.3.0

func (e *Emulator) ProjectID() string

ProjectID returns the project ID configured for this emulator.

func (*Emulator) ProjectPath added in v0.3.0

func (e *Emulator) ProjectPath() string

ProjectPath returns the project resource path.

func (*Emulator) TestMain added in v0.3.4

func (e *Emulator) TestMain(m *testing.M)

TestMain runs m.Run(), closes the emulator, and calls os.Exit with the appropriate code. A close failure is logged and causes a non-zero exit code.

Because TestMain calls os.Exit, it must be the last statement in your TestMain function. If you need additional cleanup, refer to the source of this method and write the logic manually.

Usage in TestMain:

func TestMain(m *testing.M) {
    var err error
    emulator, err = spanemuboost.RunEmulator(context.Background(),
        spanemuboost.EnableInstanceAutoConfigOnly(),
    )
    if err != nil { log.Fatal(err) }
    emulator.TestMain(m)
}

func (*Emulator) URI added in v0.3.0

func (e *Emulator) URI() string

URI returns the gRPC endpoint (host:port) of the emulator, suitable for use as SPANNER_EMULATOR_HOST.

In serial tests, you can use testing.T.Setenv to set the environment variable:

t.Setenv("SPANNER_EMULATOR_HOST", emu.URI())

Note that testing.T.Setenv panics if the test or an ancestor has called testing.T.Parallel. Prefer Emulator.ClientOptions when possible.

type Endpoint added in v0.4.4

type Endpoint struct {
	Backend    Backend `json:"backend"`
	URI        string  `json:"uri"`
	ProjectID  string  `json:"project_id"`
	InstanceID string  `json:"instance_id"`

	// Lifecycle metadata is populated by spanemuboost serve and used by stop.
	ManagedBy string `json:"managed_by,omitempty"`
	PID       int    `json:"pid,omitempty"`
	StartedAt string `json:"started_at,omitempty"`
}

Endpoint describes a running spanemuboost-compatible Spanner backend that clients can attach to without starting a new container.

func EndpointFromRuntime added in v0.4.4

func EndpointFromRuntime(runtime Runtime) (Endpoint, error)

EndpointFromRuntime builds an Endpoint from a started Runtime.

func LoadEndpoint added in v0.4.4

func LoadEndpoint() (Endpoint, error)

LoadEndpoint reads connection metadata from SPANEMUBOOST_ENDPOINT_FILE or backend-specific URI env vars.

When SPANEMUBOOST_ENDPOINT_FILE is set, the JSON file takes precedence. Otherwise Omni is selected when SPANEMUBOOST_OMNI_URI is set, and the emulator path is selected when SPANEMUBOOST_EMULATOR_URI is set.

func LoadEndpointForBackend added in v0.4.4

func LoadEndpointForBackend(backend Backend) (Endpoint, error)

LoadEndpointForBackend reads endpoint metadata for the requested backend. When SPANEMUBOOST_ENDPOINT_FILE is set, the file backend must match. Otherwise only the URI env var for the requested backend is considered.

func ReadEndpointFile added in v0.4.4

func ReadEndpointFile(path string) (Endpoint, error)

ReadEndpointFile loads an Endpoint from a JSON file written by SaveEndpoint or `spanemuboost serve`.

type Env added in v0.3.0

type Env struct {
	*Clients
	// contains filtered or unexported fields
}

Env combines an Emulator with Clients for the single-call use case. Use RunEmulatorWithClients or SetupEmulatorWithClients to create one.

func RunEmulatorWithClients added in v0.3.0

func RunEmulatorWithClients(ctx context.Context, options ...Option) (*Env, error)

RunEmulatorWithClients starts a Cloud Spanner Emulator and opens Spanner clients. Call Env.Close to close clients and terminate the container. In tests, prefer SetupEmulatorWithClients which handles cleanup automatically.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	env, err := spanemuboost.RunEmulatorWithClients(ctx)
	if err != nil {
		log.Fatalln(err)
		return
	}

	defer env.Close() //nolint:errcheck

	err = env.Client.Single().Query(ctx, spanner.NewStatement("SELECT 1")).Do(func(r *spanner.Row) error {
		fmt.Println(r)
		return nil
	})
	if err != nil {
		log.Fatalln(err)
	}
}
Output:
{fields: [type:{code:INT64}], values: [string_value:"1"]}

func SetupEmulatorWithClients added in v0.3.0

func SetupEmulatorWithClients(tb testing.TB, options ...Option) *Env

SetupEmulatorWithClients starts a Cloud Spanner Emulator with clients and registers cleanup via testing.TB.Cleanup. It calls testing.TB.Fatal on setup error. Use RunEmulatorWithClients if you need a context.Context or are not in a test.

func (*Env) Close added in v0.3.0

func (e *Env) Close() error

Close closes the clients and then terminates the emulator. Close is nil-safe and idempotent. After the first call, subsequent calls return the result of that first call.

func (*Env) Emulator added in v0.3.0

func (e *Env) Emulator() *Emulator

Emulator returns the underlying Emulator.

type LazyEmulator added in v0.3.3

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

LazyEmulator defers emulator startup until first use. Use NewLazyEmulator in a package-level var, then pass directly to SetupClients or OpenClients. Call LazyEmulator.Setup or LazyEmulator.Get for standalone access. LazyEmulator.Close is safe to call even if the emulator was never started (no-op).

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	lazy := spanemuboost.NewLazyEmulator(
		spanemuboost.EnableInstanceAutoConfigOnly(),
	)
	defer func() {
		if err := lazy.Close(); err != nil {
			log.Printf("failed to close lazy emulator: %v", err)
		}
	}()

	// OpenClients accepts a *LazyEmulator directly and starts it on first use.
	clients, err := spanemuboost.OpenClients(ctx, lazy,
		spanemuboost.WithRandomDatabaseID(),
	)
	if err != nil {
		log.Fatalln(err)
		return
	}
	defer func() {
		if err := clients.Close(); err != nil {
			log.Printf("failed to close clients: %v", err)
		}
	}()

	err = clients.Client.Single().Query(ctx, spanner.NewStatement("SELECT 1")).Do(func(r *spanner.Row) error {
		fmt.Println(r)
		return nil
	})
	if err != nil {
		log.Fatalln(err)
	}
}
Output:
{fields: [type:{code:INT64}], values: [string_value:"1"]}

func NewLazyEmulator added in v0.3.3

func NewLazyEmulator(options ...Option) *LazyEmulator

NewLazyEmulator creates a LazyEmulator that will start an emulator with the given options on first use. The emulator is not started until it is passed to SetupClients, OpenClients, or until LazyEmulator.Setup / LazyEmulator.Get is called directly.

func (*LazyEmulator) Close added in v0.3.3

func (le *LazyEmulator) Close() error

Close terminates the emulator if it was started. No-op otherwise. Close is nil-safe and idempotent — subsequent calls return the result of the first call. Close waits for any in-progress initialization to complete before checking. If Close is called before any Get or Setup, the emulator will never be started.

func (*LazyEmulator) Get added in v0.3.3

func (le *LazyEmulator) Get(ctx context.Context) (*Emulator, error)

Get starts the emulator on first call (thread-safe via sync.Once) and returns the cached *Emulator on subsequent calls.

func (*LazyEmulator) Setup added in v0.3.3

func (le *LazyEmulator) Setup(tb testing.TB) *Emulator

Setup starts the emulator on first call (thread-safe via sync.Once) and returns the cached *Emulator on subsequent calls. It calls testing.TB.Fatal if startup fails. For use with SetupClients or OpenClients, you can pass *LazyEmulator directly without calling Setup.

func (*LazyEmulator) TestMain added in v0.3.4

func (le *LazyEmulator) TestMain(m *testing.M)

TestMain runs m.Run(), closes the lazy emulator, and calls os.Exit with the appropriate code. A close failure is logged and causes a non-zero exit code. If the emulator was never started, Close is a no-op.

Because TestMain calls os.Exit, it must be the last statement in your TestMain function. If you need additional cleanup, refer to the source of this method and write the logic manually.

Usage in TestMain:

var lazyEmu = spanemuboost.NewLazyEmulator(spanemuboost.EnableInstanceAutoConfigOnly())

func TestMain(m *testing.M) { lazyEmu.TestMain(m) }

type LazyRuntime added in v0.4.0

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

LazyRuntime defers startup of the selected backend until first use. Use NewLazyRuntime in a package-level var, then pass it directly to SetupClients or OpenClients. Call LazyRuntime.Setup or LazyRuntime.Get for standalone access, and pair it with LazyRuntime.Close or LazyRuntime.TestMain when you need the lazy handle to own lifecycle cleanup. LazyRuntime.Close is safe to call even if the runtime was never started (no-op).

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/spanner"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()

	lazy := spanemuboost.NewLazyRuntime(
		spanemuboost.BackendEmulator,
		spanemuboost.EnableInstanceAutoConfigOnly(),
	)
	defer func() {
		if err := lazy.Close(); err != nil {
			log.Printf("failed to close lazy runtime: %v", err)
		}
	}()

	// OpenClients accepts a *LazyRuntime directly and starts it on first use.
	clients, err := spanemuboost.OpenClients(ctx, lazy,
		spanemuboost.WithRandomDatabaseID(),
	)
	if err != nil {
		log.Fatalln(err)
		return
	}
	defer func() {
		if err := clients.Close(); err != nil {
			log.Printf("failed to close clients: %v", err)
		}
	}()

	err = clients.Client.Single().Query(ctx, spanner.NewStatement("SELECT 1")).Do(func(r *spanner.Row) error {
		fmt.Println(r)
		return nil
	})
	if err != nil {
		log.Fatalln(err)
	}
}
Output:
{fields: [type:{code:INT64}], values: [string_value:"1"]}

func NewLazyRuntime added in v0.4.0

func NewLazyRuntime(backend Backend, options ...Option) *LazyRuntime

NewLazyRuntime creates a LazyRuntime that will start the selected backend with the given options on first use.

Example (ValidationCases)
package main

import (
	"context"
	"log"

	"cloud.google.com/go/spanner"
	"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"

	"github.com/apstndb/spanemuboost"
)

func main() {
	ctx := context.Background()
	// LazyRuntime shares one backend container across all cases. The same
	// database-per-case loop works with BackendOmni; omit
	// EnableInstanceAutoConfigOnly there because Omni uses its built-in instance.
	lazy := spanemuboost.NewLazyRuntime(
		spanemuboost.BackendEmulator,
		spanemuboost.EnableInstanceAutoConfigOnly(),
	)
	defer func() {
		if err := lazy.Close(); err != nil {
			log.Printf("failed to close runtime: %v", err)
		}
	}()

	baseDDLs := []string{
		"CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)",
	}
	baseDMLs := []string{
		"INSERT INTO Singers (SingerId, Name) VALUES (1, 'Marc Richards')",
	}
	cases := []struct {
		name      string
		setupDDLs []string
		ddl       string
		dml       spanner.Statement
		query     string
	}{
		{
			name: "ddl",
			ddl:  "CREATE TABLE Albums (SingerId INT64, AlbumId INT64) PRIMARY KEY (SingerId, AlbumId)",
		},
		{
			name: "dml",
			setupDDLs: []string{
				"CREATE TABLE Albums (SingerId INT64, AlbumId INT64, Title STRING(MAX)) PRIMARY KEY (SingerId, AlbumId)",
			},
			dml: spanner.NewStatement("INSERT INTO Albums (SingerId, AlbumId, Title) VALUES (1, 1, 'Total Junk')"),
		},
		{
			name:  "query",
			query: "SELECT Name FROM Singers WHERE SingerId = 1",
		},
	}

	for _, tc := range cases {
		// Setup options replace previous values, so compose shared setup and
		// case-specific setup before calling WithSetupDDLs.
		setupDDLs := append([]string{}, baseDDLs...)
		setupDDLs = append(setupDDLs, tc.setupDDLs...)

		// WithRandomDatabaseID isolates cases without starting another backend
		// container. For Omni, random project and instance IDs are not the
		// normal isolation mechanism.
		clients, err := spanemuboost.OpenClients(ctx, lazy,
			spanemuboost.WithRandomDatabaseID(),
			spanemuboost.WithSetupDDLs(setupDDLs),
			spanemuboost.WithSetupRawDMLs(baseDMLs),
		)
		if err != nil {
			log.Printf("%s: SETUP_INVALID: %v", tc.name, err)
			continue
		}

		func() {
			defer func() {
				if err := clients.Close(); err != nil {
					log.Printf("%s: close clients: %v", tc.name, err)
				}
			}()

			var err error
			switch {
			case tc.ddl != "":
				// Candidate DDL is executed after setup so setup failures and
				// candidate failures can be reported separately.
				op, opErr := clients.DatabaseClient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
					Database:   clients.DatabasePath(),
					Statements: []string{tc.ddl},
				})
				if opErr != nil {
					err = opErr
				} else {
					err = op.Wait(ctx)
				}
			case tc.dml.SQL != "":
				_, err = clients.Client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
					_, err := txn.Update(ctx, tc.dml)
					return err
				})
			case tc.query != "":
				iter := clients.Client.Single().Query(ctx, spanner.NewStatement(tc.query))
				err = iter.Do(func(*spanner.Row) error { return nil })
			}
			if err != nil {
				log.Printf("%s: INVALID: %v", tc.name, err)
				return
			}
			log.Printf("%s: VALID", tc.name)
		}()
	}
}

func NewLazyRuntimeFromEnvOrStart added in v0.4.4

func NewLazyRuntimeFromEnvOrStart(backend Backend, options ...Option) (*LazyRuntime, error)

NewLazyRuntimeFromEnvOrStart returns a LazyRuntime that attaches to an external endpoint when one is configured for the requested backend. When no matching endpoint env vars are set, it defers container startup until first use. When endpoint env vars are set but LoadEndpointForBackend fails, it returns an error instead of falling back to cold start.

Options passed to the constructor apply to both cold-start and attach paths. Database bootstrap options such as WithRandomDatabaseID and WithSetupDDLs are honored when OpenClients or SetupClients runs against the lazy handle.

func (*LazyRuntime) Close added in v0.4.0

func (lr *LazyRuntime) Close() error

Close terminates the runtime if it was started. No-op otherwise. Close is nil-safe and idempotent — subsequent calls return the result of the first call. Close waits for any in-progress initialization to complete before checking. If Close is called before any Get or Setup, the runtime will never be started.

func (*LazyRuntime) Get added in v0.4.0

func (lr *LazyRuntime) Get(ctx context.Context) (Runtime, error)

Get starts the selected backend on first call (thread-safe via sync.Once) and returns the cached Runtime on subsequent calls.

func (*LazyRuntime) Setup added in v0.4.0

func (lr *LazyRuntime) Setup(tb testing.TB) Runtime

Setup starts the selected backend on first call (thread-safe via sync.Once) and returns the cached Runtime on subsequent calls. It calls testing.TB.Fatal if startup fails. For use with SetupClients or OpenClients, you can pass *LazyRuntime directly without calling Setup.

func (*LazyRuntime) TestMain added in v0.4.0

func (lr *LazyRuntime) TestMain(m *testing.M)

TestMain runs m.Run(), closes the lazy runtime, and calls os.Exit with the appropriate code. A close failure is logged and causes a non-zero exit code. If the runtime was never started, Close is a no-op.

Because TestMain calls os.Exit, it must be the last statement in your TestMain function. If you need additional cleanup, refer to the source of this method and write the logic manually.

Usage in TestMain:

var lazy = spanemuboost.NewLazyRuntime(
    spanemuboost.BackendEmulator,
    spanemuboost.EnableInstanceAutoConfigOnly(),
)

func TestMain(m *testing.M) { lazy.TestMain(m) }

type Option

type Option func(*emulatorOptions) error

Option configures spanemuboost runtime bootstrap behavior. Use the package-provided With*, Without*, Enable*, Disable*, Force*, and Skip* helpers; external Option implementations are not supported.

func DisableAutoConfig

func DisableAutoConfig() Option

DisableAutoConfig disables auto config.(default enable)

func DisableBackendGuardrails added in v0.4.0

func DisableBackendGuardrails() Option

DisableBackendGuardrails disables backend-specific validation and coercion.

By default, spanemuboost rejects known-invalid backend configurations early with human-readable errors. Use this option only when trying a newer backend version whose constraints may have changed.

func DisableQueryNullFilteredIndexCheck added in v0.4.2

func DisableQueryNullFilteredIndexCheck() Option

DisableQueryNullFilteredIndexCheck disables the emulator's safeguard that rejects queries against NULL_FILTERED indexes (the emulator's --disable_query_null_filtered_index_check flag). Emulator-only.

Production Spanner answers such queries; the emulator rejects them by default because the result set can legitimately differ from a base-table scan. Use this option only in tests that intentionally exercise reads against NULL_FILTERED indexes and have accounted for that difference.

func EnableAutoConfig

func EnableAutoConfig() Option

EnableAutoConfig enables auto config.(default enable)

func EnableDatabaseAutoConfigOnly added in v0.2.0

func EnableDatabaseAutoConfigOnly() Option

EnableDatabaseAutoConfigOnly enables only database auto-creation and keeps instance auto-creation disabled.

func EnableEmulatorStdoutCopy added in v0.4.2

func EnableEmulatorStdoutCopy() Option

EnableEmulatorStdoutCopy enables copying the emulator backend's stdout to the gateway's stdout (the emulator's --copy_emulator_stdout flag). The gateway already copies the backend's stderr by default; this option adds the matching stdout stream for debugging. Emulator-only.

func EnableFaultInjection added in v0.2.5

func EnableFaultInjection() Option

EnableFaultInjection enables fault injection of Cloud Spanner Emulator (the emulator's --enable_fault_injection flag). Emulator-only.

func EnableInstanceAutoConfigOnly added in v0.2.0

func EnableInstanceAutoConfigOnly() Option

EnableInstanceAutoConfigOnly enables only instance auto-creation and keeps database auto-creation disabled.

func EnableLogRequests added in v0.4.2

func EnableLogRequests() Option

EnableLogRequests enables gRPC request and response logging in the emulator gateway (the emulator's --log_requests flag). Useful when debugging test failures; output is written to the container's stdout. Emulator-only.

func ForceSchemaTeardown added in v0.3.2

func ForceSchemaTeardown() Option

ForceSchemaTeardown forces schema resource cleanup on Clients.Close, dropping any auto-created database or instance before closing the Go clients.

By default, schema teardown is enabled for fixed IDs and disabled for random IDs. This option overrides that default for all resources.

func SkipSchemaTeardown added in v0.3.2

func SkipSchemaTeardown() Option

SkipSchemaTeardown disables schema resource cleanup on Clients.Close. Auto-created databases and instances will not be dropped on close.

By default, schema teardown is enabled for fixed IDs; use this option to opt out.

func WithChangeStreamPartitionTokenAliveSeconds added in v0.4.2

func WithChangeStreamPartitionTokenAliveSeconds(seconds int) Option

WithChangeStreamPartitionTokenAliveSeconds overrides the alive time of change stream partition tokens (the emulator's --override_change_stream_partition_token_alive_seconds flag). seconds must be positive. Emulator-only.

Per the upstream help text, the effective alive time becomes seconds..2*seconds (the emulator's default is 20..40s, which differs from production Spanner). This flag is emulator-only and has no effect on production Spanner.

func WithClientConfig added in v0.2.0

func WithClientConfig(config spanner.ClientConfig) Option

WithClientConfig sets spanner.ClientConfig for managed data clients created by spanemuboost, including OpenClients, RunWithClients, and SetupWithClients.

When this option is not used, spanemuboost sets DisableNativeMetrics to true by default, since the Spanner native metrics infrastructure is unnecessary for emulator connections and can add overhead (metadata server lookups, monitoring exporter creation).

For Omni managed clients with backend guardrails enabled, spanemuboost applies the recommended Omni defaults from RecommendedOmniClientConfig, including DisableNativeMetrics and IsExperimentalHost, overriding those two fields even when they were set explicitly in the provided config. DisableBackendGuardrails keeps the provided config untouched. RecommendedOmniClientConfig remains the recommended base for external Go clients.

func WithClientOptionsForClient added in v0.2.5

func WithClientOptionsForClient(option ...option.ClientOption) Option

WithClientOptionsForClient configures ClientOption for Clients.Client.

func WithContainerCustomizers added in v0.2.5

func WithContainerCustomizers(containerCustomizers ...testcontainers.ContainerCustomizer) Option

WithContainerCustomizers adds low-level testcontainers customizers to backend runtime containers.

Prefer WithContainerProvider instead of passing testcontainers.WithProvider directly when selecting Docker or Podman. If a customizer does set the provider, it is applied after SPANEMUBOOST_TESTCONTAINERS_PROVIDER and can override that environment default.

func WithContainerImage added in v0.4.0

func WithContainerImage(image string) Option

WithContainerImage configures the container image used for the selected backend. Empty string will be ignored.

func WithContainerProvider added in v0.4.3

func WithContainerProvider(provider testcontainers.ProviderType) Option

WithContainerProvider configures the testcontainers provider used to start backend runtime containers.

Use testcontainers.ProviderPodman when running against Podman and Testcontainers-Go cannot auto-detect Podman from DOCKER_HOST. This is common with macOS Podman machine forwarded sockets whose host path does not contain "podman.sock".

This option overrides SPANEMUBOOST_TESTCONTAINERS_PROVIDER. If several provider customizers are supplied explicitly, the last one applied to the Testcontainers request wins.

func WithDatabaseDialect

func WithDatabaseDialect(dialect databasepb.DatabaseDialect) Option

WithDatabaseDialect configures the database dialect.

func WithDatabaseID

func WithDatabaseID(databaseID string) Option

WithDatabaseID configures the database ID. Empty string resets to default.

func WithEmulatorImage deprecated

func WithEmulatorImage(image string) Option

Deprecated: WithEmulatorImage is a deprecated alias for WithContainerImage. Empty string will be ignored.

func WithInstanceID

func WithInstanceID(instanceID string) Option

WithInstanceID configures the instance ID. Empty string resets to default.

func WithMaxDatabasesPerInstance added in v0.4.2

func WithMaxDatabasesPerInstance(n int) Option

WithMaxDatabasesPerInstance overrides the emulator's maximum number of databases per instance (the emulator's --override_max_databases_per_instance flag). n must be positive. Emulator-only.

Per the upstream help text, the emulator only honors values greater than Spanner's default limit (100); smaller values are ignored. If the MAX_DATABASES_PER_INSTANCE environment variable is also set on the container, it takes precedence over this flag.

func WithProjectID

func WithProjectID(projectID string) Option

WithProjectID configures the project ID. Empty string resets to default.

func WithRandomDatabaseID added in v0.2.3

func WithRandomDatabaseID() Option

WithRandomDatabaseID enables the random database ID. Default is disabled. This clears any previously set database ID (including inherited values from OpenClients).

Because a random ID will never match an existing database, this option also enables database auto-creation (sets disableCreateDatabase to false). To disable creation again, call DisableAutoConfig after this option.

func WithRandomInstanceID added in v0.2.4

func WithRandomInstanceID() Option

WithRandomInstanceID enables the random instance ID. Default is disabled. This clears any previously set instance ID (including inherited values from OpenClients).

Because a random ID will never match an existing instance, this option also enables instance auto-creation (sets disableCreateInstance to false). To disable creation again, call DisableAutoConfig after this option.

func WithRandomProjectID added in v0.2.4

func WithRandomProjectID() Option

WithRandomProjectID enables the random project ID. Default is disabled. This clears any previously set project ID (including inherited values from OpenClients).

func WithSetupDDLs

func WithSetupDDLs(ddls []string) Option

WithSetupDDLs sets DDLs to be executed. Calling this multiple times replaces the previous value.

func WithSetupDMLs

func WithSetupDMLs(dmls []spanner.Statement) Option

WithSetupDMLs sets DMLs in spanner.Statement to be executed. Calling this multiple times replaces the previous value. This is mutually exclusive with WithSetupRawDMLs; the last one called wins.

func WithSetupRawDMLs

func WithSetupRawDMLs(rawDMLs []string) Option

WithSetupRawDMLs sets string DMLs to be executed. Calling this multiple times replaces the previous value. This is mutually exclusive with WithSetupDMLs; the last one called wins.

func WithStrictTeardown deprecated added in v0.3.0

func WithStrictTeardown() Option

Deprecated: Use ForceSchemaTeardown instead.

func WithoutRandomDatabaseID added in v0.2.4

func WithoutRandomDatabaseID() Option

WithoutRandomDatabaseID disables the random database ID. Default is disabled.

func WithoutRandomInstanceID added in v0.2.4

func WithoutRandomInstanceID() Option

WithoutRandomInstanceID disables the random instance ID. Default is disabled.

func WithoutRandomProjectID added in v0.2.4

func WithoutRandomProjectID() Option

WithoutRandomProjectID disables the random project ID. Default is disabled.

type Runtime added in v0.4.0

type Runtime interface {
	RuntimeHandle
	URI() string
	ClientOptions() []option.ClientOption
	Close() error
	ProjectID() string
	InstanceID() string
	DatabaseID() string
	ProjectPath() string
	InstancePath() string
	DatabasePath() string
}

Runtime is a started backend-neutral Spanner-compatible test runtime returned by Run or Setup.

This backend-neutral API surface is intended to remain the primary public entry point. Backend-specific behavior may evolve independently, especially for the experimental BackendOmni backend.

Implementations are provided by this package.

func Run added in v0.4.0

func Run(ctx context.Context, backend Backend, options ...Option) (Runtime, error)

Run starts the selected backend and returns it as a backend-neutral runtime. When backend is BackendOmni, backend-specific behavior remains experimental and each runtime owns a Spanner Omni container. Avoid concurrent Omni runtime startup unless the host has enough memory for about 4 GiB per container.

func Setup added in v0.4.0

func Setup(tb testing.TB, backend Backend, options ...Option) Runtime

Setup starts the selected backend and registers cleanup with testing.TB.Cleanup. When backend is BackendOmni, backend-specific behavior remains experimental and each runtime owns a Spanner Omni container. Avoid combining Omni startup with t.Parallel unless the host has enough memory for about 4 GiB per container, or share a single runtime with NewLazyRuntime.

type RuntimeEnv added in v0.4.0

type RuntimeEnv struct {
	*Clients
	// contains filtered or unexported fields
}

RuntimeEnv combines a Runtime with Clients for backend-neutral startup. When created with BackendOmni, backend-specific behavior remains experimental.

func RunWithClients added in v0.4.0

func RunWithClients(ctx context.Context, backend Backend, options ...Option) (*RuntimeEnv, error)

RunWithClients starts the selected backend and returns managed clients. When backend is BackendOmni, backend-specific behavior remains experimental and each runtime owns a Spanner Omni container. Avoid concurrent Omni runtime startup unless the host has enough memory for about 4 GiB per container.

func SetupWithClients added in v0.4.0

func SetupWithClients(tb testing.TB, backend Backend, options ...Option) *RuntimeEnv

SetupWithClients starts the selected backend with managed clients and registers cleanup with testing.TB.Cleanup. When backend is BackendOmni, backend-specific behavior remains experimental and each runtime owns a Spanner Omni container. Avoid combining Omni startup with t.Parallel unless the host has enough memory for about 4 GiB per container, or share a single runtime with NewLazyRuntime.

func (*RuntimeEnv) Close added in v0.4.0

func (e *RuntimeEnv) Close() error

Close closes the clients and then terminates the runtime. Close is nil-safe and idempotent. After the first call, subsequent calls return the result of that first call.

func (*RuntimeEnv) Runtime added in v0.4.0

func (e *RuntimeEnv) Runtime() Runtime

Runtime returns the started runtime behind this environment.

type RuntimeHandle added in v0.4.0

type RuntimeHandle interface {
	// contains filtered or unexported methods
}

RuntimeHandle is a package-provided runtime value accepted by OpenClients and SetupClients.

Supported handles are started Runtime values returned by Run or Setup, as well as *Emulator, *LazyRuntime, *LazyEmulator, and *AttachedRuntime. External implementations are not supported.

type ServeConfig added in v0.4.4

type ServeConfig struct {
	Backend      Backend
	EndpointFile string
	PIDFile      string
	Options      []Option
}

ServeConfig configures ServeFromConfig.

func ParseServeArgs added in v0.4.4

func ParseServeArgs(args []string) (ServeConfig, error)

ParseServeArgs parses `spanemuboost serve <emulator|omni> --endpoint-file path [--pid-file path] [--with-default-database]`.

type StopConfig added in v0.4.4

type StopConfig struct {
	EndpointFile string
	PIDFile      string
	Timeout      time.Duration
}

StopConfig configures StopFromConfig.

func ParseStopArgs added in v0.4.4

func ParseStopArgs(args []string) (StopConfig, error)

ParseStopArgs parses `spanemuboost stop --endpoint-file path [--pid-file path]`.

Directories

Path Synopsis
cmd
spanemuboost command

Jump to

Keyboard shortcuts

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