rspec

package module
v0.0.0-...-5d9b4af Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-rspec/rspec

rspec — go-ruby-rspec

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's RSpec — the rspec-expectations matchers and the rspec-core example-group structure model and formatters. It answers the parts of a spec run that are pure functions of values and results: does a matcher match, what failure message does it produce, which examples run and in what order, and how does the formatter render a set of results — all byte-faithful to RSpec 3.13, and without any Ruby runtime.

It is the RSpec backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine) and go-ruby-erb (the ERB compiler).

What it is — and isn't. A matcher's match logic, its failure-message strings, the describe/context/it tree, filtering, ordering, and the formatter output are all deterministic and need no interpreter, so they live here as pure Go. Running the body of an it example or a before/let hook is the host's job (rbgo evaluates the Ruby): the block-form matchers (change, raise_error) consume the host's observation of what a block did, and the reporter is driven by host-supplied results.

Features

rspec-expectations matchers

Every matcher carries Matches(actual) plus FailureMessage() / FailureMessageNegated(), rendered byte-for-byte against the gem:

  • Equalityeq (==), eql (eql?), equal (equal? identity), with Ruby's cross-type numeric == and the distinctive equal? object-identity message.
  • Truthiness / nilbe_truthy, be_falsey, be_nil.
  • Comparisonbe > / >= / < / <= / == / != (including RSpec's "a bit confusing" negated form for ordering operators).
  • Typebe_a / be_kind_of (ancestor walk) and be_instance_of.
  • Strings & collectionsmatch(/re/), include, start_with / end_with, contain_exactly / match_array (multiset diff with the missing/extra element diagnostics), all(matcher).
  • Reflectionhave_attributes, respond_to(:m).with(n).arguments, predicate matchers (be_emptyempty?).
  • Numeric / rangebe_within(delta).of(x), cover.
  • Blockschange { … }.from/to/by/by_at_least/by_at_most (the diff logic) and raise_error(Class, message|/re/), driven by host observations.
  • Custom & composedsatisfy, plus .and / .or composition (with the multi-line ...or: message) and not_to.
rspec-core structure model & formatters
  • The describe/context/it registration tree with example metadata, the before/after/around + let/subject hooks (outer-before / inner-after ordering), filtering (:focus, include/exclude tags), and ordering (defined order or a seeded random permutation).
  • The progress (. / F / *) and documentation formatters, plus the shared summary block: the pending section, the failures section, the totals line ("N examples, M failures, P pending" with RSpec's pluralisation), and the Failed examples: rerun list — all byte-faithful to a real rspec run.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three operating systems.

Install

go get github.com/go-ruby-rspec/rspec

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-rspec/rspec"
)

func main() {
	// A matcher: match logic + byte-faithful RSpec failure message.
	m := rspec.Eq(4)
	ok, msg := rspec.Expect(3, m, true) // expect(3).to eq(4)
	fmt.Println(ok)                     // false
	fmt.Print(msg)
	// expected: 4
	//      got: 3
	// (compared using ==)

	// A structure tree + formatter, driven by host-supplied results.
	root := rspec.NewRootGroup("Calc")
	a := root.It("adds")
	a.Result = rspec.Result{Status: rspec.Passed}
	b := root.It("subtracts")
	b.Location = "./calc_spec.rb:5"
	b.Result = rspec.Result{
		Status:            rspec.Failed,
		FailureExpression: "expect(5 - 2).to eq(4)",
		FailureMessage:    msg,
	}

	r := rspec.NewReporter()
	r.Record(a)
	r.Record(b)
	fmt.Print(r.Render(rspec.Timing{Run: "0.01 seconds", Load: "0.03 seconds"}))
	// .F
	// …
	// 2 examples, 1 failure
}

Ruby value model

Matcher messages embed the Ruby #inspect of the values under test, so this package reproduces MRI's inspect for a small, fixed value model a host maps its object graph onto:

Ruby Go
nil nil
true / false bool
Integer int…, uint…, *big.Int
Float float64, float32
String string
Symbol rspec.Symbol
Array []any
Hash *rspec.Hash (insertion-ordered)
Range *rspec.Range
Regexp *rspec.Regexp
Class/Module rspec.Class / rspec.Module
object *rspec.Object (class, ivars, object_id)

The reflection matchers (have_attributes, respond_to, predicates) resolve against the built-in model directly; for host objects they route through the overridable AttrReader / Responder / Predicate hooks so rbgo can dispatch the call through the interpreter.

The host seam

Deterministic pieces live here; evaluating Ruby is the host's job. The block-form matchers take an observation:

// expect { a[0] += 3 }.to change { a[0] }.by(5)
obs := rspec.Change{ExprName: "a[0]", Before: 0, After: 3} // host runs the block + probe
ok, msg := rspec.Expect(nil, rspec.ChangeObserved(obs).By(5), true)
// ok == false, msg == "expected `a[0]` to have changed by 5, but was changed by 3"

Likewise raise_error consumes a rspec.RaisedError{Class, Message} the host reports after running the example block, and the Reporter is fed Results the host produces by running each it body.

Tests & coverage

The suite pairs deterministic, ruby-free golden-vector tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential RSpec oracle: each matcher is run on both sides — here and the real rspec-expectations gem — on pass and fail actuals, and the matches? result plus both failure-message strings are compared byte-for-byte; the totals line is checked against rspec-core's SummaryNotification. The oracle skips itself where ruby / the rspec gems are absent and gates on RUBY_VERSION >= "4.0".

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-rspec/rspec authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package rspec is a pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent core of Ruby's RSpec — the rspec-expectations matchers (their match logic and byte-faithful failure messages) and the rspec-core example-group structure model and formatter output.

Running the bodies of `it` examples and before/after/let/subject hooks is the host's job (rbgo evaluates the Ruby); this library provides the parts that are pure functions of values and results: does a matcher match, what message does it produce, and how does the formatter render a set of results. It is the RSpec backend for go-embedded-ruby, but a standalone, reusable module with no dependency on a Ruby runtime.

Index

Constants

This section is empty.

Variables

View Source
var AttrReader = defaultAttrReader

AttrReader is called by have_attributes to read attribute `name` off actual. The default handles the built-in model; a host may replace it to dispatch to the interpreter. It returns (value, true) when the attribute exists.

View Source
var Predicate = defaultPredicate

Predicate is called by predicate matchers (be_empty → empty?) to evaluate the predicate method on actual, returning (result, ok) where ok is false when the object does not respond to the predicate. The default handles the built-in model; a host may replace it.

View Source
var Responder = defaultResponder

Responder is called by respond_to and predicate matchers to test whether actual answers to `method`. The default handles the built-in model and *Object.RespondsTo; a host may replace it.

Functions

func BeWithin

func BeWithin(delta float64) *beWithinMatcher

BeWithin builds be_within(delta); call Of to set the centre.

func ChangeObserved

func ChangeObserved(obs Change) *changeMatcher

ChangeObserved builds a change matcher over a host-collected observation. Chain From/To/By/ByAtLeast/ByAtMost to constrain it.

func Expect

func Expect(actual any, m Matcher, positive bool) (ok bool, message string)

Expect models `expect(actual).to(matcher)` / `.not_to(matcher)`. It returns the failure message (or "" on success) so the host can record a result without this package raising. Positive is the `to` direction.

func Inspect

func Inspect(v any) string

Inspect returns the Ruby `#inspect` of v, byte-faithful to MRI 4.0 for the value model this package supports. It is the string RSpec embeds in matcher and formatter messages.

func RespondTo

func RespondTo(methods ...Symbol) *respondToMatcher

RespondTo matches when actual responds to every named method.

func TotalsLine

func TotalsLine(examples, failures, pending, errors int) string

TotalsLine reproduces RSpec's SummaryNotification#totals_line, e.g. "4 examples, 1 failure, 1 pending" with correct pluralisation, and an "N error(s) occurred outside of examples" clause when errors > 0.

Types

type Change

type Change struct {
	ExprName string
	Before   any
	After    any
}

Change is the observation `change { probe }` collected around a block: the probe value before and after the host ran the example block. exprName is the source of the probe (e.g. "a[0]") used in the message.

type Class

type Class string

Class is a Ruby Class reference; inspect is the bare class name.

type Describer

type Describer interface {
	Description() string
}

Describer is the optional protocol for a matcher's `description` (used by composed matchers and `all`, and by the documentation formatter's generated example names). Matchers that do not implement it fall back to a generic description.

