harness

package
v0.18.2 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: LGPL-3.0 Imports: 19 Imported by: 0

Documentation

Index

Constants

View Source
const LaneWidth = 300.0

LaneWidth is the world-space X spacing between scenarios. Every scenario is laid out in its own lane so bodies from unrelated scenarios can never touch, collide, or show up in each other's raycasts and AABB overlaps. Scenario code is written in lane-local coordinates; Ctx translates on the way in and out.

View Source
const TailTicks = 10

TailTicks is how many extra ticks run after the last scheduled step, so a scenario's final mutation still gets simulated and the NaN watchdog still sees the resulting state.

View Source
const TickRate = 60.0

TickRate is the simulation rate for both Cardinal and the physics plugin. The two must match or simulated time drifts from tick time.

Variables

This section is empty.

Functions

func CloneBody

CloneBody deep-copies a PhysicsBody2D including its shapes and their chain geometry, so edits to the copy cannot reach the original. immutable.Slice derivations write through the array they share with the component, so a scenario that means to edit a body has to start from a copy like this one; Map and Collect are the two derivations that allocate instead.

func DecodeSnapshot

func DecodeSnapshot(raw []byte) (any, error)

DecodeSnapshot is the inverse of EncodeSnapshot.

func EncodeSnapshot

func EncodeSnapshot(state any) ([]byte, error)

EncodeSnapshot marshals a world state to protobuf bytes, the form a snapshot actually takes on its way to JetStream or S3. Going through bytes is what makes a two-process restore a real one.

func InitECS

func InitECS(w *cardinal.World)

InitECS runs the world's Init-hook systems and marks the ECS world initialized.

cardinal.World only does this inside StartGame, which also stands up NATS and the ConnectRPC service. This test game drives World.Tick directly so it can run with no infrastructure at all, so it reaches the unexported ecs.World through reflection — the same approach the plugin's own integration tests use.

func RegisterPostCapture

func RegisterPostCapture(w *cardinal.World, into *Capture)

RegisterPostCapture registers a capture that runs after the physics pipeline.

func RegisterPreCapture

func RegisterPreCapture(w *cardinal.World, into *Capture)

RegisterPreCapture registers a capture that runs before the physics plugin. Call it before RegisterPlugin. After a snapshot restore, the first tick's pre-capture is the deserialized ECS state with nothing else having touched it.

func RestoreWorld

func RestoreWorld(w *cardinal.World, state any) error

RestoreWorld loads a serialized world state, the way World.restore does after a crash. Note the ordering Cardinal uses: Init systems run first and only then is their state thrown away by this call.

func SnapshotWorld

func SnapshotWorld(w *cardinal.World) (any, error)

SnapshotWorld serializes a world exactly the way Cardinal's snapshot writer does, component bytes and all.

Types

type Capture

type Capture struct {
	Rows map[string]CaptureRow
	// Contacts is the singleton's ActiveContacts, normalised and sorted.
	Contacts []physics.ContactPairEntry
	// Singletons is how many physics singleton entities exist. Anything but one
	// is a bug: the plugin panics on two and loses its dedupe baseline on none.
	Singletons int
}

Capture is every body in a world, keyed by its probe label, plus the plugin's singleton state. Labels are used rather than entity IDs so a capture stays comparable across two worlds.

func (Capture) Labels

func (c Capture) Labels() []string

Labels returns the capture's labels in sorted order.

type CaptureRow

type CaptureRow struct {
	Transform physics.Transform2D
	Velocity  physics.Velocity2D
	Body      physics.PhysicsBody2D
	Entity    cardinal.EntityID
}

CaptureRow is the complete physics state of one body.

type Config

type Config struct {
	// Gravity is the world gravity vector handed to the physics plugin.
	Gravity physics.Vec2
	// SubStepCount is the plugin's solver sub-step count; zero uses the plugin default.
	SubStepCount int
	// Verbose prints passing checks as they happen, not just failures.
	Verbose bool
	// ExtraTicks runs the loop longer than the scenarios strictly require.
	ExtraTicks uint64
	// PreCapture, when set, receives a copy of every body's components at the
	// start of each tick, before the physics plugin runs.
	PreCapture *Capture
	// PostCapture, when set, receives the same after the physics pipeline has
	// reconciled, stepped and written back.
	PostCapture *Capture
	// Workers is the physics engine's worker count. Results are identical for
	// every value, so this only changes throughput.
	Workers int
	// Digest prints a digest of every body's final state at the end of the run.
	// Two runs of the same binary must produce the same digest; if they do not,
	// something in the pipeline is reading wall-clock time, map order, or
	// uninitialised memory.
	Digest bool
	// TB, when set, routes every check to a testing.TB as well as the report:
	// failures through Errorf at the scenario's call site, notes and skips through
	// Logf. The go test entry points set it; the CLI leaves it nil.
	TB testing.TB
}

