mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
Four consolidations, no assertion changed and the same 217 tests passing:
harness gains run and tryRun, retiring 68 copies of context.WithTimeout plus
Run(ctx, Options{}) plus an error check. The seven sites whose error check said
something specific keep their own wording.
mock.FailurePlan retires six wrapper drivers that were the same seven-line
method with a different error constant. The three that need more than an error
on call N stay.
traceStepLine absorbs the 15 local trace-line declarations and readTraceLines is
now the only reader. No local copy disagreed with internal/trace about a JSON
tag. Three raw readers stay on purpose: one decodes a real trace.Step, one
snapshots bytes, one asserts on raw JSON text.
One embeddable base carries the five V8-path methods the five web fakes each
redeclared.
119 lines
4.1 KiB
Go
119 lines
4.1 KiB
Go
package runner
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/verifier"
|
|
)
|
|
|
|
// mustSeededVerifier builds a verifier with a fixed seed so the JS picker's PRNG
|
|
// is reproducible across runs, making the trace byte-stable.
|
|
func mustSeededVerifier(t *testing.T, spec string, seed uint64) *verifier.Verifier {
|
|
t.Helper()
|
|
instance, err := verifier.New(verifier.WithSeed(seed))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := instance.Load(bundleSpec(t, spec)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return instance
|
|
}
|
|
|
|
// goldenSeed and goldenSteps fix the run so the mock-driven trace and summary
|
|
// are byte-reproducible. Set UPDATE_GOLDEN=1 to rewrite the committed goldens;
|
|
// the checked-in files are the asserted source of truth.
|
|
const (
|
|
goldenSeed = 0x5eed
|
|
goldenSteps = 4
|
|
)
|
|
|
|
// canonicalizeTrace rewrites the sole non-deterministic field, "timestamp"
|
|
// (wall-clock at step start), to a fixed zero value so equality compares only
|
|
// the run's observable shape. Every other field is a pure function of the seed,
|
|
// the spec, and the programmed mock hierarchy.
|
|
func canonicalizeTrace(t *testing.T, raw []byte) []byte {
|
|
t.Helper()
|
|
var out bytes.Buffer
|
|
scanner := bufio.NewScanner(bytes.NewReader(raw))
|
|
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
|
for scanner.Scan() {
|
|
var line map[string]json.RawMessage
|
|
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
|
|
t.Fatalf("decode trace line: %v", err)
|
|
}
|
|
line["timestamp"] = json.RawMessage(`"0001-01-01T00:00:00Z"`)
|
|
encoded, err := json.Marshal(line)
|
|
if err != nil {
|
|
t.Fatalf("encode trace line: %v", err)
|
|
}
|
|
out.Write(encoded)
|
|
out.WriteByte('\n')
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
t.Fatalf("scan trace: %v", err)
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func assertGolden(t *testing.T, name string, got []byte) {
|
|
t.Helper()
|
|
path := filepath.Join("testdata", name)
|
|
if os.Getenv("UPDATE_GOLDEN") == "1" {
|
|
if err := os.WriteFile(path, got, 0o644); err != nil {
|
|
t.Fatalf("write golden %s: %v", name, err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read golden %s (run with UPDATE_GOLDEN=1 to create): %v", name, err)
|
|
}
|
|
if !bytes.Equal(want, got) {
|
|
t.Errorf("golden %s mismatch\n--- want ---\n%s\n--- got ---\n%s", name, want, got)
|
|
}
|
|
}
|
|
|
|
// TestGolden_TraceStreamIsReproducible drives a fixed-seed, fixed-step run
|
|
// against the mock driver and snapshots the canonicalized trace.jsonl stream.
|
|
// A drift here means the run is no longer deterministic or the trace shape
|
|
// changed; both must be reviewed, not papered over.
|
|
func TestGolden_TraceStreamIsReproducible(t *testing.T) {
|
|
state := newHarnessWithSpec(t, fixtureSpec)
|
|
state.verifier = mustSeededVerifier(t, fixtureSpec, goldenSeed)
|
|
state.mock.HierarchyJSON = `{"attributes":{"resource-id":"HomeScreen"},"children":[{"attributes":{"resource-id":"next","bounds":"[40,80,240,160]"},"children":[],"clickable":true,"enabled":true}]}`
|
|
|
|
state.run(t, Options{IdleTimeout: 10 * time.Millisecond, MaxSteps: goldenSteps})
|
|
raw, err := os.ReadFile(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGolden(t, "trace.jsonl", canonicalizeTrace(t, raw))
|
|
}
|
|
|
|
// TestGolden_ViolationSummaryIsReproducible drives a known violation and
|
|
// snapshots the rendered summary string produced by RenderSummary (the same
|
|
// function the CLI prints). It also asserts the violated property is named, so
|
|
// a regression that drops the property from the report cannot pass silently.
|
|
func TestGolden_ViolationSummaryIsReproducible(t *testing.T) {
|
|
state := newHarnessWithSpec(t, violationSpec)
|
|
state.verifier = mustSeededVerifier(t, violationSpec, goldenSeed)
|
|
|
|
summary := state.run(t, Options{IdleTimeout: 10 * time.Millisecond, MaxSteps: goldenSteps})
|
|
|
|
var rendered bytes.Buffer
|
|
RenderSummary(&rendered, summary, "android")
|
|
|
|
if !strings.Contains(rendered.String(), "balanceNonNegative") {
|
|
t.Errorf("summary must name the violated property balanceNonNegative, got:\n%s", rendered.String())
|
|
}
|
|
assertGolden(t, "violation-summary.txt", rendered.Bytes())
|
|
}
|