type Example

type Example struct {
	Description string
	Group       *ExampleGroup
	Focused     bool
	Skip        bool     // `xit` / skip: metadata — never runs
	PendingMsg  string   // set when declared pending
	Tags        []string // metadata tag names (e.g. "slow", "focus")
	Location    string   // "path:line" for the failed-examples rerun list
	DefinedAt   int      // monotonic definition index for stable/seeded ordering

	// Result fields, filled by the host after running the body.
	Result Result
	// contains filtered or unexported fields
}

Example is one `it`/`specify` example.

func OrderDefined

func OrderDefined(examples []*Example) []*Example

OrderDefined returns the examples in definition order (RSpec's default).

func OrderRandom

func OrderRandom(examples []*Example, seed uint32) []*Example

OrderRandom returns the examples shuffled by RSpec's seeded ordering. RSpec uses `Ordering::Random`, which shuffles with a Ruby MT-seeded RNG; here we use a deterministic Fisher-Yates driven by the same 32-bit seed so a given seed yields a stable, reproducible permutation (the ordering is deterministic, which is what the golden tests pin; exact parity with MRI's Mersenne Twister would require the host RNG).

func (*Example) Focus

func (e *Example) Focus() *Example

Focus marks the example focused (`fit` / `:focus`).

func (*Example) FullDescription