Config controls how the harness builds its world.

type Ctx

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

Ctx is the API a scenario uses to spawn, inspect, mutate and assert. All positions crossing this boundary are lane-local: Ctx adds the lane offset when writing to ECS and subtracts it when reading back.

func (*Ctx) Alive

func (c *Ctx) Alive(id cardinal.EntityID) bool

Alive reports whether the entity still exists with the probe archetype.

func (*Ctx) AngVel

func (c *Ctx) AngVel(id cardinal.EntityID) float64

AngVel returns the entity's angular velocity.

func (*Ctx) Between

func (c *Ctx) Between(check string, got, lo, hi float64) bool

Between asserts lo <= got <= hi.

func (*Ctx) Body

Body returns a deep copy of the entity's PhysicsBody2D. The Shapes slice is cloned so a scenario can edit it without aliasing ECS storage; write it back with SetBody.

func (*Ctx) CircleSweep

func (c *Ctx) CircleSweep(
	fromX, fromY, toX, toY, radius, maxFraction float64, filter *physics.Filter,
) physics.CircleSweepResult

CircleSweep sweeps a circle between two lane-local points.

func (*Ctx) CountBetween

func (c *Ctx) CountBetween(kind EventKind, a, b cardinal.EntityID) int

CountBetween counts this scenario's events of a kind involving exactly a and b.

func (*Ctx) CountTouching

func (c *Ctx) CountTouching(kind EventKind, id cardinal.EntityID) int

CountTouching counts this scenario's events of a kind naming the given entity.

func (*Ctx) Destroy

func (c *Ctx) Destroy(id cardinal.EntityID) bool

Destroy removes the entity from the world.

func (*Ctx) EditBody

func (c *Ctx) EditBody(id cardinal.EntityID, edit func(pb *physics.PhysicsBody2D))

EditBody reads the body, hands it to edit, and writes the result back.

func (*Ctx) EditShape added in v0.16.8

func (c *Ctx) EditShape(id cardinal.EntityID, i int, edit func(sh *physics.ColliderShape))

EditShape applies edit to one shape of the entity's body and writes the body back. Shapes hands out element copies, so a shape changes by being read out, edited, and put back with With — there is no index to assign through.

func (*Ctx) Events

func (c *Ctx) Events(kind EventKind) []LoggedEvent

Events returns every event of the given kind recorded for this scenario so far.

func (*Ctx) EventsBetween

func (c *Ctx) EventsBetween(kind EventKind, a, b cardinal.EntityID) []LoggedEvent

EventsBetween returns this scenario's events of a kind involving exactly a and b.

func (*Ctx) EventsOnTick

func (c *Ctx) EventsOnTick(kind EventKind, tick uint64) []LoggedEvent

EventsOnTick returns this scenario's events of a kind recorded on one tick.

func (*Ctx) ExpectWorldReset

func (c *Ctx) ExpectWorldReset()

ExpectWorldReset silences the runner's "the world disappeared" watchdog for its next check, so a scenario that calls Plugin.Reset can say so. The watchdog runs on PreUpdate, so a grant from Steps (Update) is consumed on the next tick and one from EachTick (PreUpdate) on the same tick.

func (*Ctx) False

func (c *Ctx) False(check string, got bool, format string, args ...any) bool

False asserts a condition does not hold.

func (*Ctx) Greater

func (c *Ctx) Greater(check string, got, threshold float64) bool

Greater asserts got > threshold.

func (*Ctx) GreaterEq

func (c *Ctx) GreaterEq(check string, got, threshold float64) bool

GreaterEq asserts got >= threshold.

func (*Ctx) HasError

func (c *Ctx) HasError(check string, err error) bool

HasError asserts err is non-nil.

func (*Ctx) Int

func (c *Ctx) Int(check string, got, want int) bool

Int asserts an integer equals want.

func (*Ctx) IntAtLeast

func (c *Ctx) IntAtLeast(check string, got, want int) bool

IntAtLeast asserts an integer is at least want.

