engine

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

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

Go to latest
Published: May 9, 2026 License: MIT Imports: 17 Imported by: 0

README

ApiQube Engine

Declarative, plugin-driven API testing engine. One format. Any protocol. Zero boilerplate.

Go CI License Status

What is this?

engine is the core library behind ApiQube — a declarative API testing platform. It parses test manifests, builds a dependency graph from template references, executes tests through WASM protocol plugins, and emits typed events for any frontend to consume.

This is a library, not a CLI tool. For the CLI, see apiqube/qube.

Key features

  • Protocol-agnostic — HTTP, gRPC, GraphQL, WebSocket and more via WASM plugins.
  • Auto-dependency graph — detects cross-test references in templates at build time.
  • Smart parallelism — groups independent tests into waves, runs them concurrently.
  • Three-level data flowprev (implicit), save (named), alias (cross-file), plus streaming events.
  • Frontend-agnostic — typed event interface for CLI, desktop, web, SDK.
  • Capability-based plugin host — WASM plugins declare host capabilities they need; engine grants them per policy.

Usage

package main

import (
    "context"
    "fmt"

    "github.com/apiqube/engine"
)

func main() {
    eng := engine.New()
    results, err := eng.Run(context.Background(), engine.FromPaths("./tests/"))
    if err != nil {
        panic(err)
    }
    fmt.Printf("%d passed, %d failed\n", results.Passed, results.Failed)
}

Architecture

engine/
├── engine.go          Engine constructor, Run(), Check()
├── input.go           Sealed Input source — FromPaths, FromBytes, FromReader
├── options.go         Engine-level functional options
├── runopts.go         Per-Run options (handler, signals, env, plugins)
├── checkopts.go       Per-Check options
├── events.go          Sealed Event hierarchy + EventHandler interface
├── dispatcher.go      Typed and plugin-event subscription routing
├── results.go         Results, TestResult, WaveResult, ValidationError
├── data.go            RequestData, ResponseData, AssertionResult
├── protocol.go        Open Protocol type
├── status.go          TestStatus, Signal
├── schema.go          PluginSchema, FieldSpec, EventSpec for introspection
├── introspect.go      Engine.Plugins(), EventSchema()
└── internal/
    ├── manifest/      TestFile / TestCase / Expect / RetryConfig / LoadConfig
    ├── config/        .qube.yaml schema and loader
    ├── parser/        YAML loader, three-syntax normalizer
    ├── graph/         Dependency graph builder, topological sort, save requirements
    ├── dataflow/      Runtime store, prev snapshot, channel-based wait, gjson extract
    ├── template/      Template DSL — fake.*, regex(), method chains
    ├── assertion/     Operator engine and type-coercion rules
    ├── plugin/        WASM host adapter (wazero), registry, capability layer
    │   └── capabilities/   log, time, events, http
    └── runner/        Wave execution, per-test pipeline
Repo Description
apiqube/qube CLI tool (imports this engine)
apiqube/plugin-http First-party HTTP plugin
apiqube/cli V1 CLI (archived reference)

License

MIT

Documentation

Overview

Package engine is the core of the ApiQube testing platform.

The engine parses declarative test manifests, builds dependency graphs from template references, resolves templates, executes tests through protocol plugins, and emits events to frontends (CLI, desktop, web, SDK).

Engine is stateless after construction — a single Engine instance is safe for concurrent Run() calls. Each Run() creates an isolated execution context with its own event handler, signals, and data flow store.

Basic Usage

eng := engine.New()
results, err := eng.Run(ctx, engine.FromPaths("./tests/"))
if err != nil {
    return err
}
fmt.Printf("%d passed, %d failed\n", results.Passed, results.Failed)

With Event Handler

type myHandler struct{}

func (myHandler) Handle(event engine.Event) {
    switch e := event.(type) {
    case engine.TestCompleted:
        fmt.Printf("%s: %s\n", e.Name, e.Status)
    }
}