func (e *Example) FullDescription() string

FullDescription is the space-joined chain of group descriptions plus the example description — RSpec's `full_description`, used by the documentation formatter's rerun/location line and doc labels.

func (*Example) Tag

func (e *Example) Tag(names ...string) *Example

Tag adds metadata tags to the example (e.g. "slow").

type ExampleGroup

type ExampleGroup struct {
	Description string
	Parent      *ExampleGroup
	Children    []*ExampleGroup
	Examples    []*Example
	Hooks       []*Hook
	Focused     bool
	Tags        []string
}

ExampleGroup is a describe/context node in the tree.

func NewRootGroup

func NewRootGroup(desc string) *ExampleGroup

NewRootGroup starts a top-level `RSpec.describe(desc)` group.

func (*ExampleGroup) AddHook

func (g *ExampleGroup) AddHook(h *Hook)

AddHook attaches a hook/let/subject to the group.

func (*ExampleGroup) Describe

func (g *ExampleGroup) Describe(desc string) *ExampleGroup

Describe adds a nested describe/context group.

func (*ExampleGroup) Focus

func (g *ExampleGroup) Focus() *ExampleGroup

Focus marks the group focused (`fdescribe` / `:focus`).

func (*ExampleGroup) It

func (g *ExampleGroup) It(desc string) *Example

It adds an example to the group and returns it for further metadata.

func (*ExampleGroup) SelectExamples

func (g *ExampleGroup) SelectExamples(f Filter) []*Example

SelectExamples flattens the tree into the examples that should run under the filter, in definition order.

type Filter

type Filter struct {
	// IncludeTags, when non-empty, restricts to examples carrying any of them.
	IncludeTags []string
	// ExcludeTags drops examples carrying any of them.
	ExcludeTags []string
	// RunFocused, when true and any example is focused, restricts to focused
	// examples (RSpec's default :focus filter). Detected automatically by
	// SelectExamples when unset via anyFocused.
	RunFocused bool
}

Filter selects which examples run given a filter. It applies :focus (when any example/group is focused, only focused ones run), inclusion tags, and exclusion tags, then flattens the tree in definition order. The returned examples carry the before/after chains the host must run around each.

type Hash

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

Hash is an insertion-ordered Ruby Hash. Go maps have no stable order, so the matchers and inspect use this to reproduce MRI's key order faithfully.

func NewHash

func NewHash() *Hash

NewHash returns an empty ordered Hash.

func (*Hash) Get

func (h *Hash) Get(key any) (any, bool)

Get returns the value stored for key.

func (*Hash) Len

func (h *Hash) Len() int

Len reports the number of entries.

func (*Hash) Pairs

func (h *Hash) Pairs() []Pair

Pairs returns the entries in insertion order.

func (*Hash) Set

func (h *Hash) Set(key, val any)

Set inserts or updates key→val, preserving first-insertion order.

type Hook

type Hook struct {
	Kind  HookKind
	Scope HookScope
	Name  string // for let/subject: the memoized name
}

Hook is a before/after/around hook or a let/subject definition attached to a group. The body is the host's; this model records its kind and scope so the host runs them in RSpec's order.

type HookKind

type HookKind int

HookKind distinguishes before/after/around and let/subject.