func (*Ctx) Label

func (c *Ctx) Label(id cardinal.EntityID) string

Label returns the human-readable label recorded for an entity at spawn time. It keeps working after the entity is destroyed, which matters for end events.

func (*Ctx) Lane

func (c *Ctx) Lane() float64

Lane returns this scenario's world-space X offset.

func (*Ctx) Less

func (c *Ctx) Less(check string, got, threshold float64) bool

Less asserts got < threshold.

func (*Ctx) LessEq

func (c *Ctx) LessEq(check string, got, threshold float64) bool

LessEq asserts got <= threshold.

func (*Ctx) Near

func (c *Ctx) Near(check string, got, want, tol float64) bool

Near asserts got is within tol of want.

func (*Ctx) NearVec

func (c *Ctx) NearVec(check string, got, want physics.Vec2, tol float64) bool

NearVec asserts both components of got are within tol of want.

func (*Ctx) NoError

func (c *Ctx) NoError(check string, err error) bool

NoError asserts err is nil.

func (*Ctx) Note

func (c *Ctx) Note(format string, args ...any)

Note records a diagnostic line without affecting pass/fail counts.

func (*Ctx) OverlapAABB

func (c *Ctx) OverlapAABB(minX, minY, maxX, maxY float64, filter *physics.Filter) physics.AABBOverlapResult

OverlapAABB queries a lane-local axis-aligned box.

func (*Ctx) OverlapHits

func (c *Ctx) OverlapHits(res physics.AABBOverlapResult, id cardinal.EntityID) bool

OverlapHits reports whether an AABB overlap returned the given entity.

func (*Ctx) OverlapHitsShape

func (c *Ctx) OverlapHitsShape(res physics.AABBOverlapResult, id cardinal.EntityID, shapeIndex int) bool

OverlapHitsShape reports whether an AABB overlap returned a specific shape slot.

func (*Ctx) Plugin

func (c *Ctx) Plugin() *physics.Plugin

Plugin returns the physics plugin driving this world. Queries, Reset and Engine are methods on it — the package holds no runtime state.

func (*Ctx) Pos

func (c *Ctx) Pos(id cardinal.EntityID) physics.Vec2

Pos returns the entity's lane-local position, or the zero vector if it is gone.

func (*Ctx) Raycast

func (c *Ctx) Raycast(fromX, fromY, toX, toY float64, filter *physics.Filter) physics.RaycastResult

Raycast casts from one lane-local point to another. The returned hit point is translated back into lane-local space.

func (*Ctx) Report

func (c *Ctx) Report() *Report

Report exposes the underlying report for the few places that need it directly.

func (*Ctx) Rot

func (c *Ctx) Rot(id cardinal.EntityID) float64

Rot returns the entity's rotation in radians.

func (*Ctx) Scenario

func (c *Ctx) Scenario() string

Scenario returns the running scenario's name.

func (*Ctx) SetAngVel

func (c *Ctx) SetAngVel(id cardinal.EntityID, angular float64)

SetAngVel sets the entity's angular velocity.

func (*Ctx) SetBody

func (c *Ctx) SetBody(id cardinal.EntityID, pb physics.PhysicsBody2D)

SetBody replaces the entity's PhysicsBody2D.

func (*Ctx) SetPos

func (c *Ctx) SetPos(id cardinal.EntityID, x, y float64)

SetPos moves the entity to a lane-local position.

func (*Ctx) SetRot

func (c *Ctx) SetRot(id cardinal.EntityID, radians float64)

SetRot sets the entity's rotation in radians.

func (*Ctx) SetVel

func (c *Ctx) SetVel(id cardinal.EntityID, vx, vy float64)

SetVel sets the entity's linear velocity.

func (*Ctx) Skip

func (c *Ctx) Skip(check, format string, args ...any)

Skip records that a check could not run.

func (*Ctx) Spawn

func (c *Ctx) Spawn(label string, x, y float64, pb physics.PhysicsBody2D) cardinal.EntityID

Spawn creates a probe entity at rest at the given lane-local position.

func (*Ctx) SpawnFull

SpawnFull creates a probe entity from a complete pose, velocity and body.

func (*Ctx) SpawnMoving

func (c *Ctx) SpawnMoving(
	label string, x, y, vx, vy float64, pb physics.PhysicsBody2D,
) cardinal.EntityID

SpawnMoving creates a probe entity with an initial linear velocity.