eng := engine.New()
results, err := eng.Run(ctx,
    engine.FromPaths("./tests/"),
    engine.WithHandler(myHandler{}),
)

Input Sources

Tests can come from multiple sources:

engine.FromPaths("./tests/")          // files and directories
engine.FromBytes(yamlBytes)           // raw YAML bytes
engine.FromReader(os.Stdin)           // any io.Reader

Concurrent Runs

A single Engine can handle multiple concurrent Run() calls safely:

eng := engine.New()
go eng.Run(ctx1, engine.FromPaths("./suite-a/"))
go eng.Run(ctx2, engine.FromPaths("./suite-b/"))

Each run has its own isolated EventHandler, Signals channel, and data flow.

Index

Constants

View Source
const (
	ProtocolHTTP    = wire.ProtocolHTTP
	ProtocolHTTPS   = wire.ProtocolHTTPS
	ProtocolGRPC    = wire.ProtocolGRPC
	ProtocolGRPCS   = wire.ProtocolGRPCS
	ProtocolWS      = wire.ProtocolWS
	ProtocolWSS     = wire.ProtocolWSS
	ProtocolGraphQL = wire.ProtocolGraphQL
	ProtocolSQL     = wire.ProtocolSQL
	ProtocolKafka   = wire.ProtocolKafka
	ProtocolAMQP    = wire.ProtocolAMQP
	ProtocolRedis   = wire.ProtocolRedis
)
View Source
const (
	SeverityError   = wire.SeverityError
	SeverityWarning = wire.SeverityWarning
)
View Source
const (
	FieldString = wire.FieldString
	FieldNumber = wire.FieldNumber
	FieldBool   = wire.FieldBool
	FieldObject = wire.FieldObject
	FieldArray  = wire.FieldArray
	FieldMap    = wire.FieldMap
	FieldAny    = wire.FieldAny
)
View Source
const (
	StatusPassed  = wire.StatusPassed
	StatusFailed  = wire.StatusFailed
	StatusSkipped = wire.StatusSkipped
	StatusErrored = wire.StatusErrored
)
View Source
const (
	SignalPause    = wire.SignalPause
	SignalResume   = wire.SignalResume
	SignalSkipTest = wire.SignalSkipTest
)

Variables

View Source
var ErrNoInput = errors.New("engine: no input source provided")

ErrNoInput is returned when Run or Check is called without a valid Input source.

Functions

func Subscribe

func Subscribe[T Event](d *Dispatcher, fn func(T))

Subscribe registers a handler for a core engine event type. T must be one of the sealed Event types declared in this package (RunStarted, TestCompleted, WaveCompleted, Progress, ...).

Multiple handlers can subscribe to the same event type — all of them will be invoked in registration order when the event is dispatched.

Example:

d := engine.NewDispatcher()
engine.Subscribe(d, func(e engine.TestCompleted) {
    fmt.Println(e.Name, e.Status)
})

func SubscribePluginEvent

func SubscribePluginEvent(d *Dispatcher, name string, fn func(PluginEvent))

SubscribePluginEvent registers a raw handler for a plugin event identified by its fully-qualified name ("<plugin>.<kind>", e.g. "grpc.StreamError").

Use this when you want the full PluginEvent including its Plugin, Kind, and raw Data map — for example, to route events dynamically, log them generically, or handle events whose shape you do not want to model as a Go struct.

Example:

engine.SubscribePluginEvent(d, "grpc.StreamError", func(e engine.PluginEvent) {
    log.Printf("%s on stream %v", e.Kind, e.Data["stream_id"])
})

func SubscribePluginTyped

func SubscribePluginTyped[T any](d *Dispatcher, name string, fn func(T))

SubscribePluginTyped registers a typed handler for a plugin event. The consumer defines a Go struct matching the fields declared in the plugin's EventSpec schema; Dispatcher decodes PluginEvent.Data into that struct via JSON round-trip and invokes the handler with the typed value.