const (
	// BeforeHook runs before an example (or the group for :context scope).
	BeforeHook HookKind = iota
	// AfterHook runs after.
	AfterHook
	// AroundHook wraps the example.
	AroundHook
	// LetDef is a lazy memoized helper (`let(:name) { … }`).
	LetDef
	// SubjectDef is the group's subject (`subject { … }` / `subject(:name)`).
	SubjectDef
)

type HookScope

type HookScope int

HookScope is :example (default) or :context (once per group).

const (
	// ExampleScope runs the hook around each example.
	ExampleScope HookScope = iota
	// ContextScope runs the hook once for the whole group.
	ContextScope
)

type Matcher

type Matcher interface {
	// Matches reports whether actual satisfies the matcher (RSpec `matches?`).
	Matches(actual any) bool
	// FailureMessage is rendered when a positive expectation (`to`) fails.
	FailureMessage() string
	// FailureMessageNegated is rendered when a negative expectation (`not_to`)
	// fails (RSpec `failure_message_when_negated`).
	FailureMessageNegated() string
}

Matcher is the RSpec matcher protocol as this package models it: a predicate on the actual value plus the two failure messages RSpec renders for `to` and `not_to`. It mirrors rspec-expectations' `matches?` / `failure_message` / `failure_message_when_negated`.

func All

func All(inner Matcher) Matcher

All matches when every element of actual satisfies inner.

func And

func And(left, right Matcher) Matcher

And composes two matchers so both must match (RSpec `matcher.and(other)`).

func BeEqualOp

func BeEqualOp(x any) Matcher

func BeFalsey

func BeFalsey() Matcher

BeFalsey matches nil or false.

func BeGreaterOrEqual

func BeGreaterOrEqual(x any) Matcher

func BeGreaterThan

func BeGreaterThan(x any) Matcher

Be returns a comparison builder; combine with a comparison operator via the Be* constructors below. Bare `be` (no operator) asserts truthiness like be_truthy but with its own message; RSpec's bare `be` is rarely used, so the operator forms are the common path.

func BeInstanceOf

func BeInstanceOf(class string) Matcher

BeInstanceOf matches when actual's exact class is the named class.

func BeKindOf

func BeKindOf(class string) Matcher

BeKindOf matches when actual is a kind of the named class (be_a / be_kind_of).

func BeLessOrEqual

func BeLessOrEqual(x any) Matcher

func BeLessThan

func BeLessThan(x any) Matcher

func BeNil

func BeNil() Matcher

BeNil matches nil.

func BeNotEqualOp

func BeNotEqualOp(x any) Matcher

func BePredicate

func BePredicate(predicate string) Matcher

BePredicate builds a predicate matcher for `predicate?` (predicate given without the trailing '?'), e.g. BePredicate("empty") is be_empty.

func BeTruthy

func BeTruthy() Matcher

BeTruthy matches any truthy value.

func ContainExactly

func ContainExactly(items ...any) Matcher

ContainExactly matches when actual holds exactly the given elements, any order.

func Cover

func Cover(values ...any) Matcher

Cover matches when a Range actual covers every argument.

func EndWith

func EndWith(items ...any) Matcher

EndWith matches when actual ends with the given suffix elements/substring.

func Eq

func Eq(expected any) Matcher

Eq matches when actual == expected (Ruby `==`).

func Eql

func Eql(expected any) Matcher

Eql matches when actual.eql?(expected) (type-strict equality).

func Equal

func Equal(expected any) Matcher

Equal matches when actual.equal?(expected) (same object identity).

func HaveAttributes

func HaveAttributes(attrs *Hash) Matcher

HaveAttributes matches when actual's attributes equal the expected pairs.

func Include

func Include(items ...any) Matcher

Include matches when actual includes every argument.

func Match

func Match(pattern any) Matcher

Match matches when actual matches the given Regexp (or string pattern).

func MatchArray

func MatchArray(items []any) Matcher

MatchArray is contain_exactly with a single array argument.

func Or

func Or(left, right Matcher) Matcher

Or composes two matchers so either must match (RSpec `matcher.or(other)`).

func RaiseErrorObserved

func RaiseErrorObserved(obs RaisedError, class string, message any) Matcher

RaiseErrorObserved builds a raise_error matcher over a host observation. class "" matches any error; message may be a string, *Regexp, or nil.

func Satisfy

func Satisfy(desc, expr string, pred func(any) bool) Matcher