func (*Ctx) SpawnSpinning

func (c *Ctx) SpawnSpinning(
	label string, x, y, angular float64, pb physics.PhysicsBody2D,
) cardinal.EntityID

SpawnSpinning creates a probe entity with an initial angular velocity.

func (*Ctx) Speed

func (c *Ctx) Speed(id cardinal.EntityID) float64

Speed returns the magnitude of the entity's linear velocity.

func (*Ctx) Str

func (c *Ctx) Str(check, got, want string) bool

Str asserts a string equals want.

func (*Ctx) Tick

func (c *Ctx) Tick() uint64

Tick returns the tick currently being simulated.

func (*Ctx) True

func (c *Ctx) True(check string, got bool, format string, args ...any) bool

True asserts a condition holds.

func (*Ctx) Vel

func (c *Ctx) Vel(id cardinal.EntityID) physics.Vec2

Vel returns the entity's linear velocity.

type Diff

type Diff struct {
	Label string
	Field string
	Got   string
	Want  string
}

Diff describes one field that differs between two captures.

func CompareCaptures

func CompareCaptures(want, got Capture, tol float64) []Diff

CompareCaptures reports every field of every body that differs between want and got, within tol on floats. Bodies present in one capture and not the other are reported too, because a body lost in a rebuild is the worst failure of all.

func CompareContacts

func CompareContacts(want, got Capture) []Diff

CompareContacts reports differences between two worlds' ActiveContacts.

func (Diff) String

func (d Diff) String() string

type EventKind

type EventKind uint8

EventKind distinguishes the four physics system events.

const (
	ContactBegin EventKind = iota
	ContactEnd
	TriggerBegin
	TriggerEnd
)

Physics system event kinds.

func (EventKind) String

func (k EventKind) String() string

String returns the event kind's name as it appears in failure messages.

type LoggedEvent

type LoggedEvent struct {
	Payload physics.ContactEventPayload
	Tick    uint64
	Kind    EventKind
}

LoggedEvent is one physics event captured with the tick it arrived on.

func (LoggedEvent) FilterFor

FilterFor returns the fixture filter the given entity contributed to the event.

func (LoggedEvent) Involves

func (e LoggedEvent) Involves(a, b cardinal.EntityID) bool

Involves reports whether the event is between exactly a and b, in either order.

func (LoggedEvent) ShapeIndexFor

func (e LoggedEvent) ShapeIndexFor(id cardinal.EntityID) (int, bool)

ShapeIndexFor returns the shape slot the given entity contributed to the event, so a compound body's contacts can be traced back to the exact child.

func (LoggedEvent) Touches

func (e LoggedEvent) Touches(a cardinal.EntityID) bool

Touches reports whether the event names the given entity on either side.

type Outcome

type Outcome uint8

Outcome is the result of a single assertion.

const (
	OutcomePass Outcome = iota
	OutcomeFail
	OutcomeSkip
)

Assertion outcomes.

type ProbeRow

type ProbeRow struct {
	Probe     probe.Probe
	Transform physics.Transform2D
	Velocity  physics.Velocity2D
	Body      physics.PhysicsBody2D
}

ProbeRow is the archetype every harness-spawned body uses.

type Report

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

Report accumulates assertion results and prints them. It is written for a single-goroutine tick loop, so it holds no lock.

func NewReport

func NewReport(verbose bool) *Report

NewReport builds an empty report. verbose prints every passing assertion; otherwise only failures, skips and notes are streamed while the run proceeds.

func (*Report) BindTB

func (r *Report) BindTB(tb testing.TB)

BindTB routes every result to a testing.TB as well as the report: failures through Errorf, attributed to the scenario's own line via Helper; notes, skips and (when verbose) passes through Logf. Colour is switched off because the destination is a test log. The CLI never binds.

func (*Report) Bound

func (r *Report) Bound() bool

Bound reports whether results are being routed to a testing.TB.

func (*Report) Fail

func (r *Report) Fail(scenario, check string, tick uint64, format string, args ...any)

Fail records a violated assertion and prints it immediately, so a run that later panics still shows everything that had already gone wrong.

func (*Report) Failures

func (r *Report) Failures() []Result

Failures returns every failed assertion in the order they were recorded.

func (*Report) Note

func (r *Report) Note(scenario string, tick uint64, format string, args ...any)

Note records a diagnostic observation that is neither pass nor fail. Use it for behaviour worth eyeballing (measured bounce heights, whether a non-bullet body tunnelled) where a hard threshold would only produce flaky failures.