Decoding errors are silently dropped — the handler simply does not fire. If you need strict error handling, use SubscribePluginEvent and decode manually, or query the plugin schema via Engine.Plugins() at startup to validate your struct against the declared fields.

Example:

type StreamMessage struct {
    StreamID string `json:"stream_id"`
    Count    int    `json:"count"`
    Bytes    int64  `json:"bytes"`
}

engine.SubscribePluginTyped(d, "grpc.StreamMessageReceived",
    func(e StreamMessage) {
        metrics.Record(e.StreamID, e.Count)
    })

Types

type AssertionResult

type AssertionResult struct {
	Expression string `json:"expression"`
	Passed     bool   `json:"passed"`
	Expected   any    `json:"expected,omitempty"`
	Actual     any    `json:"actual,omitempty"`
	Message    string `json:"message,omitempty"`
}

AssertionResult captures the outcome of a single assertion check.

type CheckOption

type CheckOption func(*checkConfig)

CheckOption configures a single Check() invocation. Check validates manifests without executing them, so it accepts a smaller set of options than Run — only input source, plugins, and config.

func WithCheckConfigPath

func WithCheckConfigPath(path string) CheckOption

WithCheckConfigPath loads a specific .qube.yaml for validation context.

func WithCheckPlugins

func WithCheckPlugins(dir string) CheckOption

WithCheckPlugins sets the plugin directory for this Check, so plugin-specific fields in manifests can be validated.

type ConfigLoaded

type ConfigLoaded struct {
	Path        string   `json:"path"`
	Targets     []string `json:"targets,omitempty"`
	PluginCount int      `json:"plugin_count"`
}

ConfigLoaded is emitted after the .qube.yaml config is parsed.

func (ConfigLoaded) Type

func (ConfigLoaded) Type() string

type Dispatcher

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

Dispatcher routes events to handlers registered by type or by plugin event name. It implements EventHandler, so you can pass it to WithHandler() just like any other handler — frontends that need typed subscriptions use Dispatcher, simpler frontends can implement EventHandler directly with a type switch.

Three subscription styles coexist:

  • Subscribe[T] — typed handler for a core engine event (T must be Event)
  • SubscribePluginEvent — raw handler for a plugin event by fully-qualified name
  • SubscribePluginTyped[T] — decodes a plugin event's Data into a user-defined T

Dispatcher is safe for concurrent use — subscriptions and dispatches can happen from any goroutine.

func NewDispatcher

func NewDispatcher() *Dispatcher

NewDispatcher creates an empty Dispatcher with no registered handlers.

func (*Dispatcher) Handle

func (d *Dispatcher) Handle(event Event)

Handle implements EventHandler. It dispatches the event to all registered handlers matching either its Go type or (for plugin events) its fully-qualified name.

type Engine

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

Engine is the core of the ApiQube testing platform.

Engine is stateless after construction — safe for concurrent Run() calls. Configuration set via Options applies to all runs. Per-run configuration (event handler, signals, env overrides) is passed via RunOptions.

func New

func New(opts ...Option) *Engine

New creates a new Engine with the given Options. Defaults: parallel=true, maxConcurrent=GOMAXPROCS, failFast=false.

func (*Engine) Check

func (e *Engine) Check(ctx context.Context, input Input, opts ...CheckOption) ([]ValidationError, error)

Check validates manifests from the given Input source without executing them.

Returns a list of validation errors (parse errors, unresolved alias references, dependency cycles). An empty slice means everything validated.

func (*Engine) Close

func (e *Engine) Close() error

Close releases plugin resources held by the engine. Safe to call multiple times. The engine cannot be used after Close.

func (*Engine) EventSchema

func (e *Engine) EventSchema(fullyQualifiedName string) *EventSpec

EventSchema looks up the schema for a single plugin event by its fully-qualified name ("<plugin>.<kind>").

Returns nil if no loaded plugin declares an event with that name, or if no plugins have been loaded yet.

func (*Engine) Plugins