Satisfy matches when pred(actual) is true. desc is the human description used in the message (RSpec's optional string argument); when empty, expr provides the `satisfy expression \`expr\“ default.

func StartWith

func StartWith(items ...any) Matcher

StartWith matches when actual starts with the given prefix elements/substring.

type Module

type Module string

Module is a Ruby Module reference; inspect is the bare module name.

type Object

type Object struct {
	Class      string
	IVars      map[string]any
	Order      []string
	ID         uint64 // object_id for equal? identity; 0 means "use pointer identity"
	RespondsTo []string
}

Object is an opaque Ruby object of the given Class. RSpecClass reports the class name matchers such as be_a / be_instance_of test against; IVars and the object identity (ID) let the host reproduce `#<Class:0x…>` style inspects and have_attributes / respond_to reflection. RespondsTo lists the message names the object answers to (for respond_to and predicate matchers).

type Pair

type Pair struct {
	Key, Val any
}

Pair is one insertion-ordered Hash entry.

type RaisedError

type RaisedError struct {
	Raised  bool
	Class   string
	Message string
}

RaisedError is the host's observation of what an example block raised: the Ruby class name of the exception and its message, or Raised=false when the block completed normally.

type Range

type Range struct {
	Begin     any
	End       any
	Exclusive bool
}

Range is a Ruby Range; inspect is `begin..end` (or `begin...end` when Exclusive), matching MRI.

type Regexp

type Regexp struct {
	Source string
	Flags  string
}

Regexp is a Ruby Regexp literal; inspect is `/source/flags`.

type Reporter

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

Reporter accumulates example results and renders the formatter output. It is driven by the host, which calls Example for each finished example in run order, then Summary once at the end.

func NewReporter

func NewReporter() *Reporter

NewReporter returns an empty reporter.

func (*Reporter) Documentation

func (r *Reporter) Documentation() string

Documentation renders the documentation-formatter tree: each group heading indented by depth, each example labelled with its outcome suffix.

func (*Reporter) Progress

func (r *Reporter) Progress() string

Progress renders the progress-formatter body (the run of `.`/`F`/`*` chars).

func (*Reporter) Record

func (r *Reporter) Record(e *Example)

Record appends a finished example (with its Result set) in run order.

func (*Reporter) Render

func (r *Reporter) Render(timing Timing) string

Render produces the full progress-formatter report (progress line, blank line, then the summary block) for the given timing — the common one-shot entry point mirroring `rspec`'s default output.

func (*Reporter) RenderDoc

func (r *Reporter) RenderDoc(timing Timing) string

RenderDoc produces the full documentation-formatter report.

func (*Reporter) Summary

func (r *Reporter) Summary(timing Timing) string

Summary renders the pending section, failures section, totals line, and failed-examples rerun list, given a Timing to place in the "Finished in …" line. Timing is caller-supplied so the output is deterministic in tests.

type Result

type Result struct {
	Status Status
	// FailureMessage is the matcher's message (or exception text) on failure.
	FailureMessage string
	// FailureExpression is the source line RSpec prints after "Failure/Error:".
	FailureExpression string
	// PendingReason is shown for pending examples in the pending section.
	PendingReason string
	// ExceptionClass / backtrace are the host seam for errors; the formatter uses
	// them when present but they are environment-specific, so tests focus on the
	// deterministic message body.
	ExceptionClass string
	Backtrace      []string
}

Result is the host-reported outcome of running an example body.

type Status

type Status int

Status is an example's outcome, set by the host after it runs the body.

const (
	// StatusUnknown means the example has not been run.
	StatusUnknown Status = iota
	// Passed means the example met all expectations.
	Passed
	// Failed means an expectation failed or the body raised.
	Failed
	// Pending means the example is pending (expected to fail) — or was skipped.
	Pending
)

type Symbol

type Symbol string

Symbol is a Ruby Symbol (`:name`). Its inspect is the leading-colon form.

type Timing

type Timing struct {
	// Text, when set, is used verbatim (the whole "X seconds (files took …)"
	// clause). Otherwise Run/Load format the standard phrasing.
	Text string
	Run  string // e.g. "0.01 seconds"
	Load string // e.g. "0.03 seconds"
}

Timing is the caller-supplied timing for the "Finished in …" line. RSpec formats it as "Finished in X seconds (files took Y seconds to load)"; the host measures the durations, so a formatter renders whatever the host reports, keeping the rest of the output deterministic.

func (Timing) String

func (t Timing) String() string

Jump to

Keyboard shortcuts

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