func (*Report) Pass

func (r *Report) Pass(scenario, check string, tick uint64)

Pass records a satisfied assertion.

func (*Report) Print

func (r *Report) Print()

Print writes the end-of-run summary: a per-scenario table, then a repeat of every failure so the verdict does not require scrolling back through the log.

func (*Report) Skip

func (r *Report) Skip(scenario, check string, tick uint64, format string, args ...any)

Skip records an assertion that could not run (a precondition was missing). Skips never fail the suite but are always reported, because a silently skipped check is indistinguishable from a passing one otherwise.

func (*Report) Totals

func (r *Report) Totals() (int, int, int)

Totals returns the run-wide pass, fail and skip counts.

type Result

type Result struct {
	Scenario string
	Check    string
	Detail   string
	Tick     uint64
	Outcome  Outcome
}

Result is one recorded assertion.

type Runner

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

Runner owns the scenario list, the report, and the tick loop.

func New

func New(scenarios []Scenario, cfg Config) *Runner

New builds a runner over the given scenarios, assigning each one its own lane.

func (*Runner) Advance

func (r *Runner) Advance(w *cardinal.World, n int)

Advance ticks the world n times, continuing from wherever it left off. The timestamps are derived from the tick index so two worlds ticked the same number of times see the same sequence.

func (*Runner) BuildWorld

func (r *Runner) BuildWorld(cfg Config) (*cardinal.World, error)

BuildWorld creates the Cardinal world and registers everything in the order the physics pipeline requires: scenario Init and PreUpdate systems first, then the plugin, then the Update-hook step system.

func (*Runner) Digest

func (r *Runner) Digest(w *cardinal.World) (int, uint64)

printDigest reports a hash over every surviving body's final pose and velocity, plus the extremes, so two runs can be compared at a glance. Digest hashes every probe body's final pose and velocity and returns the body count with the hash. Two runs of the same build on the same machine must agree, and so must runs that differ only in Config.Workers.

func (*Runner) EventsUpTo

func (r *Runner) EventsUpTo(scenario string, kind EventKind, tick uint64) int

EventsUpTo counts events of a kind recorded for a scenario on or before a tick. The restore check uses it to tell "the rebuilt world replayed every existing overlap as new" apart from "the rebuilt world kept its dedupe baseline".

func (*Runner) LastTick

func (r *Runner) LastTick() uint64

LastTick returns the final tick the loop will run.

func (*Runner) Plugin

func (r *Runner) Plugin() *physics.Plugin

Plugin returns the physics plugin bound to this runner's world. It is nil until BuildWorld has run.

func (*Runner) Report

func (r *Runner) Report() *Report

Report returns the accumulated results.

func (*Runner) Run

func (r *Runner) Run(w *cardinal.World) int

Run initializes the world, ticks it to completion, prints the report, and returns the process exit code (0 when every check passed).

func (*Runner) Ticked

func (r *Runner) Ticked() int

Ticked reports how many ticks Advance has run.

type Scenario

type Scenario struct {
	Setup    func(c *Ctx)
	EachTick func(c *Ctx)
	Name     string
	Steps    []Step
	// contains filtered or unexported fields
}

Scenario is one self-contained group of physics checks.

Setup runs on cardinal.Init, before the plugin's own Init system, so the initial FullRebuildFromECS sees the bodies. EachTick runs on PreUpdate before the physics pipeline (use it to drive gameplay-owned bodies). Steps run on Update, after the pipeline, where post-step positions and this tick's contact events are both visible.

func (*Scenario) LastTick

func (s *Scenario) LastTick() uint64

LastTick returns the highest tick any of the scenario's steps is scheduled for.

type SingletonRow

type SingletonRow struct {
	Tag            physics.PhysicsSingletonTag
	ActiveContacts physics.ActiveContacts
}

SingletonRow is the plugin's own bookkeeping entity. It carries ActiveContacts, the persisted record of which pairs have had a Begin emitted and not yet an End. The physics step diffs it against Box2D's live contact list after a rebuild, so if it does not survive a restore the rebuilt world replays every existing overlap as a new contact.

type Step

type Step struct {
	Do   func(c *Ctx)
	Tick uint64
}

Step is one scheduled action or assertion, run on the given tick after the physics pipeline has stepped. Steps sharing a tick run in declaration order.

Jump to

Keyboard shortcuts

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