func (e *Engine) Plugins() []PluginSchema

Plugins returns the metadata of every plugin currently loaded by this engine.

Frontends use this to introspect plugin capabilities at startup: the list of supported protocols, the host capabilities each plugin requires, the available manifest fields, and the catalog of events a plugin can emit.

Returns an empty slice if no plugins have been loaded yet (i.e. before the first Run or Check).

func (*Engine) Run

func (e *Engine) Run(ctx context.Context, input Input, opts ...RunOption) (*Results, error)

Run executes tests from the given Input source.

Input is mandatory. Additional RunOptions configure the event handler, signals, environment, etc. Run returns a non-nil *Results in all paths; the error is non-nil only on engine-level failures (parse error, dependency cycle, plugin load failure, ctx cancelled). Individual test failures are reported within Results.

type Event

type Event interface {
	Type() string
	// contains filtered or unexported methods
}

Event is a sealed interface — only types in this package can implement it.

type EventHandler

type EventHandler interface {
	Handle(event Event)
}

EventHandler receives events from the engine during execution. Frontends implement this with a type switch to handle events they care about.

type EventSpec

type EventSpec = wire.EventSpec

EventSpec describes a single event that a plugin can emit at runtime.

type FieldSpec

type FieldSpec = wire.FieldSpec

FieldSpec describes one field in a plugin contract — used both for manifest fields (what the plugin reads from test cases) and event fields (what the plugin writes into events it emits).

type FieldType

type FieldType = wire.FieldType

FieldType enumerates the YAML/JSON types a plugin field may hold.

type GraphResolved

type GraphResolved struct {
	TotalWaves       int      `json:"total_waves"`
	Dependencies     int      `json:"dependencies"`
	SaveRequirements int      `json:"save_requirements"`
	Warnings         []string `json:"warnings,omitempty"`
}

GraphResolved is emitted after the dependency graph is built, before execution starts.

func (GraphResolved) Type

func (GraphResolved) Type() string

type Input

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

Input is a source of test manifests for Run and Check. Create instances via FromPaths, FromBytes, or FromReader.

Input is a sealed interface — only this package can implement it.

func FromBytes

func FromBytes(data []byte) Input

FromBytes loads test manifests from raw YAML bytes. Useful for web UIs where tests are authored in-memory.

func FromPaths

func FromPaths(paths ...string) Input

FromPaths loads test manifests from the given file or directory paths. Directories are walked recursively for .yaml and .yml files.

func FromReader

func FromReader(r io.Reader) Input

FromReader loads test manifests from any io.Reader. Useful for piped input or streaming sources.

type NopHandler

type NopHandler struct{}

NopHandler is an EventHandler that discards all events.

func (NopHandler) Handle

func (NopHandler) Handle(Event)

Handle implements EventHandler.

type Option

type Option func(*Engine)

Option configures the Engine (set once, reused across runs).

func WithFailFast

func WithFailFast(enabled bool) Option

func WithMaxConcurrent

func WithMaxConcurrent(n int) Option

func WithParallel

func WithParallel(enabled bool) Option

func WithPluginDir

func WithPluginDir(dir string) Option

type PluginEvent

type PluginEvent struct {
	Plugin string         `json:"plugin"`
	Kind   string         `json:"kind"`
	Data   map[string]any `json:"data,omitempty"`
}

PluginEvent is the universal wrapper for events emitted by plugins.

Plugins (WASM or built-in) declare their events via PluginInfo.Events at load time. When a plugin emits an event, the engine builds a PluginEvent with the plugin name, event kind, and typed payload data.

Frontends subscribe via Dispatcher using SubscribePluginEvent (raw) or SubscribePluginTyped (decoded into a user-defined Go struct).

func (PluginEvent) FullName

func (e PluginEvent) FullName() string

FullName returns the fully-qualified event name "<plugin>.<kind>".

func (PluginEvent) Type

func (PluginEvent) Type() string

type PluginLoaded

type PluginLoaded struct {
	Name      string   `json:"name"`
	Version   string   `json:"version"`
	Protocols []string `json:"protocols"`
}

PluginLoaded is emitted when a plugin is successfully loaded.

func (PluginLoaded) Type

func (PluginLoaded) Type() string

type PluginSchema

type PluginSchema = wire.PluginSchema

PluginSchema is the subset of PluginInfo exposed to consumers of the engine. Frontends use it to introspect loaded plugins — their protocols, the host capabilities they require, the manifest fields they read, and the events they may emit.

type Progress

type Progress struct {
	Completed int `json:"completed"`
	Total     int `json:"total"`
	Wave      int `json:"wave"`
}

Progress is emitted periodically to report execution progress.

func (Progress) Type

func (Progress) Type() string

type Protocol

type Protocol = wire.Protocol

Protocol identifies a target protocol, derived from the target URL scheme or declared explicitly by a plugin.

Engine treats Protocol as an open set — plugins may register new protocols at load time. The constants below cover common cases but are not exhaustive.

type RequestData

type RequestData struct {
	Method   string            `json:"method,omitempty"`
	URL      string            `json:"url,omitempty"`
	Headers  map[string]string `json:"headers,omitempty"`
	Body     any               `json:"body,omitempty"`
	Metadata map[string]any    `json:"metadata,omitempty"`
}

RequestData captures what was sent to the target during a test. Protocol-specific fields go in Metadata.

type ResponseData

type ResponseData struct {
	Status   any               `json:"status"`
	Headers  map[string]string `json:"headers,omitempty"`
	Body     any               `json:"body,omitempty"`
	Duration time.Duration     `json:"duration"`
	Metadata map[string]any    `json:"metadata,omitempty"`
}

ResponseData captures what the target returned. Status is any because HTTP uses int, gRPC uses string, etc.

type Results

type Results struct {
	Total    int           `json:"total"`
	Passed   int           `json:"passed"`
	Failed   int           `json:"failed"`
	Skipped  int           `json:"skipped"`
	Errored  int           `json:"errored"`
	Duration time.Duration `json:"duration"`
	Tests    []TestResult  `json:"tests"`
	Waves    []WaveResult  `json:"waves"`
}

Results holds the aggregated outcome of a Run(). Frontends use this as the final snapshot after execution.

type RunCompleted

type RunCompleted struct {
	Total    int           `json:"total"`
	Passed   int           `json:"passed"`
	Failed   int           `json:"failed"`
	Skipped  int           `json:"skipped"`
	Errored  int           `json:"errored"`
	Duration time.Duration `json:"duration"`
}

RunCompleted is emitted once when all waves have finished.

func (RunCompleted) Type

func (RunCompleted) Type() string

type RunOption

type RunOption func(*runConfig)

RunOption configures a single Run() invocation. RunOptions are isolated per-execution — safe to use with concurrent Run() calls.

func WithConfigPath

func WithConfigPath(path string) RunOption

WithConfigPath loads a specific .qube.yaml config file for this Run, overriding any engine-level default.

func WithEnv

func WithEnv(env map[string]string) RunOption

WithEnv overrides environment variables for this Run. Values are accessible in templates via {{ env.KEY }}.

func WithHandler

func WithHandler(h EventHandler) RunOption

WithHandler sets the EventHandler for this Run. If not set, events are discarded via NopHandler.

func WithPlugins

func WithPlugins(dir string) RunOption

WithPlugins sets the plugin directory for this Run, overriding any engine-level default. Useful for multi-tenant cloud deployments.

func WithSignals

func WithSignals(ch chan Signal) RunOption

WithSignals provides a channel for sending control signals (pause, resume, skip) to the engine during execution.

type RunStarted

type RunStarted struct {
	Files      []string `json:"files"`
	TotalTests int      `json:"total_tests"`
	TotalWaves int      `json:"total_waves"`
}

RunStarted is emitted once when a Run() begins, after parsing and graph building.

func (RunStarted) Type

func (RunStarted) Type() string

type Severity

type Severity = wire.Severity

Severity classifies the importance of a validation issue.

type Signal

type Signal = wire.Signal

Signal is a control command from frontend to engine during a run.

type TemplateError

type TemplateError struct {
	File       string `json:"file"`
	Test       string `json:"test"`
	Expression string `json:"expression"`
	Message    string `json:"message"`
}

TemplateError is emitted when a template reference cannot be resolved.

func (TemplateError) Type

func (TemplateError) Type() string

type TestCompleted

type TestCompleted struct {
	TestResult
}

TestCompleted is emitted after a single test case completes.

func (TestCompleted) Type

func (TestCompleted) Type() string

type TestResult

type TestResult struct {
	Name       string            `json:"name"`
	File       string            `json:"file"`
	Protocol   Protocol          `json:"protocol"`
	Target     string            `json:"target,omitempty"`
	Status     TestStatus        `json:"status"`
	Duration   time.Duration     `json:"duration"`
	Request    *RequestData      `json:"request,omitempty"`
	Response   *ResponseData     `json:"response,omitempty"`
	Assertions []AssertionResult `json:"assertions,omitempty"`
	Error      string            `json:"error,omitempty"`
}

TestResult is the complete record of a single test case execution. The TestCompleted event embeds this type, so any field added here is automatically available on the event.

type TestStarted

type TestStarted struct {
	Name     string   `json:"name"`
	File     string   `json:"file"`
	Protocol Protocol `json:"protocol"`
	Target   string   `json:"target"`
}

TestStarted is emitted before a single test case runs.

func (TestStarted) Type

func (TestStarted) Type() string

type TestStatus

type TestStatus = wire.TestStatus

TestStatus represents the outcome of a test case.

type ValidationError

type ValidationError = wire.ValidationError

ValidationError describes a problem found during manifest validation.

type WaveCompleted

type WaveCompleted struct {
	WaveResult
}

WaveCompleted is emitted after a parallel wave has finished.

func (WaveCompleted) Type

func (WaveCompleted) Type() string

type WaveResult

type WaveResult struct {
	Index    int           `json:"index"`
	Parallel bool          `json:"parallel"`
	Tests    []TestResult  `json:"tests"`
	Duration time.Duration `json:"duration"`
	Passed   int           `json:"passed"`
	Failed   int           `json:"failed"`
	Skipped  int           `json:"skipped"`
}

WaveResult is the complete record of a single execution wave. The WaveCompleted event embeds this type.

type WaveStarted

type WaveStarted struct {
	Index     int      `json:"index"`
	TestNames []string `json:"test_names"`
	Parallel  bool     `json:"parallel"`
}

WaveStarted is emitted before a parallel wave begins executing.

func (WaveStarted) Type

func (WaveStarted) Type() string

Directories

Path Synopsis
internal
assertion
Package assertion evaluates expect expressions against test responses.
Package assertion evaluates expect expressions against test responses.
dataflow
Package dataflow manages data passing between test cases at runtime.
Package dataflow manages data passing between test cases at runtime.
graph
Package graph builds and analyzes the dependency graph between test cases.
Package graph builds and analyzes the dependency graph between test cases.
parser
Package parser loads and normalizes test manifests from YAML sources.
Package parser loads and normalizes test manifests from YAML sources.
plugin/capabilities
Package capabilities provides the host-side functions WASM plugins import to interact with the outside world.
Package capabilities provides the host-side functions WASM plugins import to interact with the outside world.
runner
Package runner executes parsed test plans by coordinating all engine subsystems.
Package runner executes parsed test plans by coordinating all engine subsystems.
template
Package template implements the ApiQube template DSL.
Package template implements the ApiQube template DSL.
wire
Package wire holds the value types shared between the public engine package and internal subsystems (plugin, runner).
Package wire holds the value types shared between the public engine package and internal subsystems (plugin, runner).

Jump to

Keyboard shortcuts

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