diff --git a/vibes/call_concurrency_test.go b/vibes/call_concurrency_test.go index f34e30b..fc3a1a0 100644 --- a/vibes/call_concurrency_test.go +++ b/vibes/call_concurrency_test.go @@ -42,8 +42,7 @@ type callResult struct { } func TestScriptCallOverlappingCallsKeepFunctionEnvIsolated(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def helper + script := compileScriptDefault(t, `def helper tenant end @@ -51,9 +50,6 @@ def run sync.wait() helper end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } barrier := &blockingSync{ entered: make(chan struct{}, 1), @@ -108,8 +104,7 @@ end`) } func TestScriptCallOverlappingCallsKeepClassVarsIsolated(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`class Counter + script := compileScriptDefault(t, `class Counter @@count = 0 def self.bump @@ -126,9 +121,6 @@ def run Counter.bump Counter.count end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } barrier := &blockingSync{ entered: make(chan struct{}, 1), @@ -181,8 +173,7 @@ end`) } func TestScriptCallRebindsEscapedFunctionsToCurrentCallEnv(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def format_tenant(value) + script := compileScriptDefault(t, `def format_tenant(value) tenant + "-" + value end @@ -193,9 +184,6 @@ end def run_with(fn, value) fn(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } exported, err := script.Call(context.Background(), "export_fn", nil, CallOptions{ Globals: map[string]Value{ @@ -223,8 +211,7 @@ end`) } func TestScriptCallRebindingDoesNotMutateSharedArgMaps(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def format_tenant(value) + script := compileScriptDefault(t, `def format_tenant(value) tenant + "-" + value end @@ -236,9 +223,6 @@ def run(ctx) sync.wait() ctx.fn("value") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } exported, err := script.Call(context.Background(), "export_fn", nil, CallOptions{ Globals: map[string]Value{ @@ -311,7 +295,7 @@ end`) func TestScriptCallPreservesForeignFunctionEnv(t *testing.T) { engine := MustNewEngine(Config{}) - producer, err := engine.Compile(`def helper(value) + producer := compileScriptWithEngine(t, engine, `def helper(value) "foreign-" + value end @@ -322,16 +306,10 @@ end def export_fn wrapper end`) - if err != nil { - t.Fatalf("compile producer failed: %v", err) - } - consumer, err := engine.Compile(`def run_with(fn, value) + consumer := compileScriptWithEngine(t, engine, `def run_with(fn, value) fn(value) end`) - if err != nil { - t.Fatalf("compile consumer failed: %v", err) - } foreignFn, err := producer.Call(context.Background(), "export_fn", nil, CallOptions{}) if err != nil { @@ -351,8 +329,7 @@ end`) } func TestScriptCallRebindsEscapedClassValuesToCurrentCall(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`class Bucket + script := compileScriptDefault(t, `class Bucket @@count = 0 def self.bump @@ -373,9 +350,6 @@ def run_with(klass) klass.bump klass.snapshot end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } exportedClass, err := script.Call(context.Background(), "export_class", nil, CallOptions{ Globals: map[string]Value{ @@ -410,8 +384,7 @@ end`) } func TestScriptCallRebindsEscapedInstancesToCurrentCallState(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`class Bucket + script := compileScriptDefault(t, `class Bucket @@count = 0 def initialize(name) @@ -431,9 +404,6 @@ end def run_with(bucket) bucket.report end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } exportedInstance, err := script.Call(context.Background(), "export_instance", []Value{NewString("seed")}, CallOptions{ Globals: map[string]Value{ @@ -471,13 +441,9 @@ end`) } func TestScriptCallRebindingPreservesHashAndObjectKindsForAliasedMaps(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run(a, b) + script := compileScriptDefault(t, `def run(a, b) [a, b] end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } shared := map[string]Value{"x": NewInt(1)} hashVal := NewHash(shared) diff --git a/vibes/capability_common_test.go b/vibes/capability_common_test.go index e5babaa..a1a6563 100644 --- a/vibes/capability_common_test.go +++ b/vibes/capability_common_test.go @@ -1,7 +1,6 @@ package vibes import ( - "strings" "testing" ) @@ -26,9 +25,7 @@ func TestValidateCapabilityTypedValueUsesCompositeTypeChecks(t *testing.T) { if err == nil { t.Fatalf("expected composite type mismatch") } - if !strings.Contains(err.Error(), "payload expected array, got array") { - t.Fatalf("unexpected composite type mismatch: %v", err) - } + requireErrorContains(t, err, "payload expected array, got array") } func TestValidateCapabilityTypedValueUsesShapeTypeChecks(t *testing.T) { @@ -50,7 +47,5 @@ func TestValidateCapabilityTypedValueUsesShapeTypeChecks(t *testing.T) { if err == nil { t.Fatalf("expected shape type mismatch") } - if !strings.Contains(err.Error(), "payload expected { id: string }, got { extra: int, id: string }") { - t.Fatalf("unexpected shape type mismatch: %v", err) - } + requireErrorContains(t, err, "payload expected { id: string }, got { extra: int, id: string }") } diff --git a/vibes/capability_context_test.go b/vibes/capability_context_test.go index e6fbdf0..c5e5cf8 100644 --- a/vibes/capability_context_test.go +++ b/vibes/capability_context_test.go @@ -2,18 +2,13 @@ package vibes import ( "context" - "strings" "testing" ) func TestContextCapabilityResolver(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() ctx.user.id end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } type ctxKey string resolver := func(ctx context.Context) (Value, error) { @@ -30,25 +25,18 @@ end`) ctx := context.WithValue(context.Background(), ctxKey("user_id"), "player-1") ctx = context.WithValue(ctx, ctxKey("role"), "coach") - result, err := script.Call(ctx, "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewContextCapability("ctx", resolver)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, ctx, script, "run", nil, callOptionsWithCapabilities( + MustNewContextCapability("ctx", resolver), + )) if result.Kind() != KindString || result.String() != "player-1" { t.Fatalf("unexpected result: %#v", result) } } func TestContextCapabilityRejectsCallableValue(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() ctx.user.id end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } resolver := func(ctx context.Context) (Value, error) { return NewObject(map[string]Value{ @@ -61,49 +49,31 @@ end`) }), nil } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewContextCapability("ctx", resolver)}, - }) - if err == nil { - t.Fatalf("expected callable context data error") - } - if got := err.Error(); !strings.Contains(got, "ctx capability value must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewContextCapability("ctx", resolver), + )) + requireErrorContains(t, err, "ctx capability value must be data-only") } func TestContextCapabilityRejectsNonObjectValue(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() 1 end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } resolver := func(ctx context.Context) (Value, error) { return NewString("invalid"), nil } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewContextCapability("ctx", resolver)}, - }) - if err == nil { - t.Fatalf("expected resolver shape error") - } - if got := err.Error(); !strings.Contains(got, "ctx capability resolver must return hash/object") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewContextCapability("ctx", resolver), + )) + requireErrorContains(t, err, "ctx capability resolver must return hash/object") } func TestContextCapabilityRejectsCyclicValue(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() ctx end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } resolver := func(context.Context) (Value, error) { cyclic := map[string]Value{} @@ -111,24 +81,18 @@ end`) return NewHash(cyclic), nil } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewContextCapability("ctx", resolver)}, - }) - if err == nil { - t.Fatalf("expected cyclic value error") - } - if got := err.Error(); !strings.Contains(got, "ctx capability value must not contain cyclic references") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewContextCapability("ctx", resolver), + )) + requireErrorContains(t, err, "ctx capability value must not contain cyclic references") } func TestNewContextCapabilityRejectsInvalidArguments(t *testing.T) { resolver := func(context.Context) (Value, error) { return NewObject(map[string]Value{}), nil } - if _, err := NewContextCapability("", resolver); err == nil || !strings.Contains(err.Error(), "name must be non-empty") { - t.Fatalf("expected empty name error, got %v", err) - } - if _, err := NewContextCapability("ctx", nil); err == nil || !strings.Contains(err.Error(), "requires a resolver") { - t.Fatalf("expected nil resolver error, got %v", err) - } + _, err := NewContextCapability("", resolver) + requireErrorContains(t, err, "name must be non-empty") + + _, err = NewContextCapability("ctx", nil) + requireErrorContains(t, err, "requires a resolver") } diff --git a/vibes/capability_contracts_test.go b/vibes/capability_contracts_test.go index 57fdf73..b667da1 100644 --- a/vibes/capability_contracts_test.go +++ b/vibes/capability_contracts_test.go @@ -3,7 +3,6 @@ package vibes import ( "context" "fmt" - "strings" "testing" ) @@ -482,13 +481,10 @@ func (stdlibContractLeakProbeCapability) CapabilityContracts() map[string]Capabi } func TestCapabilityContractRejectsInvalidArguments(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() probe.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -497,22 +493,17 @@ end`) if err == nil { t.Fatalf("expected contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.call expects a single int argument") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "probe.call expects a single int argument") if invocations != 0 { t.Fatalf("capability should not execute when arg contract fails") } } func TestCapabilityContractRejectsInvalidReturnValue(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() probe.call(1) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -528,22 +519,17 @@ end`) if err == nil { t.Fatalf("expected return contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.call must return string") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "probe.call must return string") if invocations != 1 { t.Fatalf("expected capability to execute once before return validation, got %d", invocations) } } func TestDuplicateCapabilityContractsFailBinding(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() 1 end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error _, err = script.Call(context.Background(), "run", nil, CallOptions{ Capabilities: []CapabilityAdapter{ @@ -554,21 +540,16 @@ end`) if err == nil { t.Fatalf("expected duplicate contract error") } - if got := err.Error(); !strings.Contains(got, "duplicate capability contract for dup.call") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "duplicate capability contract for dup.call") } func TestCapabilityContractsDoNotAttachByGlobalBuiltinName(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() base = { a: 1 } override = { b: 2 } base.merge(override) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error result, err := script.Call(context.Background(), "run", nil, CallOptions{ Capabilities: []CapabilityAdapter{unrelatedNamedContractCapability{}}, @@ -585,13 +566,10 @@ end`) } func TestCapabilityContractsTraverseInstanceValues(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() box.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -602,22 +580,17 @@ end`) if err == nil { t.Fatalf("expected instance contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "probe.call expects int") if invocations != 0 { t.Fatalf("instance capability should not execute when contract fails") } } func TestCapabilityContractsTraverseClassValues(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() holder.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -628,23 +601,18 @@ end`) if err == nil { t.Fatalf("expected class contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.class_call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "probe.class_call expects int") if invocations != 0 { t.Fatalf("class capability should not execute when contract fails") } } func TestCapabilityContractsBindForFactoryReturnedBuiltins(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() worker = factory.make() worker.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -655,23 +623,18 @@ end`) if err == nil { t.Fatalf("expected factory-returned contract validation error") } - if got := err.Error(); !strings.Contains(got, "factory.call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "factory.call expects int") if invocations != 0 { t.Fatalf("factory capability should not execute when contract fails") } } func TestCapabilityContractsBindAfterReceiverMutation(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() mut.install() mut.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -682,22 +645,17 @@ end`) if err == nil { t.Fatalf("expected receiver-mutation contract validation error") } - if got := err.Error(); !strings.Contains(got, "mut.call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "mut.call expects int") if invocations != 0 { t.Fatalf("mutated receiver capability should not execute when contract fails") } } func TestCapabilityContractsAreScopedPerAdapter(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() foo.call("ok") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 result, err := script.Call(context.Background(), "run", nil, CallOptions{ @@ -718,14 +676,11 @@ end`) } func TestCapabilityContractsBindAfterSiblingScopeMutation(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() publisher.install() peer.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -736,23 +691,18 @@ end`) if err == nil { t.Fatalf("expected sibling-mutation contract validation error") } - if got := err.Error(); !strings.Contains(got, "peer.call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "peer.call expects int") if invocations != 0 { t.Fatalf("sibling mutation capability should not execute when contract fails") } } func TestCapabilityContractsDoNotAttachToForeignBuiltinsByName(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() publisher.install() publisher.call("ok") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error shared := &foreignBuiltinRef{} invocations := 0 @@ -774,15 +724,12 @@ end`) } func TestCapabilityContractsBindAfterArgumentMutation(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() target = {} cap.install(target) target.call("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 _, err = script.Call(context.Background(), "run", nil, CallOptions{ @@ -793,24 +740,19 @@ end`) if err == nil { t.Fatalf("expected argument-mutation contract validation error") } - if got := err.Error(); !strings.Contains(got, "cap.call expects int") { - t.Fatalf("unexpected error: %s", got) - } + requireErrorContains(t, err, "cap.call expects int") if invocations != 0 { t.Fatalf("argument mutation capability should not execute when contract fails") } } func TestCapabilityContractsDoNotHijackForeignBuiltinsFromArguments(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() target = { passthrough: foreign.call } cap2.install(target) target.passthrough("ok") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error invocations := 0 result, err := script.Call(context.Background(), "run", nil, CallOptions{ @@ -831,15 +773,12 @@ end`) } func TestCapabilityContractsDoNotHijackReceiverStoredForeignBuiltins(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() cap.foreign = { a: 1 }.merge cap.touch() cap.foreign({ b: 2 }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error result, err := script.Call(context.Background(), "run", nil, CallOptions{ Capabilities: []CapabilityAdapter{ @@ -858,8 +797,7 @@ end`) } func TestCapabilityContractsDoNotAttachToExpandedStdlibBuiltinsByName(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() cap.touch() parsed = JSON.parse("{\"name\":\"alex\"}") { @@ -871,9 +809,7 @@ func TestCapabilityContractsDoNotAttachToExpandedStdlibBuiltinsByName(t *testing remap_value: { name: "Alex" }.remap_keys({ name: :player_name }).fetch(:player_name) } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error result, err := script.Call(context.Background(), "run", nil, CallOptions{ Capabilities: []CapabilityAdapter{ @@ -908,8 +844,7 @@ end`) } func TestCapabilityContractsStayEnforcedThroughExpandedStdlibTransforms(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def call_through_transforms() + script := compileScriptDefault(t, `def call_through_transforms() hash_handler = { handler: probe.call }.remap_keys({ handler: :run }).fetch(:run) chunk_handler = [probe.call].chunk(1).first.first { @@ -927,9 +862,7 @@ def fail_through_chunk() handler = [probe.call].chunk(1).first.first handler("bad") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + var err error successInvocations := 0 okResult, err := script.Call(context.Background(), "call_through_transforms", nil, CallOptions{ @@ -962,9 +895,7 @@ end`) if err == nil { t.Fatalf("expected remap path contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.call expects a single int argument") { - t.Fatalf("unexpected remap path error: %s", got) - } + requireErrorContains(t, err, "probe.call expects a single int argument") if remapInvocations != 0 { t.Fatalf("contract should reject remap path before invoke, got %d calls", remapInvocations) } @@ -978,9 +909,7 @@ end`) if err == nil { t.Fatalf("expected chunk path contract validation error") } - if got := err.Error(); !strings.Contains(got, "probe.call expects a single int argument") { - t.Fatalf("unexpected chunk path error: %s", got) - } + requireErrorContains(t, err, "probe.call expects a single int argument") if chunkInvocations != 0 { t.Fatalf("contract should reject chunk path before invoke, got %d calls", chunkInvocations) } diff --git a/vibes/capability_db_test.go b/vibes/capability_db_test.go index 89f81a3..794fbf3 100644 --- a/vibes/capability_db_test.go +++ b/vibes/capability_db_test.go @@ -2,7 +2,6 @@ package vibes import ( "context" - "strings" "testing" ) @@ -86,22 +85,15 @@ func TestDBCapabilityFindAndContextPropagation(t *testing.T) { "id": NewString("player-7"), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run(id) + script := compileScriptDefault(t, `def run(id) db.find("Player", id, include: "team") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } type ctxKey string ctx := context.WithValue(context.Background(), ctxKey("trace"), "enabled") - result, err := script.Call(ctx, "run", []Value{NewString("player-7")}, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, ctx, script, "run", []Value{NewString("player-7")}, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) if result.Kind() != KindHash || result.Hash()["id"].String() != "player-7" { t.Fatalf("unexpected result: %#v", result) } @@ -132,24 +124,17 @@ func TestDBCapabilityEachInvokesBlock(t *testing.T) { NewHash(map[string]Value{"amount": NewInt(5)}), }, } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() total = 0 db.each("ScoreEntry", where: { player_id: "p-1" }) do |entry| total = total + entry[:amount] end total end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) if result.Kind() != KindInt || result.Int() != 30 { t.Fatalf("unexpected result: %#v", result) } @@ -172,8 +157,7 @@ func TestDBCapabilityEachLoopControlCannotCrossCallbackBoundary(t *testing.T) { NewHash(map[string]Value{"id": NewString("p-2")}), }, } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def break_from_callback() + script := compileScriptDefault(t, `def break_from_callback() db.each("Player") do |row| if row[:id] == "p-2" break @@ -188,90 +172,56 @@ def next_from_callback() end end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "break_from_callback", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil || !strings.Contains(err.Error(), "break used outside of loop") { - t.Fatalf("expected callback break outside-loop error, got %v", err) - } + err := callScriptErr(t, context.Background(), script, "break_from_callback", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "break used outside of loop") - _, err = script.Call(context.Background(), "next_from_callback", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil || !strings.Contains(err.Error(), "next used outside of loop") { - t.Fatalf("expected callback next outside-loop error, got %v", err) - } + err = callScriptErr(t, context.Background(), script, "next_from_callback", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "next used outside of loop") } func TestDBCapabilityRejectsCallableUpdateAttributes(t *testing.T) { stub := &dbCapabilityStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def helper(value) + script := compileScriptDefault(t, `def helper(value) value end def run() db.update("Player", "p-1", { callback: helper }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil { - t.Fatalf("expected callable attributes error") - } - if got := err.Error(); !strings.Contains(got, "db.update attributes must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "db.update attributes must be data-only") } func TestDBCapabilityRejectsNonHashUpdateAttributes(t *testing.T) { stub := &dbCapabilityStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() db.update("Player", "p-1", 123) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil { - t.Fatalf("expected non-hash attributes error") - } - if got := err.Error(); !strings.Contains(got, "db.update attributes expected hash, got int") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "db.update attributes expected hash, got int") } func TestDBCapabilityEachRequiresBlock(t *testing.T) { stub := &dbCapabilityStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() db.each("Player") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil { - t.Fatalf("expected block error") - } - if got := err.Error(); !strings.Contains(got, "db.each requires a block") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "db.each requires a block") } func TestDBCapabilityRejectsCallableReturn(t *testing.T) { @@ -282,23 +232,14 @@ func TestDBCapabilityRejectsCallableReturn(t *testing.T) { }), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() db.find("Player", "p-1") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil { - t.Fatalf("expected return contract error") - } - if got := err.Error(); !strings.Contains(got, "db.find return value must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "db.find return value must be data-only") } func TestDBCapabilityRejectsCallableRows(t *testing.T) { @@ -311,25 +252,16 @@ func TestDBCapabilityRejectsCallableRows(t *testing.T) { }), }, } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() db.each("Player") do |row| row end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }) - if err == nil { - t.Fatalf("expected callable row error") - } - if got := err.Error(); !strings.Contains(got, "db.each row 0 must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) + requireErrorContains(t, err, "db.each row 0 must be data-only") } func TestDBCapabilityReturnsAreClonedFromHostState(t *testing.T) { @@ -347,23 +279,17 @@ func TestDBCapabilityReturnsAreClonedFromHostState(t *testing.T) { }), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() player = db.find("Player", "p-1") player[:profile][:name] = "script" rows = db.query("Player") rows[0][:profile][:name] = "row-script" end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewDBCapability("db", stub)}, - }); err != nil { - t.Fatalf("call failed: %v", err) - } + callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", stub), + )) findName := stub.findResult.Hash()["profile"].Hash()["name"] if findName.Kind() != KindString || findName.String() != "host" { @@ -379,17 +305,14 @@ end`) func TestNewDBCapabilityRejectsInvalidArguments(t *testing.T) { stub := &dbCapabilityStub{} - if _, err := NewDBCapability("", stub); err == nil || !strings.Contains(err.Error(), "name must be non-empty") { - t.Fatalf("expected empty name error, got %v", err) - } + _, err := NewDBCapability("", stub) + requireErrorContains(t, err, "name must be non-empty") var db Database - if _, err := NewDBCapability("db", db); err == nil || !strings.Contains(err.Error(), "requires a non-nil implementation") { - t.Fatalf("expected nil db error, got %v", err) - } + _, err = NewDBCapability("db", db) + requireErrorContains(t, err, "requires a non-nil implementation") var typedNil *dbCapabilityStub - if _, err := NewDBCapability("db", typedNil); err == nil || !strings.Contains(err.Error(), "requires a non-nil implementation") { - t.Fatalf("expected typed nil db error, got %v", err) - } + _, err = NewDBCapability("db", typedNil) + requireErrorContains(t, err, "requires a non-nil implementation") } diff --git a/vibes/capability_events_test.go b/vibes/capability_events_test.go index 18e1bb1..13344ee 100644 --- a/vibes/capability_events_test.go +++ b/vibes/capability_events_test.go @@ -2,7 +2,6 @@ package vibes import ( "context" - "strings" "testing" ) @@ -24,22 +23,15 @@ func (s *eventsCapabilityStub) Publish(ctx context.Context, req EventPublishRequ func TestEventsCapabilityPublish(t *testing.T) { stub := &eventsCapabilityStub{publishResult: NewBool(true)} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() events.publish("players_totals", { id: "p-1", total: "55.00 USD" }, trace: "abc") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } type ctxKey string ctx := context.WithValue(context.Background(), ctxKey("request_id"), "req-1") - result, err := script.Call(ctx, "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewEventsCapability("events", stub)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, ctx, script, "run", nil, callOptionsWithCapabilities( + MustNewEventsCapability("events", stub), + )) if result.Kind() != KindBool || !result.Bool() { t.Fatalf("unexpected result: %#v", result) } @@ -63,48 +55,30 @@ end`) func TestEventsCapabilityRejectsCallablePayload(t *testing.T) { stub := &eventsCapabilityStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def helper(value) + script := compileScriptDefault(t, `def helper(value) value end def run() events.publish("topic", { callback: helper }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewEventsCapability("events", stub)}, - }) - if err == nil { - t.Fatalf("expected callable payload error") - } - if got := err.Error(); !strings.Contains(got, "events.publish payload must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewEventsCapability("events", stub), + )) + requireErrorContains(t, err, "events.publish payload must be data-only") } func TestEventsCapabilityRejectsNonHashPayload(t *testing.T) { stub := &eventsCapabilityStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() events.publish("topic", 42) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewEventsCapability("events", stub)}, - }) - if err == nil { - t.Fatalf("expected non-hash payload error") - } - if got := err.Error(); !strings.Contains(got, "events.publish payload expected hash, got int") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewEventsCapability("events", stub), + )) + requireErrorContains(t, err, "events.publish payload expected hash, got int") } func TestEventsCapabilityRejectsCallableReturn(t *testing.T) { @@ -115,23 +89,14 @@ func TestEventsCapabilityRejectsCallableReturn(t *testing.T) { }), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() events.publish("topic", { id: "p-1" }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewEventsCapability("events", stub)}, - }) - if err == nil { - t.Fatalf("expected return contract error") - } - if got := err.Error(); !strings.Contains(got, "events.publish return value must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewEventsCapability("events", stub), + )) + requireErrorContains(t, err, "events.publish return value must be data-only") } func TestEventsCapabilityReturnsAreClonedFromHostState(t *testing.T) { @@ -142,20 +107,14 @@ func TestEventsCapabilityReturnsAreClonedFromHostState(t *testing.T) { }), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() event = events.publish("topic", { id: "p-1" }) event[:meta][:trace] = "script" end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewEventsCapability("events", stub)}, - }); err != nil { - t.Fatalf("call failed: %v", err) - } + callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewEventsCapability("events", stub), + )) trace := stub.publishResult.Hash()["meta"].Hash()["trace"] if trace.Kind() != KindString || trace.String() != "host" { @@ -165,17 +124,14 @@ end`) func TestNewEventsCapabilityRejectsInvalidArguments(t *testing.T) { stub := &eventsCapabilityStub{} - if _, err := NewEventsCapability("", stub); err == nil || !strings.Contains(err.Error(), "name must be non-empty") { - t.Fatalf("expected empty name error, got %v", err) - } + _, err := NewEventsCapability("", stub) + requireErrorContains(t, err, "name must be non-empty") var publisher EventPublisher - if _, err := NewEventsCapability("events", publisher); err == nil || !strings.Contains(err.Error(), "requires a non-nil implementation") { - t.Fatalf("expected nil publisher error, got %v", err) - } + _, err = NewEventsCapability("events", publisher) + requireErrorContains(t, err, "requires a non-nil implementation") var typedNil *eventsCapabilityStub - if _, err := NewEventsCapability("events", typedNil); err == nil || !strings.Contains(err.Error(), "requires a non-nil implementation") { - t.Fatalf("expected typed nil publisher error, got %v", err) - } + _, err = NewEventsCapability("events", typedNil) + requireErrorContains(t, err, "requires a non-nil implementation") } diff --git a/vibes/capability_foundations_test.go b/vibes/capability_foundations_test.go index f86ae9b..458441c 100644 --- a/vibes/capability_foundations_test.go +++ b/vibes/capability_foundations_test.go @@ -2,7 +2,6 @@ package vibes import ( "context" - "strings" "testing" ) @@ -16,15 +15,11 @@ func TestCapabilityFoundationsMixedAdapters(t *testing.T) { events := &eventsCapabilityStub{publishResult: NewNil()} jobs := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run(player_id) + script := compileScriptDefault(t, `def run(player_id) player = db.find("Player", player_id) events.publish("player_seen", { id: player[:id], actor: ctx.user.id }) jobs.enqueue("audit_player", { id: player[:id], raised: player[:raised] }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } ctxCap := MustNewContextCapability("ctx", func(context.Context) (Value, error) { return NewObject(map[string]Value{ @@ -34,17 +29,12 @@ end`) }), nil }) - result, err := script.Call(context.Background(), "run", []Value{NewString("player-1")}, CallOptions{ - Capabilities: []CapabilityAdapter{ - MustNewDBCapability("db", db), - MustNewEventsCapability("events", events), - ctxCap, - MustNewJobQueueCapability("jobs", jobs), - }, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", []Value{NewString("player-1")}, callOptionsWithCapabilities( + MustNewDBCapability("db", db), + MustNewEventsCapability("events", events), + ctxCap, + MustNewJobQueueCapability("jobs", jobs), + )) if result.Kind() != KindString || result.String() != "queued" { t.Fatalf("unexpected result: %#v", result) } @@ -74,29 +64,18 @@ func TestCapabilityFoundationsEachRespectsStepQuota(t *testing.T) { } db := &dbCapabilityStub{eachRows: rows} - engine := MustNewEngine(Config{StepQuota: 50}) - script, err := engine.Compile(`def run() + script := compileScriptWithConfig(t, Config{StepQuota: 50}, `def run() total = 0 db.each("ScoreEntry") do |row| total = total + row[:amount] end total end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{ - MustNewDBCapability("db", db), - }, - }) - if err == nil { - t.Fatalf("expected step quota error") - } - if got := err.Error(); !strings.Contains(got, "step quota exceeded") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", db), + )) + requireErrorContains(t, err, "step quota exceeded") } func TestCapabilityFoundationsEachNoopBlockRespectsStepQuota(t *testing.T) { @@ -106,26 +85,15 @@ func TestCapabilityFoundationsEachNoopBlockRespectsStepQuota(t *testing.T) { } db := &dbCapabilityStub{eachRows: rows} - engine := MustNewEngine(Config{StepQuota: 20}) - script, err := engine.Compile(`def run() + script := compileScriptWithConfig(t, Config{StepQuota: 20}, `def run() db.each("ScoreEntry") do |row| end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{ - MustNewDBCapability("db", db), - }, - }) - if err == nil { - t.Fatalf("expected step quota error") - } - if got := err.Error(); !strings.Contains(got, "step quota exceeded") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", db), + )) + requireErrorContains(t, err, "step quota exceeded") } func TestCapabilityFoundationsEachRespectsRecursionLimit(t *testing.T) { @@ -135,8 +103,7 @@ func TestCapabilityFoundationsEachRespectsRecursionLimit(t *testing.T) { }, } - engine := MustNewEngine(Config{RecursionLimit: 5, StepQuota: 10_000}) - script, err := engine.Compile(`def recurse(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 5, StepQuota: 10_000}, `def recurse(n) if n <= 0 0 else @@ -149,19 +116,9 @@ def run() recurse(20) end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{ - MustNewDBCapability("db", db), - }, - }) - if err == nil { - t.Fatalf("expected recursion limit error") - } - if got := err.Error(); !strings.Contains(got, "recursion depth exceeded") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewDBCapability("db", db), + )) + requireErrorContains(t, err, "recursion depth exceeded") } diff --git a/vibes/capability_jobqueue_test.go b/vibes/capability_jobqueue_test.go index 398c6e5..381dc87 100644 --- a/vibes/capability_jobqueue_test.go +++ b/vibes/capability_jobqueue_test.go @@ -73,23 +73,16 @@ func (s *sharedReturnQueue) Retry(ctx context.Context, req JobQueueRetryRequest) func TestJobQueueCapabilityEnqueue(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", { foo: "bar" }, delay: 2.seconds, key: "abc", queue: "standard") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } type ctxKey string ctx := context.WithValue(context.Background(), ctxKey("trace"), "on") - result, err := script.Call(ctx, "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, ctx, script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) if result.Kind() != KindString || result.String() != "queued" { t.Fatalf("unexpected enqueue result: %#v", result) } @@ -133,20 +126,13 @@ end`) func TestJobQueueCapabilityRetry(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.retry("job-7", attempts: 3, priority: "high") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) if result.Kind() != KindBool || !result.Bool() { t.Fatalf("unexpected retry result: %#v", result) } @@ -170,23 +156,16 @@ end`) } func TestJobQueueCapabilityEnqueueOptionsAreClonedFromScriptState(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() payload = { foo: "script-payload" } meta = { trace: "script-meta" } jobs.enqueue("demo", payload, meta: meta) { payload: payload[:foo], trace: meta[:trace] } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", mutatingInputQueue{})}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", mutatingInputQueue{}), + )) hash := result.Hash() if hash["payload"].Kind() != KindString || hash["payload"].String() != "script-payload" { t.Fatalf("script payload was mutated by host: %#v", result) @@ -197,23 +176,16 @@ end`) } func TestJobQueueCapabilityRetryOptionsAreClonedFromScriptState(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() arg = { attempt: { value: "script-attempt" } } kw = { value: "script-kw" } jobs.retry("job-1", arg, kw: kw) { attempt: arg[:attempt][:value], kw: kw[:value] } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", mutatingInputQueue{})}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", mutatingInputQueue{}), + )) hash := result.Hash() if hash["attempt"].Kind() != KindString || hash["attempt"].String() != "script-attempt" { t.Fatalf("script retry arg was mutated by host: %#v", result) @@ -236,23 +208,17 @@ func TestJobQueueCapabilityReturnsAreClonedFromHostState(t *testing.T) { }), }), } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() queued = jobs.enqueue("demo", { foo: "bar" }) queued[:meta][:status] = "script-enqueue" retried = jobs.retry("job-1") retried[:meta][:status] = "script-retry" end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }); err != nil { - t.Fatalf("call failed: %v", err) - } + callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) enqueueStatus := stub.enqueueResult.Hash()["meta"].Hash()["status"] if enqueueStatus.Kind() != KindString || enqueueStatus.String() != "host-enqueue" { @@ -267,66 +233,43 @@ end`) func TestJobQueueCapabilityRejectsInvalidPayload(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", 42) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected error for invalid payload") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue payload expected hash, got int") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.enqueue payload expected hash, got int") } func TestJobQueueCapabilityRejectsCallablePayload(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def helper(value) + script := compileScriptDefault(t, `def helper(value) value end def run() jobs.enqueue("demo", { callback: helper }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected callable payload contract error") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue payload must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.enqueue payload must be data-only") } func TestNilCapabilityAdapterFiltering(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("test", { foo: "bar" }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{nil, MustNewJobQueueCapability("jobs", stub), nil}, - }) - if err != nil { - t.Fatalf("call failed: %v", err) - } + result := callScript(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + nil, + MustNewJobQueueCapability("jobs", stub), + nil, + )) if result.Kind() != KindString || result.String() != "queued" { t.Fatalf("unexpected result: %#v", result) } @@ -337,106 +280,61 @@ end`) func TestJobQueueRejectsNegativeDelay(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", { foo: "bar" }, delay: -5) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected error for negative delay") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue delay must be non-negative") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.enqueue delay must be non-negative") } func TestJobQueueRejectsUnexpectedEnqueuePositionalArgs(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", { foo: "bar" }, { extra: true }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected positional arg error") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue expects job name and payload") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.enqueue expects job name and payload") } func TestJobQueueRejectsEmptyKey(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", { foo: "bar" }, key: "") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected error for empty key") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue key must be non-empty") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.enqueue key must be non-empty") } func TestJobQueueRejectsUnexpectedRetryPositionalArgs(t *testing.T) { stub := &jobQueueStub{} - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.retry("job-7", { attempts: 1 }, { force: true }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", stub)}, - }) - if err == nil { - t.Fatalf("expected retry positional arg error") - } - if got := err.Error(); !strings.Contains(got, "jobs.retry expects job id and optional options hash") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", stub), + )) + requireErrorContains(t, err, "jobs.retry expects job id and optional options hash") } func TestJobQueueRejectsCallableReturnValue(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run() + script := compileScriptDefault(t, `def run() jobs.enqueue("demo", { foo: "bar" }) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{ - Capabilities: []CapabilityAdapter{MustNewJobQueueCapability("jobs", invalidReturnQueue{})}, - }) - if err == nil { - t.Fatalf("expected return contract error") - } - if got := err.Error(); !strings.Contains(got, "jobs.enqueue return value must be data-only") { - t.Fatalf("unexpected error: %s", got) - } + err := callScriptErr(t, context.Background(), script, "run", nil, callOptionsWithCapabilities( + MustNewJobQueueCapability("jobs", invalidReturnQueue{}), + )) + requireErrorContains(t, err, "jobs.enqueue return value must be data-only") } func TestNewJobQueueCapabilityRejectsEmptyName(t *testing.T) { diff --git a/vibes/capability_test_helpers_test.go b/vibes/capability_test_helpers_test.go new file mode 100644 index 0000000..7c74a60 --- /dev/null +++ b/vibes/capability_test_helpers_test.go @@ -0,0 +1,119 @@ +package vibes + +import ( + "context" + "os" + "strings" + "testing" +) + +func compileScriptWithConfig(t testing.TB, cfg Config, source string) *Script { + t.Helper() + engine := MustNewEngine(cfg) + script, err := engine.Compile(source) + if err != nil { + t.Fatalf("compile failed: %v", err) + } + return script +} + +func compileScriptDefault(t testing.TB, source string) *Script { + t.Helper() + return compileScriptWithConfig(t, Config{}, source) +} + +func compileScriptFromFileWithConfig(t testing.TB, cfg Config, path string) *Script { + t.Helper() + source, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return compileScriptWithConfig(t, cfg, string(source)) +} + +func compileScriptFromFileDefault(t testing.TB, path string) *Script { + t.Helper() + return compileScriptFromFileWithConfig(t, Config{}, path) +} + +func compileScriptFromFileWithEngine(t testing.TB, engine *Engine, path string) *Script { + t.Helper() + source, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return compileScriptWithEngine(t, engine, string(source)) +} + +func compileScriptWithEngine(t testing.TB, engine *Engine, source string) *Script { + t.Helper() + script, err := engine.Compile(source) + if err != nil { + t.Fatalf("compile failed: %v", err) + } + return script +} + +func compileScriptErrorWithConfig(t testing.TB, cfg Config, source string) error { + t.Helper() + engine := MustNewEngine(cfg) + _, err := engine.Compile(source) + if err == nil { + t.Fatalf("expected compile to fail") + } + return err +} + +func compileScriptErrorDefault(t testing.TB, source string) error { + t.Helper() + return compileScriptErrorWithConfig(t, Config{}, source) +} + +func requireCompileErrorContainsWithConfig(t testing.TB, cfg Config, source string, want string) { + t.Helper() + err := compileScriptErrorWithConfig(t, cfg, source) + requireErrorContains(t, err, want) +} + +func requireCompileErrorContainsDefault(t testing.TB, source string, want string) { + t.Helper() + requireCompileErrorContainsWithConfig(t, Config{}, source, want) +} + +func callScript(t testing.TB, ctx context.Context, script *Script, fn string, args []Value, opts CallOptions) Value { + t.Helper() + result, err := script.Call(ctx, fn, args, opts) + if err != nil { + t.Fatalf("call failed: %v", err) + } + return result +} + +func callScriptErr(t testing.TB, ctx context.Context, script *Script, fn string, args []Value, opts CallOptions) error { + t.Helper() + _, err := script.Call(ctx, fn, args, opts) + if err == nil { + t.Fatalf("expected call to fail") + } + return err +} + +func requireCallErrorContains(t testing.TB, script *Script, fn string, args []Value, opts CallOptions, want string) { + t.Helper() + err := callScriptErr(t, context.Background(), script, fn, args, opts) + requireErrorContains(t, err, want) +} + +func requireErrorContains(t testing.TB, err error, want string) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", want) + } + if got := err.Error(); !strings.Contains(got, want) { + t.Fatalf("unexpected error: %s", got) + } +} + +func callOptionsWithCapabilities(capabilities ...CapabilityAdapter) CallOptions { + return CallOptions{Capabilities: capabilities} +} diff --git a/vibes/examples_test.go b/vibes/examples_test.go index 8495f2c..f627afa 100644 --- a/vibes/examples_test.go +++ b/vibes/examples_test.go @@ -3,9 +3,7 @@ package vibes import ( "context" "maps" - "os" "path/filepath" - "strings" "testing" ) @@ -2041,9 +2039,7 @@ func TestExamples(t *testing.T) { if err == nil { t.Fatalf("expected error %q, got nil", tc.wantErr) } - if !strings.Contains(err.Error(), tc.wantErr) { - t.Fatalf("expected error containing %q, got %q", tc.wantErr, err.Error()) - } + requireErrorContains(t, err, tc.wantErr) } else { if err != nil { t.Fatalf("unexpected error: %v", err) @@ -2064,16 +2060,7 @@ func compileExample(t *testing.T, rel string) *Script { func compileExampleWithConfig(t *testing.T, rel string, cfg Config) *Script { t.Helper() path := filepath.Join("..", "examples", rel) - source, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read %s: %v", rel, err) - } - engine := MustNewEngine(cfg) - script, err := engine.Compile(string(source)) - if err != nil { - t.Fatalf("compile %s: %v", rel, err) - } - return script + return compileScriptFromFileWithConfig(t, cfg, path) } func makeBuiltin(name string, fn builtinAdapter) Value { diff --git a/vibes/execution_benchmark_test.go b/vibes/execution_benchmark_test.go index 823a7bf..01396ac 100644 --- a/vibes/execution_benchmark_test.go +++ b/vibes/execution_benchmark_test.go @@ -35,17 +35,13 @@ func benchmarkEngine() *Engine { } func BenchmarkExecutionArithmeticLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(n) total = 0 for i in 1..n total = total + i end total end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } args := []Value{NewInt(400)} b.ReportAllocs() @@ -58,8 +54,7 @@ end`) } func BenchmarkExecutionArrayPipeline(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(values) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(values) mapped = values.map do |v| v + 1 end @@ -72,9 +67,6 @@ func BenchmarkExecutionArrayPipeline(b *testing.B) { acc + v end end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } values := make([]Value, 600) for i := range values { @@ -92,8 +84,7 @@ end`) } func BenchmarkExecutionMethodDispatchLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`class Counter + script := compileScriptWithEngine(b, benchmarkEngine(), `class Counter def initialize(seed) @value = seed end @@ -114,9 +105,6 @@ def run(n) end counter.value end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } args := []Value{NewInt(300)} b.ReportAllocs() @@ -129,8 +117,7 @@ end`) } func BenchmarkExecutionCapabilityFindLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(n) total = 0 for i in 1..n row = db.find("Player", "player-1") @@ -138,9 +125,6 @@ func BenchmarkExecutionCapabilityFindLoop(b *testing.B) { end total end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } args := []Value{NewInt(300)} opts := CallOptions{ @@ -159,8 +143,7 @@ end`) } func BenchmarkExecutionJSONParseLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(raw, n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(raw, n) total = 0 for i in 1..n payload = JSON.parse(raw) @@ -168,9 +151,6 @@ func BenchmarkExecutionJSONParseLoop(b *testing.B) { end total end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } args := []Value{ NewString(`{"score":7,"tags":["a","b","c"],"active":true}`), @@ -186,17 +166,13 @@ end`) } func BenchmarkExecutionJSONStringifyLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(payload, n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(payload, n) out = "" for i in 1..n out = JSON.stringify(payload) end out end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } payload := NewHash(map[string]Value{ "id": NewString("player-7"), @@ -215,17 +191,13 @@ end`) } func BenchmarkExecutionRegexReplaceAllLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(text, n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(text, n) out = "" for i in 1..n out = Regex.replace_all(text, "ID-[0-9]+", "X") end out end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } args := []Value{ NewString("ID-12 ID-34 ID-56 ID-78 ID-90"), @@ -241,17 +213,13 @@ end`) } func BenchmarkExecutionTallyLoop(b *testing.B) { - engine := benchmarkEngine() - script, err := engine.Compile(`def run(values, n) + script := compileScriptWithEngine(b, benchmarkEngine(), `def run(values, n) out = {} for i in 1..n out = values.tally end out end`) - if err != nil { - b.Fatalf("compile failed: %v", err) - } values := make([]Value, 600) for i := range values { diff --git a/vibes/fuzz_test.go b/vibes/fuzz_test.go index a1a503d..ff0e683 100644 --- a/vibes/fuzz_test.go +++ b/vibes/fuzz_test.go @@ -23,8 +23,7 @@ func FuzzCompileScriptDoesNotPanic(f *testing.F) { } func FuzzRuntimeEdgeCasesDoNotPanic(f *testing.F) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(` + script := compileScriptDefault(f, ` def run(text, pattern) stable_groups = text.split("").group_by_stable do |char| if char == "" @@ -45,9 +44,6 @@ def run(text, pattern) } end `) - if err != nil { - f.Fatalf("compile failed: %v", err) - } f.Add("", "a*") f.Add("ID-12 ID-34", "ID-[0-9]+") diff --git a/vibes/integration_classes_test.go b/vibes/integration_classes_test.go index d00f297..f8a108c 100644 --- a/vibes/integration_classes_test.go +++ b/vibes/integration_classes_test.go @@ -2,45 +2,24 @@ package vibes import ( "context" - "strings" "testing" ) func TestClassPrivacyEnforced(t *testing.T) { script := compileTestProgram(t, "classes/privacy.vibe") - _, err := script.Call(context.Background(), "violate", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected privacy violation") - } - if !strings.Contains(err.Error(), "private method secret") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "violate", nil, CallOptions{}, "private method secret") } func TestClassErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/classes.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("undefined_method", "unknown") - checkErr("private_method_external", "private method") - checkErr("write_to_readonly", "read-only property") - checkErr("wrong_init_args", "argument") + requireCallErrorContains(t, script, "undefined_method", nil, CallOptions{}, "unknown") + requireCallErrorContains(t, script, "private_method_external", nil, CallOptions{}, "private method") + requireCallErrorContains(t, script, "write_to_readonly", nil, CallOptions{}, "read-only property") + requireCallErrorContains(t, script, "wrong_init_args", nil, CallOptions{}, "argument") // run function should work - val, err := script.Call(context.Background(), "run", nil, CallOptions{}) - if err != nil { - t.Fatalf("run: unexpected error: %v", err) - } + val := callScript(t, context.Background(), script, "run", nil, CallOptions{}) if val.Kind() != KindHash { t.Fatalf("run: expected hash, got %v", val.Kind()) } diff --git a/vibes/integration_test.go b/vibes/integration_test.go index 6ed85ec..789daab 100644 --- a/vibes/integration_test.go +++ b/vibes/integration_test.go @@ -13,16 +13,7 @@ import ( func compileTestProgram(t *testing.T, rel string) *Script { t.Helper() path := filepath.Join("..", "tests", rel) - source, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read %s: %v", rel, err) - } - engine := MustNewEngine(Config{}) - script, err := engine.Compile(string(source)) - if err != nil { - t.Fatalf("compile %s: %v", rel, err) - } - return script + return compileScriptFromFileDefault(t, path) } func compileComplexExample(t *testing.T, rel string) *Script { @@ -32,16 +23,7 @@ func compileComplexExample(t *testing.T, rel string) *Script { func compileComplexExampleWithConfig(t *testing.T, rel string, cfg Config) *Script { t.Helper() path := filepath.Join("..", "tests", "complex", rel) - source, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read %s: %v", rel, err) - } - engine := MustNewEngine(cfg) - script, err := engine.Compile(string(source)) - if err != nil { - t.Fatalf("compile %s: %v", rel, err) - } - return script + return compileScriptFromFileWithConfig(t, cfg, path) } func TestComplexExamplesCompile(t *testing.T) { @@ -60,13 +42,7 @@ func TestComplexExamplesCompile(t *testing.T) { for _, path := range files { t.Run(filepath.Base(path), func(t *testing.T) { full := filepath.Join("..", path) - data, err := os.ReadFile(full) - if err != nil { - t.Fatalf("read %s: %v", full, err) - } - if _, err := engine.Compile(string(data)); err != nil { - t.Fatalf("compile %s: %v", path, err) - } + _ = compileScriptFromFileWithEngine(t, engine, full) }) } } @@ -433,20 +409,9 @@ func TestProgramFixtures(t *testing.T) { func TestBlockErrorCases(t *testing.T) { script := compileTestProgram(t, "blocks/error_cases.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error %v", fn, err) - } - } - - checkErr("each_without_block", "requires a block") - checkErr("map_without_block", "requires a block") - checkErr("reduce_empty_without_init", "requires an initial value") + requireCallErrorContains(t, script, "each_without_block", nil, CallOptions{}, "requires a block") + requireCallErrorContains(t, script, "map_without_block", nil, CallOptions{}, "requires a block") + requireCallErrorContains(t, script, "reduce_empty_without_init", nil, CallOptions{}, "requires an initial value") val, err := script.Call(context.Background(), "reduce_empty_with_init", nil, CallOptions{}) if err != nil { @@ -457,13 +422,7 @@ func TestBlockErrorCases(t *testing.T) { func TestBlockErrorPropagation(t *testing.T) { script := compileTestProgram(t, "blocks/block_error_propagation.vibe") - _, err := script.Call(context.Background(), "explode", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected error from block") - } - if !strings.Contains(err.Error(), "unknown_method") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "explode", nil, CallOptions{}, "unknown_method") } func TestComplexExamplesStress(t *testing.T) { @@ -517,14 +476,7 @@ func TestAllVibeFilesCompileAndRun(t *testing.T) { for _, path := range files { rel, _ := filepath.Rel(testsDir, path) t.Run(rel, func(t *testing.T) { - source, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read: %v", err) - } - script, err := engine.Compile(string(source)) - if err != nil { - t.Fatalf("compile: %v", err) - } + script := compileScriptFromFileWithEngine(t, engine, path) if fn, ok := script.Function("run"); ok { _, err := script.Call(context.Background(), fn.Name, nil, CallOptions{}) if err != nil { @@ -538,96 +490,38 @@ func TestAllVibeFilesCompileAndRun(t *testing.T) { func TestRuntimeErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/runtime.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("div_by_zero", "division by zero") - checkErr("mod_by_zero", "modulo by zero") - checkErr("array_index_out_of_bounds", "index out of bounds") - checkErr("string_index_out_of_bounds", "index out of bounds") - checkErr("method_missing", "unknown") - checkErr("nil_method", "nil") + requireCallErrorContains(t, script, "div_by_zero", nil, CallOptions{}, "division by zero") + requireCallErrorContains(t, script, "mod_by_zero", nil, CallOptions{}, "modulo by zero") + requireCallErrorContains(t, script, "array_index_out_of_bounds", nil, CallOptions{}, "index out of bounds") + requireCallErrorContains(t, script, "string_index_out_of_bounds", nil, CallOptions{}, "index out of bounds") + requireCallErrorContains(t, script, "method_missing", nil, CallOptions{}, "unknown") + requireCallErrorContains(t, script, "nil_method", nil, CallOptions{}, "nil") } func TestTypeErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/types.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("sub_mismatch", "unsupported") - checkErr("mul_mismatch", "unsupported") - checkErr("div_mismatch", "unsupported") - checkErr("unary_mismatch", "unsupported") - - // Test arg_type_mismatch by passing a string instead of int - _, err := script.Call(context.Background(), "arg_type_mismatch", []Value{strVal("wrong")}, CallOptions{}) - if err == nil { - t.Fatalf("arg_type_mismatch: expected error") - } - if !strings.Contains(err.Error(), "argument n expected int, got string") { - t.Fatalf("arg_type_mismatch: unexpected error '%v', want argument+expected+actual", err) - } - - _, err = script.Call(context.Background(), "return_type_mismatch", nil, CallOptions{}) - if err == nil { - t.Fatalf("return_type_mismatch: expected error") - } - if !strings.Contains(err.Error(), "return value for return_type_mismatch expected int, got string") { - t.Fatalf("return_type_mismatch: unexpected error '%v', want expected+actual", err) - } + requireCallErrorContains(t, script, "sub_mismatch", nil, CallOptions{}, "unsupported") + requireCallErrorContains(t, script, "mul_mismatch", nil, CallOptions{}, "unsupported") + requireCallErrorContains(t, script, "div_mismatch", nil, CallOptions{}, "unsupported") + requireCallErrorContains(t, script, "unary_mismatch", nil, CallOptions{}, "unsupported") + requireCallErrorContains(t, script, "arg_type_mismatch", []Value{strVal("wrong")}, CallOptions{}, "argument n expected int, got string") + requireCallErrorContains(t, script, "return_type_mismatch", nil, CallOptions{}, "return value for return_type_mismatch expected int, got string") } func TestAttributeErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/attributes.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("set_readonly", "read-only property") + requireCallErrorContains(t, script, "set_readonly", nil, CallOptions{}, "read-only property") } func TestYieldErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/yield.vibe") - // yield without a block should error - _, err := script.Call(context.Background(), "yield_without_block", nil, CallOptions{}) - if err == nil { - t.Fatalf("yield_without_block: expected error") - } - if !strings.Contains(err.Error(), "no block given") { - t.Fatalf("yield_without_block: unexpected error '%v', want 'no block given'", err) - } + requireCallErrorContains(t, script, "yield_without_block", nil, CallOptions{}, "no block given") // run function should work since it uses blocks correctly - val, err := script.Call(context.Background(), "run", nil, CallOptions{}) - if err != nil { - t.Fatalf("run: unexpected error: %v", err) - } + val := callScript(t, context.Background(), script, "run", nil, CallOptions{}) assertValueEqual(t, val, hashVal(map[string]Value{ "count": intVal(3), })) @@ -636,25 +530,11 @@ func TestYieldErrorCases(t *testing.T) { func TestArgumentErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/arguments.vibe") - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("too_few_args", "argument") - checkErr("too_many_args", "argument") + requireCallErrorContains(t, script, "too_few_args", nil, CallOptions{}, "argument") + requireCallErrorContains(t, script, "too_many_args", nil, CallOptions{}, "argument") // run function should work - val, err := script.Call(context.Background(), "run", nil, CallOptions{}) - if err != nil { - t.Fatalf("run: unexpected error: %v", err) - } + val := callScript(t, context.Background(), script, "run", nil, CallOptions{}) if val.Kind() != KindHash { t.Fatalf("run: expected hash, got %v", val.Kind()) } @@ -679,11 +559,7 @@ def run results end ` - engine := MustNewEngine(Config{}) - script, err := engine.Compile(source) - if err != nil { - t.Fatalf("compile: %v", err) - } + script := compileScriptDefault(t, source) for i := range 100 { result, err := script.Call(context.Background(), "run", nil, CallOptions{}) @@ -710,11 +586,7 @@ def run { a: a, b: b } end ` - engine := MustNewEngine(Config{}) - script, err := engine.Compile(source) - if err != nil { - t.Fatalf("compile: %v", err) - } + script := compileScriptDefault(t, source) for i := range 50 { result, err := script.Call(context.Background(), "run", nil, CallOptions{}) diff --git a/vibes/interpreter_test.go b/vibes/interpreter_test.go index 21f5c10..d4fdc41 100644 --- a/vibes/interpreter_test.go +++ b/vibes/interpreter_test.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "testing" ) @@ -14,9 +13,7 @@ func TestNewEngineRejectsMissingModulePath(t *testing.T) { if err == nil { t.Fatalf("expected NewEngine to reject missing module path") } - if !strings.Contains(err.Error(), "invalid module path") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "invalid module path") } func TestNewEngineRejectsFileModulePath(t *testing.T) { @@ -30,9 +27,7 @@ func TestNewEngineRejectsFileModulePath(t *testing.T) { if err == nil { t.Fatalf("expected NewEngine to reject file module path") } - if !strings.Contains(err.Error(), "is not a directory") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "is not a directory") } func TestNewEngineAcceptsValidModulePaths(t *testing.T) { @@ -61,9 +56,7 @@ func TestNewEngineValidatesConfiguredModulePathAsProvided(t *testing.T) { if err == nil { t.Fatalf("expected NewEngine to reject module path that os.Stat rejects") } - if !strings.Contains(err.Error(), "invalid module path") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "invalid module path") return } @@ -76,13 +69,9 @@ func TestNewEngineValidatesConfiguredModulePathAsProvided(t *testing.T) { } func TestCompileAndCallAdd(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def add(a, b) + script := compileScriptDefault(t, `def add(a, b) a + b end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "add", []Value{NewInt(2), NewInt(3)}, CallOptions{}) if err != nil { @@ -94,13 +83,9 @@ end`) } func TestMoneyBuiltin(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def total + script := compileScriptDefault(t, `def total money("10.00 USD") + money("5.00 USD") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "total", nil, CallOptions{}) if err != nil { @@ -116,13 +101,9 @@ end`) } func TestGlobalsAccess(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def user_id + script := compileScriptDefault(t, `def user_id ctx.user.id end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } ctxVal := NewObject(map[string]Value{ "user": NewObject(map[string]Value{ @@ -140,13 +121,9 @@ end`) } func TestAssertFailure(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def check + script := compileScriptDefault(t, `def check assert false, "boom" end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } if _, err := script.Call(context.Background(), "check", nil, CallOptions{}); err == nil { t.Fatalf("expected assertion error") @@ -154,13 +131,9 @@ end`) } func TestSymbolIndex(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def amount(row) + script := compileScriptDefault(t, `def amount(row) row[:amount] end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } row := NewHash(map[string]Value{"amount": NewInt(42)}) result, err := script.Call(context.Background(), "amount", []Value{row}, CallOptions{}) @@ -173,13 +146,9 @@ end`) } func TestDurationLiteral(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def seconds + script := compileScriptDefault(t, `def seconds (2.minutes).seconds end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "seconds", nil, CallOptions{}) if err != nil { @@ -191,17 +160,13 @@ end`) } func TestZeroArgCallWithoutParens(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def helper + script := compileScriptDefault(t, `def helper 7 end def run helper * 6 end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -213,8 +178,7 @@ end`) } func TestNestedZeroArgCalls(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def inner + script := compileScriptDefault(t, `def inner 10 end @@ -225,9 +189,6 @@ end def outer middle * 2 end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "outer", nil, CallOptions{}) if err != nil { @@ -239,8 +200,7 @@ end`) } func TestMixedZeroArgAndRegularCalls(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def zero_arg + script := compileScriptDefault(t, `def zero_arg 5 end @@ -251,9 +211,6 @@ end def run zero_arg + with_args(10, 20) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -265,14 +222,10 @@ end`) } func TestMethodChainingWithZeroArgMethods(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run + script := compileScriptDefault(t, `def run values = [1, 2, 3, 4, 5] values.sum end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -284,14 +237,10 @@ end`) } func TestZeroArgMethodChaining(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(`def run + script := compileScriptDefault(t, `def run values = [1, 2, 2, 3, 3, 3] values.uniq.sum end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { diff --git a/vibes/memory_quota_test.go b/vibes/memory_quota_test.go index 225bead..9e0373e 100644 --- a/vibes/memory_quota_test.go +++ b/vibes/memory_quota_test.go @@ -40,57 +40,34 @@ def run end ` +func requireRunMemoryQuotaError(t *testing.T, script *Script, args []Value, opts CallOptions) { + t.Helper() + requireCallErrorContains(t, script, "run", args, opts, "memory quota exceeded") +} + func TestMemoryQuotaExceeded(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 2048, - }) - - script, err := engine.Compile(quotaFixture) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + }, quotaFixture) + requireRunMemoryQuotaError(t, script, nil, CallOptions{}) } func TestMemoryQuotaCountsClassVars(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 3072, - }) - - script, err := engine.Compile(classVarFixture) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + }, classVarFixture) + requireRunMemoryQuotaError(t, script, nil, CallOptions{}) } func TestMemoryQuotaAllowsExecution(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 1 << 20, - }) - - script, err := engine.Compile(quotaFixture) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + }, quotaFixture) + var err error result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -101,61 +78,35 @@ func TestMemoryQuotaAllowsExecution(t *testing.T) { } func TestMemoryQuotaExceededOnCompletion(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 2048, - }) - - script, err := engine.Compile(splitFixture) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + }, splitFixture) input := strings.Repeat("a,", 4000) - _, err = script.Call(context.Background(), "run", []Value{NewString(input)}, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireRunMemoryQuotaError(t, script, []Value{NewString(input)}, CallOptions{}) } func TestMemoryQuotaExceededForEmptyBodyDefaultArg(t *testing.T) { - engine := MustNewEngine(Config{ + cfg := Config{ StepQuota: 20000, MemoryQuotaBytes: 2048, - }) + } largeCSV := strings.Repeat("abcdefghij,", 1500) source := `def run(payload = "` + largeCSV + `".split(",")) end` - script, err := engine.Compile(source) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + script := compileScriptWithConfig(t, cfg, source) + requireRunMemoryQuotaError(t, script, nil, CallOptions{}) } func TestMemoryQuotaExceededForBoundArguments(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 2048, - }) - - script, err := engine.Compile(`def run(payload) + }, `def run(payload) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } parts := make([]Value, 2000) for i := range parts { @@ -163,60 +114,33 @@ end`) } largeArg := NewArray(parts) - _, err = script.Call(context.Background(), "run", []Value{largeArg}, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error for positional arg") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected positional arg error: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{ + requireRunMemoryQuotaError(t, script, []Value{largeArg}, CallOptions{}) + requireRunMemoryQuotaError(t, script, nil, CallOptions{ Keywords: map[string]Value{ "payload": largeArg, }, }) - if err == nil { - t.Fatalf("expected memory quota error for keyword arg") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected keyword arg error: %v", err) - } } func TestMemoryQuotaCountsIndependentEmptySlices(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 4096, - }) - - script, err := engine.Compile(`def run + }, `def run items = [] for i in 1..400 items = items.push([]) end items.size end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error for many independent empty slices") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireRunMemoryQuotaError(t, script, nil, CallOptions{}) } func TestMemoryQuotaExceededWithWhileLoopAllocations(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ StepQuota: 20000, MemoryQuotaBytes: 2048, - }) - - script, err := engine.Compile(`def run() + }, `def run() items = [] n = 0 while n < 200 @@ -225,17 +149,7 @@ func TestMemoryQuotaExceededWithWhileLoopAllocations(t *testing.T) { end items.size end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected memory quota error for while-loop allocations") - } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireRunMemoryQuotaError(t, script, nil, CallOptions{}) } func TestAssignmentPostCheckDoesNotDoubleCountAssignedValue(t *testing.T) { @@ -393,9 +307,7 @@ func TestTransientExpressionAllocationsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for transient expression allocation") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestIndexedTransientAllocationsAreChecked(t *testing.T) { @@ -449,9 +361,7 @@ func TestIndexedTransientAllocationsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for indexed transient allocation") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestTransientMethodCallReceiverAllocationsAreChecked(t *testing.T) { @@ -508,9 +418,7 @@ func TestTransientMethodCallReceiverAllocationsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for transient method-call receiver allocation") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestIfConditionTransientAllocationsAreChecked(t *testing.T) { @@ -566,9 +474,7 @@ func TestIfConditionTransientAllocationsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for if-condition transient allocation") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestAggregateBuiltinArgumentsAreChecked(t *testing.T) { @@ -627,9 +533,7 @@ func TestAggregateBuiltinArgumentsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for aggregate builtin arguments") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestTransientAssignmentValueIsCheckedBeforeAssign(t *testing.T) { @@ -698,9 +602,7 @@ func TestTransientAssignmentValueIsCheckedBeforeAssign(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for transient assignment value") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestTransientUnaryOperandAllocationsAreChecked(t *testing.T) { @@ -754,9 +656,7 @@ func TestTransientUnaryOperandAllocationsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for unary transient operand") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestTransientBinaryOperandsAreChecked(t *testing.T) { @@ -811,9 +711,7 @@ func TestTransientBinaryOperandsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for binary transient operands") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestAssignmentTargetExpressionsAreChecked(t *testing.T) { @@ -875,9 +773,7 @@ func TestAssignmentTargetExpressionsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for assignment target transient allocation") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } func TestAggregateYieldArgumentsAreChecked(t *testing.T) { @@ -937,7 +833,5 @@ func TestAggregateYieldArgumentsAreChecked(t *testing.T) { if err == nil { t.Fatalf("expected memory quota error for aggregate yield arguments") } - if !strings.Contains(err.Error(), "memory quota exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "memory quota exceeded") } diff --git a/vibes/modules_test.go b/vibes/modules_test.go index 12320e7..fd94364 100644 --- a/vibes/modules_test.go +++ b/vibes/modules_test.go @@ -10,16 +10,20 @@ import ( "testing" ) +const moduleFixturesRoot = "testdata/modules" + +func moduleTestEngine(t testing.TB) *Engine { + t.Helper() + return MustNewEngine(Config{ModulePaths: []string{filepath.FromSlash(moduleFixturesRoot)}}) +} + func TestRequireProvidesExports(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) helpers = require("helper") helpers.triple(value) + double(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -31,16 +35,13 @@ end`) } func TestRequireSupportsModuleAlias(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) require("helper", as: "helpers") require("helper", as: "helpers") helpers.triple(value) + helpers.double(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -52,7 +53,7 @@ end`) } func TestRequireAliasValidation(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) cases := []struct { name string @@ -91,44 +92,31 @@ end`, for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - script, err := engine.Compile(tc.source) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected alias validation error") - } else if !strings.Contains(err.Error(), tc.wantErr) { - t.Fatalf("unexpected error: %v", err) - } + script := compileScriptWithEngine(t, engine, tc.source) + err := callScriptErr(t, context.Background(), script, "run", nil, CallOptions{}) + requireErrorContains(t, err, tc.wantErr) }) } } func TestRequireAliasRejectsConflictingGlobal(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def helpers(value) + script := compileScriptWithEngine(t, engine, `def helpers(value) value end def run() require("helper", as: "helpers") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected alias conflict error") - } else if !strings.Contains(err.Error(), `require: alias "helpers" already defined`) { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, `require: alias "helpers" already defined`) } func TestRequireAliasConflictDoesNotLeakExportsWhenRescued(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def helpers(value) + script := compileScriptWithEngine(t, engine, `def helpers(value) value end @@ -145,9 +133,6 @@ def run(value) "missing" end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -159,9 +144,9 @@ end`) } func TestRequirePreservesModuleLocalResolution(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def rate() + script := compileScriptWithEngine(t, engine, `def rate() 100 end @@ -169,9 +154,6 @@ def run(amount) fees = require("collision") fees.apply_fee(amount) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(10)}, CallOptions{}) if err != nil { @@ -183,9 +165,9 @@ end`) } func TestRequireNamespaceConflictKeepsExistingGlobalBinding(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def double(value) + script := compileScriptWithEngine(t, engine, `def double(value) value + 1 end @@ -196,9 +178,6 @@ def run(value) module: mod.double(value) } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -217,9 +196,9 @@ end`) } func TestRequireNamespaceConflictKeepsFirstModuleBinding(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) first = require("helper") second = require("helper_alt") require("helper_alt", as: "alt") @@ -230,9 +209,6 @@ func TestRequireNamespaceConflictKeepsFirstModuleBinding(t *testing.T) { alias: alt.double(value) } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -257,34 +233,24 @@ end`) } func TestRequireMissingModule(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("missing") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected missing module error") - } else if !strings.Contains(err.Error(), `module "missing" not found`) { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, `module "missing" not found`) } func TestRequireCachesModules(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("helper") require("helper") require("helper") double(10) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -315,13 +281,10 @@ end writeModule(1) engine := MustNewEngine(Config{ModulePaths: []string{moduleRoot}}) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("dynamic") mod.value() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } first, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -355,7 +318,7 @@ end`) } func TestRequireRejectsAbsolutePaths(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) absPath := "/etc/passwd" if filepath.Separator == '\\' { @@ -371,63 +334,39 @@ func TestRequireRejectsAbsolutePaths(t *testing.T) { require(%q) end`, absPath) - script, err := engine.Compile(source) - if err != nil { - t.Fatalf("compile failed: %v", err) - } + script := compileScriptWithEngine(t, engine, source) - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected error for absolute path") - } else if !strings.Contains(err.Error(), "must be relative") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "must be relative") } func TestRequireRejectsPathTraversal(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("nested/../../etc/passwd") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected error for path traversal") - } else if !strings.Contains(err.Error(), "escapes search paths") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes search paths") } func TestRequireRejectsBackslashPathTraversal(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("nested\\..\\..\\etc\\passwd") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected error for backslash path traversal") - } else if !strings.Contains(err.Error(), "escapes search paths") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes search paths") } func TestRequireNormalizesPathSeparators(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) unix_style = require("shared/math") windows_style = require("shared\\math") unix_style.double(value) + windows_style.double(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -442,26 +381,19 @@ end`) } func TestRequireRelativePathRequiresModuleCaller(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("./helper") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected relative caller error") - } else if !strings.Contains(err.Error(), "requires a module caller") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "requires a module caller") } func TestRequireRelativePathDoesNotLeakFromModuleIntoHostFunction(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def host_relative() + script := compileScriptWithEngine(t, engine, `def host_relative() require("./helper") end @@ -469,27 +401,17 @@ def run() mod = require("module_calls_host") mod.invoke_host_relative() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected relative caller error from host function") - } else if !strings.Contains(err.Error(), "requires a module caller") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "requires a module caller") } func TestRequireSupportsRelativePathsWithinModuleRoot(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) mod = require("relative/root") mod.run(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(5)}, CallOptions{}) if err != nil { @@ -501,21 +423,14 @@ end`) } func TestRequireRelativePathRejectsEscapingModuleRoot(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("relative/escape") mod.run() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected module root escape error") - } else if !strings.Contains(err.Error(), "escapes module root") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes module root") } func TestRequireRelativePathRejectsSymlinkEscape(t *testing.T) { @@ -548,19 +463,12 @@ end } engine := MustNewEngine(Config{ModulePaths: []string{moduleRoot}}) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("entry") mod.run() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected symlink escape error") - } else if !strings.Contains(err.Error(), "escapes module root") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes module root") } func TestRequireSearchPathRejectsSymlinkEscape(t *testing.T) { @@ -585,18 +493,11 @@ end } engine := MustNewEngine(Config{ModulePaths: []string{moduleRoot}}) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("link/secret") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected symlink escape error") - } else if !strings.Contains(err.Error(), "escapes module root") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes module root") } func TestRequireRelativePathRejectsOutOfRootCachedModule(t *testing.T) { @@ -628,20 +529,13 @@ end } engine := MustNewEngine(Config{ModulePaths: []string{moduleRoot}}) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("link/secret") entry = require("entry") entry.run() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected module root escape error") - } else if !strings.Contains(err.Error(), "escapes module root") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "escapes module root") } func TestRequireRelativePathUsesCacheBeforeFilesystemResolution(t *testing.T) { @@ -663,13 +557,10 @@ end } engine := MustNewEngine(Config{ModulePaths: []string{moduleRoot}}) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("entry") mod.run() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -693,9 +584,9 @@ end`) } func TestRequireRelativePathWorksInModuleDefinedBlockYieldedFromHost(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def host_each() + script := compileScriptWithEngine(t, engine, `def host_each() yield() end @@ -703,9 +594,6 @@ def run() mod = require("block_host_yield") mod.run() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -717,9 +605,9 @@ end`) } func TestRequireExportsOnlyNonPrivateFunctions(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) mod = require("private_exports") if mod["_internal"] != nil 0 @@ -727,9 +615,6 @@ func TestRequireExportsOnlyNonPrivateFunctions(t *testing.T) { visible(value) + mod.call_internal(value) end end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(2)}, CallOptions{}) if err != nil { @@ -741,9 +626,9 @@ end`) } func TestRequireSupportsPrivateModuleExportOptOut(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) mod = require("explicit_exports") { has_exposed: mod["exposed"] != nil, @@ -754,9 +639,6 @@ func TestRequireSupportsPrivateModuleExportOptOut(t *testing.T) { explicit_hidden: mod._explicit_hidden(value) } end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -781,108 +663,69 @@ end`) } func TestRequirePrivateFunctionsAreNotInjectedAsGlobals(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) require("explicit_exports") helper(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", []Value{NewInt(2)}, CallOptions{}); err == nil { - t.Fatalf("expected undefined helper error") - } else if !strings.Contains(err.Error(), "undefined variable helper") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", []Value{NewInt(2)}, CallOptions{}, "undefined variable helper") } func TestExportKeywordValidation(t *testing.T) { - engine := MustNewEngine(Config{}) + requireCompileErrorContainsDefault(t, `export helper`, "expected 'def'") - _, err := engine.Compile(`export helper`) - if err == nil || !strings.Contains(err.Error(), "expected 'def'") { - t.Fatalf("expected export def parse error, got %v", err) - } - - _, err = engine.Compile(`class Example + requireCompileErrorContainsDefault(t, `class Example export def value() 1 end -end`) - if err == nil || !strings.Contains(err.Error(), "export is only supported for top-level functions") { - t.Fatalf("expected top-level export parse error, got %v", err) - } +end`, "export is only supported for top-level functions") - _, err = engine.Compile(`def outer() + requireCompileErrorContainsDefault(t, `def outer() if true export def nested() 1 end end -end`) - if err == nil || !strings.Contains(err.Error(), "export is only supported for top-level functions") { - t.Fatalf("expected nested export parse error, got %v", err) - } +end`, "export is only supported for top-level functions") } func TestPrivateKeywordValidation(t *testing.T) { - engine := MustNewEngine(Config{}) - - _, err := engine.Compile(`private helper`) - if err == nil || !strings.Contains(err.Error(), "expected 'def'") { - t.Fatalf("expected private def parse error, got %v", err) - } + requireCompileErrorContainsDefault(t, `private helper`, "expected 'def'") - _, err = engine.Compile(`def outer() + requireCompileErrorContainsDefault(t, `def outer() if true private def nested() 1 end end -end`) - if err == nil || !strings.Contains(err.Error(), "private is only supported for top-level functions and class methods") { - t.Fatalf("expected nested private parse error, got %v", err) - } +end`, "private is only supported for top-level functions and class methods") - _, err = engine.Compile(`private def self.value() + requireCompileErrorContainsDefault(t, `private def self.value() 1 -end`) - if err == nil || !strings.Contains(err.Error(), "private cannot be used with class methods") { - t.Fatalf("expected private class-method parse error, got %v", err) - } +end`, "private cannot be used with class methods") } func TestRequirePrivateFunctionsRemainModuleScoped(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) require("private_exports") _internal(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", []Value{NewInt(2)}, CallOptions{}); err == nil { - t.Fatalf("expected undefined private function error") - } else if !strings.Contains(err.Error(), "undefined variable _internal") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", []Value{NewInt(2)}, CallOptions{}, "undefined variable _internal") } func TestRequireModuleCacheAvoidsDuplicateLoads(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("circular_a") require("circular_b") "ok" end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -894,33 +737,23 @@ end`) } func TestRequireRuntimeModuleRecursionHitsRecursionLimit(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("circular_runtime_a") mod.enter() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected recursion limit error") - } else if !strings.Contains(err.Error(), "recursion depth exceeded") { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "recursion depth exceeded") } func TestRequireAllowsCachedModuleReuseAcrossModuleCalls(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() mod = require("require_cached_a") mod.start() end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { @@ -932,15 +765,12 @@ end`) } func TestRequireConcurrentLoading(t *testing.T) { - engine := MustNewEngine(Config{ModulePaths: []string{filepath.Join("testdata", "modules")}}) + engine := moduleTestEngine(t) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("helper") double(5) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } const goroutines = 10 results := make(chan error, goroutines) @@ -977,14 +807,11 @@ func TestRequireStrictEffectsRequiresAllowRequire(t *testing.T) { ModulePaths: []string{filepath.Join("testdata", "modules")}, }) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("helper") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) + _, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err == nil { t.Fatalf("expected strict effects require error") } @@ -999,13 +826,10 @@ func TestRequireStrictEffectsAllowsRequireWhenOptedIn(t *testing.T) { ModulePaths: []string{filepath.Join("testdata", "modules")}, }) - script, err := engine.Compile(`def run(v) + script := compileScriptWithEngine(t, engine, `def run(v) helpers = require("helper") helpers.triple(v) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(4)}, CallOptions{ AllowRequire: true, @@ -1024,7 +848,7 @@ func TestRequireModuleAllowList(t *testing.T) { ModuleAllowList: []string{"shared/*"}, }) - script, err := engine.Compile(`def run_allowed(value) + script := compileScriptWithEngine(t, engine, `def run_allowed(value) mod = require("shared/math") mod.double(value) end @@ -1033,9 +857,6 @@ def run_denied(value) mod = require("helper") mod.double(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } allowed, err := script.Call(context.Background(), "run_allowed", []Value{NewInt(3)}, CallOptions{}) if err != nil { @@ -1045,11 +866,7 @@ end`) t.Fatalf("expected allowed result 6, got %#v", allowed) } - if _, err := script.Call(context.Background(), "run_denied", []Value{NewInt(3)}, CallOptions{}); err == nil { - t.Fatalf("expected denied module error") - } else if !strings.Contains(err.Error(), `require: module "helper" not allowed by policy`) { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run_denied", []Value{NewInt(3)}, CallOptions{}, `require: module "helper" not allowed by policy`) } func TestRequireModuleAllowListStarMatchesNestedModules(t *testing.T) { @@ -1058,13 +875,10 @@ func TestRequireModuleAllowListStarMatchesNestedModules(t *testing.T) { ModuleAllowList: []string{"*"}, }) - script, err := engine.Compile(`def run(value) + script := compileScriptWithEngine(t, engine, `def run(value) mod = require("shared/math") mod.double(value) end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } result, err := script.Call(context.Background(), "run", []Value{NewInt(4)}, CallOptions{}) if err != nil { @@ -1082,18 +896,11 @@ func TestRequireModuleDenyListOverridesAllowList(t *testing.T) { ModuleDenyList: []string{"helper"}, }) - script, err := engine.Compile(`def run() + script := compileScriptWithEngine(t, engine, `def run() require("helper") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - if _, err := script.Call(context.Background(), "run", nil, CallOptions{}); err == nil { - t.Fatalf("expected deny-list error") - } else if !strings.Contains(err.Error(), `require: module "helper" denied by policy`) { - t.Fatalf("unexpected error: %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, `require: module "helper" denied by policy`) } func TestModulePolicyPatternValidation(t *testing.T) { @@ -1101,9 +908,7 @@ func TestModulePolicyPatternValidation(t *testing.T) { ModulePaths: []string{filepath.Join("testdata", "modules")}, ModuleAllowList: []string{"[invalid"}, }) - if err == nil || !strings.Contains(err.Error(), "invalid module allow-list pattern") { - t.Fatalf("expected invalid allow-list pattern error, got %v", err) - } + requireErrorContains(t, err, "invalid module allow-list pattern") } func TestFormatModuleCycleUsesConciseChain(t *testing.T) { diff --git a/vibes/recursion_test.go b/vibes/recursion_test.go index 9a20a3c..5472945 100644 --- a/vibes/recursion_test.go +++ b/vibes/recursion_test.go @@ -3,92 +3,57 @@ package vibes import ( "context" "errors" - "strings" "testing" ) func TestRecursionLimitExceeded(t *testing.T) { - engine := MustNewEngine(Config{ - RecursionLimit: 3, - }) - - script, err := engine.Compile(`def recurse(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 3}, `def recurse(n) if n <= 0 "done" else recurse(n - 1) end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - _, err = script.Call(context.Background(), "recurse", []Value{NewInt(5)}, CallOptions{}) - if err == nil { - t.Fatalf("expected recursion depth error") - } + err := callScriptErr(t, context.Background(), script, "recurse", []Value{NewInt(5)}, CallOptions{}) var re *RuntimeError if !errors.As(err, &re) { t.Fatalf("expected RuntimeError, got %T", err) } - if !strings.Contains(err.Error(), "recursion depth exceeded (limit 3)") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "recursion depth exceeded (limit 3)") } func TestRecursionLimitAllowsWithinBound(t *testing.T) { - engine := MustNewEngine(Config{ - RecursionLimit: 5, - }) - - script, err := engine.Compile(`def recurse(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 5}, `def recurse(n) if n <= 0 0 else recurse(n - 1) + 1 end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - result, err := script.Call(context.Background(), "recurse", []Value{NewInt(4)}, CallOptions{}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + result := callScript(t, context.Background(), script, "recurse", []Value{NewInt(4)}, CallOptions{}) if result.Kind() != KindInt || result.Int() != 4 { t.Fatalf("unexpected result: %v", result) } } func TestRecursionLimitDefaultApplies(t *testing.T) { - engine := MustNewEngine(Config{}) - - script, err := engine.Compile(`def recurse(n) + script := compileScriptDefault(t, `def recurse(n) if n <= 0 "done" else recurse(n - 1) end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - _, err = script.Call(context.Background(), "recurse", []Value{NewInt(100)}, CallOptions{}) - if err == nil { - t.Fatalf("expected recursion depth error at default limit") - } - if !strings.Contains(err.Error(), "recursion depth exceeded") { - t.Fatalf("unexpected error: %v", err) - } + err := callScriptErr(t, context.Background(), script, "recurse", []Value{NewInt(100)}, CallOptions{}) + requireErrorContains(t, err, "recursion depth exceeded") } func TestMutualRecursionRespectsLimit(t *testing.T) { - engine := MustNewEngine(Config{RecursionLimit: 4}) - - script, err := engine.Compile(`def a(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 4}, `def a(n) if n <= 0 "done" else @@ -103,93 +68,51 @@ def b(n) a(n - 1) end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - _, err = script.Call(context.Background(), "a", []Value{NewInt(10)}, CallOptions{}) - if err == nil { - t.Fatalf("expected recursion depth error") - } - if !strings.Contains(err.Error(), "recursion depth exceeded") { - t.Fatalf("unexpected error: %v", err) - } + err := callScriptErr(t, context.Background(), script, "a", []Value{NewInt(10)}, CallOptions{}) + requireErrorContains(t, err, "recursion depth exceeded") } func TestRecursionLimitWinsOverStepQuota(t *testing.T) { - engine := MustNewEngine(Config{ - RecursionLimit: 3, - StepQuota: 1_000_000, - }) - - script, err := engine.Compile(`def spin(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 3, StepQuota: 1_000_000}, `def spin(n) if n <= 0 0 else 1 + spin(n - 1) end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - _, err = script.Call(context.Background(), "spin", []Value{NewInt(50)}, CallOptions{}) - if err == nil { - t.Fatalf("expected recursion depth error") - } - if !strings.Contains(err.Error(), "recursion depth exceeded (limit 3)") { - t.Fatalf("unexpected error: %v", err) - } + err := callScriptErr(t, context.Background(), script, "spin", []Value{NewInt(50)}, CallOptions{}) + requireErrorContains(t, err, "recursion depth exceeded (limit 3)") } func TestRecursionLimitNoLeakAfterError(t *testing.T) { - engine := MustNewEngine(Config{RecursionLimit: 4}) - - script, err := engine.Compile(`def ping(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 4}, `def ping(n) if n <= 0 "ok" else ping(n - 1) end end`) - if err != nil { - t.Fatalf("compile: %v", err) - } // First call exceeds the limit. _, _ = script.Call(context.Background(), "ping", []Value{NewInt(10)}, CallOptions{}) // Second call within the limit should still succeed. - result, err := script.Call(context.Background(), "ping", []Value{NewInt(3)}, CallOptions{}) - if err != nil { - t.Fatalf("unexpected error on second call: %v", err) - } + result := callScript(t, context.Background(), script, "ping", []Value{NewInt(3)}, CallOptions{}) if result.Kind() != KindString || result.String() != "ok" { t.Fatalf("unexpected result: %v", result) } } func TestRecursionLimitWithWhileLoopFrames(t *testing.T) { - engine := MustNewEngine(Config{ - RecursionLimit: 4, - StepQuota: 1_000_000, - }) - - script, err := engine.Compile(`def recurse(n) + script := compileScriptWithConfig(t, Config{RecursionLimit: 4, StepQuota: 1_000_000}, `def recurse(n) while n > 0 n = n - 1 end recurse(1) end`) - if err != nil { - t.Fatalf("compile: %v", err) - } - _, err = script.Call(context.Background(), "recurse", []Value{NewInt(3)}, CallOptions{}) - if err == nil { - t.Fatalf("expected recursion depth error") - } - if !strings.Contains(err.Error(), "recursion depth exceeded (limit 4)") { - t.Fatalf("unexpected error: %v", err) - } + err := callScriptErr(t, context.Background(), script, "recurse", []Value{NewInt(3)}, CallOptions{}) + requireErrorContains(t, err, "recursion depth exceeded (limit 4)") } diff --git a/vibes/runtime_test.go b/vibes/runtime_test.go index df7c7ba..fa0c19e 100644 --- a/vibes/runtime_test.go +++ b/vibes/runtime_test.go @@ -11,12 +11,7 @@ import ( func compileScript(t *testing.T, source string) *Script { t.Helper() - engine := MustNewEngine(Config{}) - script, err := engine.Compile(source) - if err != nil { - t.Fatalf("compile error: %v", err) - } - return script + return compileScriptDefault(t, source) } func callFunc(t *testing.T, script *Script, name string, args []Value) Value { @@ -29,17 +24,13 @@ func callFunc(t *testing.T, script *Script, name string, args []Value) Value { } func TestCompileMalformedCallTargetDoesNotPanic(t *testing.T) { - engine := MustNewEngine(Config{}) defer func() { if r := recover(); r != nil { t.Fatalf("compile panicked: %v", r) } }() - _, err := engine.Compile(`be(in (000000000`) - if err == nil { - t.Fatalf("expected compile error for malformed input") - } + _ = compileScriptErrorDefault(t, `be(in (000000000`) } func TestHashMergeAndKeys(t *testing.T) { @@ -603,10 +594,7 @@ func TestArrayChunkWindowValidation(t *testing.T) { end `) - _, err := script.Call(context.Background(), "bad_chunk", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "array.chunk size must be a positive integer") { - t.Fatalf("expected chunk validation error, got %v", err) - } + requireCallErrorContains(t, script, "bad_chunk", nil, CallOptions{}, "array.chunk size must be a positive integer") nativeMaxInt := int64(^uint(0) >> 1) hugeChunk := callFunc(t, script, "huge_chunk", []Value{NewInt(nativeMaxInt)}) if hugeChunk.Kind() != KindArray { @@ -617,10 +605,7 @@ func TestArrayChunkWindowValidation(t *testing.T) { t.Fatalf("expected one chunk for oversized chunk size, got %d", len(chunks)) } compareArrays(t, chunks[0], []Value{NewInt(1), NewInt(2)}) - _, err = script.Call(context.Background(), "bad_window", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "array.window size must be a positive integer") { - t.Fatalf("expected window validation error, got %v", err) - } + requireCallErrorContains(t, script, "bad_window", nil, CallOptions{}, "array.window size must be a positive integer") hugeWindow := callFunc(t, script, "huge_window", []Value{NewInt(nativeMaxInt)}) if hugeWindow.Kind() != KindArray || len(hugeWindow.Array()) != 0 { t.Fatalf("expected huge window size to return empty array, got %v", hugeWindow) @@ -628,19 +613,10 @@ func TestArrayChunkWindowValidation(t *testing.T) { overflowSize := int64(1 << 62) if nativeMaxInt < overflowSize { - _, err = script.Call(context.Background(), "huge_chunk", []Value{NewInt(overflowSize)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "array.chunk size must be a positive integer") { - t.Fatalf("expected chunk overflow validation error, got %v", err) - } - _, err = script.Call(context.Background(), "huge_window", []Value{NewInt(overflowSize)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "array.window size must be a positive integer") { - t.Fatalf("expected window overflow validation error, got %v", err) - } - } - _, err = script.Call(context.Background(), "bad_group_by_stable", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "array.group_by_stable requires a block") { - t.Fatalf("expected group_by_stable block error, got %v", err) + requireCallErrorContains(t, script, "huge_chunk", []Value{NewInt(overflowSize)}, CallOptions{}, "array.chunk size must be a positive integer") + requireCallErrorContains(t, script, "huge_window", []Value{NewInt(overflowSize)}, CallOptions{}, "array.window size must be a positive integer") } + requireCallErrorContains(t, script, "bad_group_by_stable", nil, CallOptions{}, "array.group_by_stable requires a block") } func TestArrayConcatAndSubtract(t *testing.T) { @@ -665,23 +641,15 @@ func TestArrayConcatAndSubtract(t *testing.T) { } func TestHashLiteralSyntaxRestriction(t *testing.T) { - engine := MustNewEngine(Config{}) - _, err := engine.Compile(` + _ = compileScriptErrorDefault(t, ` def broken() { "name" => "alex" } end `) - if err == nil { - t.Fatalf("expected compile error for legacy hash syntax") - } } func TestParseErrorIncludesCodeFrameAndKeywordMessage(t *testing.T) { - engine := MustNewEngine(Config{}) - _, err := engine.Compile("def broken()\n call(foo: )\nend\n") - if err == nil { - t.Fatalf("expected compile error") - } + err := compileScriptErrorDefault(t, "def broken()\n call(foo: )\nend\n") msg := err.Error() if !strings.Contains(msg, "missing value for keyword argument foo") { t.Fatalf("expected keyword argument parse error, got: %s", msg) @@ -716,11 +684,7 @@ func TestReservedWordLabelsInHashesAndCallKwargs(t *testing.T) { } func TestParseErrorIncludesBlockParameterHint(t *testing.T) { - engine := MustNewEngine(Config{}) - _, err := engine.Compile("def broken()\n [1].each do |a,|\n a\n end\nend\n") - if err == nil { - t.Fatalf("expected compile error") - } + err := compileScriptErrorDefault(t, "def broken()\n [1].each do |a,|\n a\n end\nend\n") msg := err.Error() if !strings.Contains(msg, "trailing comma in block parameter list") { t.Fatalf("expected trailing comma hint, got: %s", msg) @@ -777,37 +741,23 @@ func TestTypedBlockSignatures(t *testing.T) { }) compareArrays(t, untouched, []Value{NewInt(1), NewString("two")}) - _, err := script.Call(context.Background(), "increment_all", []Value{ + requireCallErrorContains(t, script, "increment_all", []Value{ NewArray([]Value{NewInt(1), NewString("oops")}), - }, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument n expected int, got string") { - t.Fatalf("expected typed block argument error, got %v", err) - } - - _, err = script.Call(context.Background(), "typed_union", []Value{ + }, CallOptions{}, "argument n expected int, got string") + requireCallErrorContains(t, script, "typed_union", []Value{ NewArray([]Value{NewBool(true)}), - }, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected int | string, got bool") { - t.Fatalf("expected typed union block argument error, got %v", err) - } - - _, err = script.Call(context.Background(), "enforce_yield_type", []Value{NewString("bad")}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument n expected int, got string") { - t.Fatalf("expected typed yield argument error, got %v", err) - } + }, CallOptions{}, "argument v expected int | string, got bool") + requireCallErrorContains(t, script, "enforce_yield_type", []Value{NewString("bad")}, CallOptions{}, "argument n expected int, got string") } func TestArraySumRejectsNonNumeric(t *testing.T) { - engine := MustNewEngine(Config{}) - script, err := engine.Compile(` + script := compileScriptDefault(t, ` def bad() ["a"].sum() end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } + var err error _, err = script.Call(context.Background(), "bad", nil, CallOptions{}) if err == nil { t.Fatalf("expected runtime error for non-numeric sum") @@ -865,8 +815,7 @@ func TestRuntimeErrorStackTrace(t *testing.T) { } func TestRuntimeErrorCondensesDeepStackRendering(t *testing.T) { - engine := MustNewEngine(Config{RecursionLimit: 128}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{RecursionLimit: 128}, ` def recurse(n) if n <= 0 1 / 0 @@ -878,10 +827,8 @@ func TestRuntimeErrorCondensesDeepStackRendering(t *testing.T) { recurse(40) end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } + var err error _, err = script.Call(context.Background(), "run", nil, CallOptions{}) if err == nil { t.Fatalf("expected runtime error") @@ -989,20 +936,13 @@ func TestWhileLoops(t *testing.T) { t.Fatalf("skip_false expected nil, got %v", got) } - engine := MustNewEngine(Config{StepQuota: 40}) - spinScript, err := engine.Compile(` + spinScript := compileScriptWithConfig(t, Config{StepQuota: 40}, ` def spin() while true end end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } - _, err = spinScript.Call(context.Background(), "spin", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "step quota exceeded") { - t.Fatalf("expected step quota error for infinite while loop, got %v", err) - } + requireCallErrorContains(t, spinScript, "spin", nil, CallOptions{}, "step quota exceeded") } func TestUntilLoops(t *testing.T) { @@ -1044,20 +984,13 @@ func TestUntilLoops(t *testing.T) { t.Fatalf("skip_until_true expected nil, got %v", got) } - engine := MustNewEngine(Config{StepQuota: 40}) - spinScript, err := engine.Compile(` + spinScript := compileScriptWithConfig(t, Config{StepQuota: 40}, ` def spin_until() until false end end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } - _, err = spinScript.Call(context.Background(), "spin_until", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "step quota exceeded") { - t.Fatalf("expected step quota error for infinite until loop, got %v", err) - } + requireCallErrorContains(t, spinScript, "spin_until", nil, CallOptions{}, "step quota exceeded") } func TestCaseWhenExpressions(t *testing.T) { @@ -1206,10 +1139,7 @@ func TestBeginRescueEnsure(t *testing.T) { t.Fatalf("ensure_return_override mismatch: %v", got) } - _, err := script.Call(context.Background(), "ensure_without_rescue", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "division by zero") { - t.Fatalf("expected ensure_without_rescue to preserve original error, got %v", err) - } + requireCallErrorContains(t, script, "ensure_without_rescue", nil, CallOptions{}, "division by zero") } func TestBeginRescueTypedMatching(t *testing.T) { @@ -1261,10 +1191,8 @@ func TestBeginRescueTypedMatching(t *testing.T) { t.Fatalf("typed_union mismatch: %v", got) } - _, err := script.Call(context.Background(), "rescue_mismatch", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "division by zero") { - t.Fatalf("expected typed rescue mismatch to preserve original error, got %v", err) - } + err := callScriptErr(t, context.Background(), script, "rescue_mismatch", nil, CallOptions{}) + requireErrorContains(t, err, "division by zero") var divideErr *RuntimeError if !errors.As(err, ÷Err) { t.Fatalf("expected RuntimeError, got %T", err) @@ -1273,10 +1201,8 @@ func TestBeginRescueTypedMatching(t *testing.T) { t.Fatalf("expected runtime error type %s, got %s", runtimeErrorTypeBase, divideErr.Type) } - _, err = script.Call(context.Background(), "assertion_passthrough", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "raw") { - t.Fatalf("expected assertion passthrough error, got %v", err) - } + err = callScriptErr(t, context.Background(), script, "assertion_passthrough", nil, CallOptions{}) + requireErrorContains(t, err, "raw") var assertionErr *RuntimeError if !errors.As(err, &assertionErr) { t.Fatalf("expected RuntimeError, got %T", err) @@ -1327,8 +1253,7 @@ func TestBeginRescueDoesNotCatchLoopControlSignals(t *testing.T) { } func TestBeginRescueDoesNotCatchHostControlSignals(t *testing.T) { - engine := MustNewEngine(Config{StepQuota: 60}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{StepQuota: 60}, ` def run() begin while true @@ -1338,19 +1263,12 @@ func TestBeginRescueDoesNotCatchHostControlSignals(t *testing.T) { end end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "step quota exceeded") { - t.Fatalf("expected host quota signal to bypass rescue, got %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "step quota exceeded") } func TestBeginRescueTypedUnknownTypeFailsCompile(t *testing.T) { - engine := MustNewEngine(Config{}) - _, err := engine.Compile(` + requireCompileErrorContainsDefault(t, ` def bad() begin 1 / 0 @@ -1358,10 +1276,7 @@ func TestBeginRescueTypedUnknownTypeFailsCompile(t *testing.T) { "fallback" end end - `) - if err == nil || !strings.Contains(err.Error(), "unknown rescue error type NotARealError") { - t.Fatalf("expected unknown rescue type compile error, got %v", err) - } + `, "unknown rescue error type NotARealError") } func TestBeginRescueReraisePreservesStack(t *testing.T) { @@ -1403,10 +1318,8 @@ func TestBeginRescueReraisePreservesStack(t *testing.T) { t.Fatalf("catches_reraise mismatch: %v", got) } - _, err := script.Call(context.Background(), "outer", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "boom") { - t.Fatalf("expected reraise error, got %v", err) - } + err := callScriptErr(t, context.Background(), script, "outer", nil, CallOptions{}) + requireErrorContains(t, err, "boom") var rtErr *RuntimeError if !errors.As(err, &rtErr) { t.Fatalf("expected RuntimeError, got %T", err) @@ -1430,15 +1343,9 @@ func TestBeginRescueReraisePreservesStack(t *testing.T) { t.Fatalf("expected outer frame fourth, got %s", rtErr.Frames[3].Function) } - _, err = script.Call(context.Background(), "raise_outside", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "raise used outside of rescue") { - t.Fatalf("expected raise outside rescue error, got %v", err) - } - - _, err = script.Call(context.Background(), "raise_new_message", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "custom boom") { - t.Fatalf("expected raise message error, got %v", err) - } + requireCallErrorContains(t, script, "raise_outside", nil, CallOptions{}, "raise used outside of rescue") + err = callScriptErr(t, context.Background(), script, "raise_new_message", nil, CallOptions{}) + requireErrorContains(t, err, "custom boom") var raisedErr *RuntimeError if !errors.As(err, &raisedErr) { t.Fatalf("expected RuntimeError, got %T", err) @@ -1506,15 +1413,8 @@ func TestLoopControlBreakAndNext(t *testing.T) { whileBreakNext := callFunc(t, script, "while_break_next", nil) compareArrays(t, whileBreakNext, []Value{NewInt(1), NewInt(2), NewInt(4)}) - _, err := script.Call(context.Background(), "break_outside", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "break used outside of loop") { - t.Fatalf("expected outside-loop break error, got %v", err) - } - - _, err = script.Call(context.Background(), "next_outside", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "next used outside of loop") { - t.Fatalf("expected outside-loop next error, got %v", err) - } + requireCallErrorContains(t, script, "break_outside", nil, CallOptions{}, "break used outside of loop") + requireCallErrorContains(t, script, "next_outside", nil, CallOptions{}, "next used outside of loop") } func TestLoopControlNestedAndBlockBoundaryBehavior(t *testing.T) { @@ -1610,25 +1510,10 @@ func TestLoopControlNestedAndBlockBoundaryBehavior(t *testing.T) { nestedNext := callFunc(t, script, "nested_next", nil) compareArrays(t, nestedNext, []Value{NewInt(11), NewInt(13), NewInt(21), NewInt(23)}) - _, err := script.Call(context.Background(), "break_from_block_boundary", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "break cannot cross call boundary") { - t.Fatalf("expected block-boundary break error, got %v", err) - } - - _, err = script.Call(context.Background(), "next_from_block_boundary", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "next cannot cross call boundary") { - t.Fatalf("expected block-boundary next error, got %v", err) - } - - _, err = script.Call(context.Background(), "break_from_setter_boundary", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "break cannot cross call boundary") { - t.Fatalf("expected setter-boundary break error, got %v", err) - } - - _, err = script.Call(context.Background(), "next_from_setter_boundary", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "next cannot cross call boundary") { - t.Fatalf("expected setter-boundary next error, got %v", err) - } + requireCallErrorContains(t, script, "break_from_block_boundary", nil, CallOptions{}, "break cannot cross call boundary") + requireCallErrorContains(t, script, "next_from_block_boundary", nil, CallOptions{}, "next cannot cross call boundary") + requireCallErrorContains(t, script, "break_from_setter_boundary", nil, CallOptions{}, "break cannot cross call boundary") + requireCallErrorContains(t, script, "next_from_setter_boundary", nil, CallOptions{}, "next cannot cross call boundary") } func TestLoopControlInsideClassMethods(t *testing.T) { @@ -1945,9 +1830,7 @@ func TestTimeParseAndAliases(t *testing.T) { if err == nil { t.Fatalf("expected parse error") } - if !strings.Contains(err.Error(), "could not parse time") { - t.Fatalf("unexpected parse error: %v", err) - } + requireErrorContains(t, err, "could not parse time") } func TestTimeParseCommonLayouts(t *testing.T) { @@ -2040,26 +1923,11 @@ func TestNumericConversionBuiltins(t *testing.T) { t.Fatalf("float_from_string mismatch: %v", got["float_from_string"]) } - _, err := script.Call(context.Background(), "bad_int_fraction", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "to_int cannot convert non-integer float") { - t.Fatalf("expected fractional to_int error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_int_string", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "to_int expects a base-10 integer string") { - t.Fatalf("expected string to_int error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_float_string", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "to_float expects a numeric string") { - t.Fatalf("expected string to_float error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_float_nan", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "to_float expects a finite numeric string") { - t.Fatalf("expected NaN to_float error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_float_inf", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "to_float expects a finite numeric string") { - t.Fatalf("expected Inf to_float error, got %v", err) - } + requireCallErrorContains(t, script, "bad_int_fraction", nil, CallOptions{}, "to_int cannot convert non-integer float") + requireCallErrorContains(t, script, "bad_int_string", nil, CallOptions{}, "to_int expects a base-10 integer string") + requireCallErrorContains(t, script, "bad_float_string", nil, CallOptions{}, "to_float expects a numeric string") + requireCallErrorContains(t, script, "bad_float_nan", nil, CallOptions{}, "to_float expects a finite numeric string") + requireCallErrorContains(t, script, "bad_float_inf", nil, CallOptions{}, "to_float expects a finite numeric string") } func TestJSONBuiltins(t *testing.T) { @@ -2110,15 +1978,8 @@ func TestJSONBuiltins(t *testing.T) { t.Fatalf("stringify mismatch: %q", got) } - _, err := script.Call(context.Background(), "parse_invalid", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.parse invalid JSON") { - t.Fatalf("expected parse invalid JSON error, got %v", err) - } - - _, err = script.Call(context.Background(), "stringify_unsupported", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.stringify unsupported value type function") { - t.Fatalf("expected stringify unsupported error, got %v", err) - } + requireCallErrorContains(t, script, "parse_invalid", nil, CallOptions{}, "JSON.parse invalid JSON") + requireCallErrorContains(t, script, "stringify_unsupported", nil, CallOptions{}, "JSON.stringify unsupported value type function") } func TestRegexBuiltins(t *testing.T) { @@ -2183,10 +2044,7 @@ func TestRegexBuiltins(t *testing.T) { t.Fatalf("replace_boundary mismatch: %v", out["replace_boundary"]) } - _, err := script.Call(context.Background(), "invalid_regex", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "Regex.match invalid regex") { - t.Fatalf("expected invalid regex error, got %v", err) - } + requireCallErrorContains(t, script, "invalid_regex", nil, CallOptions{}, "Regex.match invalid regex") } func TestJSONAndRegexMalformedInputs(t *testing.T) { @@ -2208,27 +2066,14 @@ func TestJSONAndRegexMalformedInputs(t *testing.T) { end `) - _, err := script.Call(context.Background(), "bad_json_trailing", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.parse invalid JSON: trailing data") { - t.Fatalf("expected trailing JSON error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_json_syntax", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.parse invalid JSON") { - t.Fatalf("expected malformed JSON syntax error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_regex_replace", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "Regex.replace invalid regex") { - t.Fatalf("expected regex replace error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_regex_replace_all", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "Regex.replace_all invalid regex") { - t.Fatalf("expected regex replace_all error, got %v", err) - } + requireCallErrorContains(t, script, "bad_json_trailing", nil, CallOptions{}, "JSON.parse invalid JSON: trailing data") + requireCallErrorContains(t, script, "bad_json_syntax", nil, CallOptions{}, "JSON.parse invalid JSON") + requireCallErrorContains(t, script, "bad_regex_replace", nil, CallOptions{}, "Regex.replace invalid regex") + requireCallErrorContains(t, script, "bad_regex_replace_all", nil, CallOptions{}, "Regex.replace_all invalid regex") } func TestJSONAndRegexSizeGuards(t *testing.T) { - engine := MustNewEngine(Config{MemoryQuotaBytes: 4 << 20}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{MemoryQuotaBytes: 4 << 20}, ` def parse_raw(raw) JSON.parse(raw) end @@ -2245,46 +2090,23 @@ func TestJSONAndRegexSizeGuards(t *testing.T) { Regex.replace_all(text, pattern, replacement) end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } largeJSON := `{"data":"` + strings.Repeat("x", maxJSONPayloadBytes) + `"}` - _, err = script.Call(context.Background(), "parse_raw", []Value{NewString(largeJSON)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.parse input exceeds limit") { - t.Fatalf("expected JSON.parse size guard error, got %v", err) - } + requireCallErrorContains(t, script, "parse_raw", []Value{NewString(largeJSON)}, CallOptions{}, "JSON.parse input exceeds limit") largeValue := NewHash(map[string]Value{ "data": NewString(strings.Repeat("x", maxJSONPayloadBytes)), }) - _, err = script.Call(context.Background(), "stringify_value", []Value{largeValue}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "JSON.stringify output exceeds limit") { - t.Fatalf("expected JSON.stringify size guard error, got %v", err) - } + requireCallErrorContains(t, script, "stringify_value", []Value{largeValue}, CallOptions{}, "JSON.stringify output exceeds limit") largePattern := strings.Repeat("a", maxRegexPatternSize+1) - _, err = script.Call(context.Background(), "regex_match_guard", []Value{NewString(largePattern), NewString("aaa")}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "Regex.match pattern exceeds limit") { - t.Fatalf("expected Regex.match pattern guard error, got %v", err) - } + requireCallErrorContains(t, script, "regex_match_guard", []Value{NewString(largePattern), NewString("aaa")}, CallOptions{}, "Regex.match pattern exceeds limit") largeText := strings.Repeat("a", maxRegexInputBytes+1) - _, err = script.Call(context.Background(), "regex_match_guard", []Value{NewString("a+"), NewString(largeText)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "Regex.match text exceeds limit") { - t.Fatalf("expected Regex.match text guard error, got %v", err) - } + requireCallErrorContains(t, script, "regex_match_guard", []Value{NewString("a+"), NewString(largeText)}, CallOptions{}, "Regex.match text exceeds limit") hugeReplacement := strings.Repeat("x", maxRegexInputBytes/2) - _, err = script.Call( - context.Background(), - "regex_replace_all_guard", - []Value{NewString("abc"), NewString(""), NewString(hugeReplacement)}, - CallOptions{}, - ) - if err == nil || !strings.Contains(err.Error(), "Regex.replace_all output exceeds limit") { - t.Fatalf("expected Regex.replace_all output guard error, got %v", err) - } + requireCallErrorContains(t, script, "regex_replace_all_guard", []Value{NewString("abc"), NewString(""), NewString(hugeReplacement)}, CallOptions{}, "Regex.replace_all output exceeds limit") largeRun := strings.Repeat("a", maxRegexInputBytes-1024) replaced, err := script.Call( @@ -2329,10 +2151,9 @@ func TestLocaleSensitiveOperationsDeterministic(t *testing.T) { } func TestRandomIdentifierBuiltins(t *testing.T) { - engine := MustNewEngine(Config{ + script := compileScriptWithConfig(t, Config{ RandomReader: bytes.NewReader(bytes.Repeat([]byte{0xAB}, 128)), - }) - script, err := engine.Compile(` + }, ` def values() { uuid: uuid(), @@ -2357,9 +2178,6 @@ func TestRandomIdentifierBuiltins(t *testing.T) { uuid(1) end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } result, err := script.Call(context.Background(), "values", nil, CallOptions{}) if err != nil { @@ -2393,48 +2211,30 @@ func TestRandomIdentifierBuiltins(t *testing.T) { t.Fatalf("id_default mismatch: %v", got) } - if _, err := script.Call(context.Background(), "bad_length_type", nil, CallOptions{}); err == nil || !strings.Contains(err.Error(), "random_id length must be integer") { - t.Fatalf("expected length type error, got %v", err) - } - if _, err := script.Call(context.Background(), "bad_length_float", nil, CallOptions{}); err == nil || !strings.Contains(err.Error(), "random_id length must be integer") { - t.Fatalf("expected length float error, got %v", err) - } - if _, err := script.Call(context.Background(), "bad_length_value", nil, CallOptions{}); err == nil || !strings.Contains(err.Error(), "random_id length must be positive") { - t.Fatalf("expected length value error, got %v", err) - } - if _, err := script.Call(context.Background(), "bad_uuid_args", nil, CallOptions{}); err == nil || !strings.Contains(err.Error(), "uuid does not take arguments") { - t.Fatalf("expected uuid args error, got %v", err) - } + requireCallErrorContains(t, script, "bad_length_type", nil, CallOptions{}, "random_id length must be integer") + requireCallErrorContains(t, script, "bad_length_float", nil, CallOptions{}, "random_id length must be integer") + requireCallErrorContains(t, script, "bad_length_value", nil, CallOptions{}, "random_id length must be positive") + requireCallErrorContains(t, script, "bad_uuid_args", nil, CallOptions{}, "uuid does not take arguments") } func TestRandomIdentifierBuiltinsRandomSourceFailure(t *testing.T) { - engine := MustNewEngine(Config{RandomReader: bytes.NewReader([]byte{1, 2, 3})}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{RandomReader: bytes.NewReader([]byte{1, 2, 3})}, ` def run() uuid() end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "random source failed") { - t.Fatalf("expected random source failure, got %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "random source failed") } func TestRandomIdentifierBuiltinsUsesUnbiasedSampling(t *testing.T) { - engine := MustNewEngine(Config{RandomReader: bytes.NewReader([]byte{248, 1})}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{RandomReader: bytes.NewReader([]byte{248, 1})}, ` def run() random_id(1) end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } + var err error got, err := script.Call(context.Background(), "run", nil, CallOptions{}) if err != nil { t.Fatalf("call failed: %v", err) @@ -2445,20 +2245,13 @@ func TestRandomIdentifierBuiltinsUsesUnbiasedSampling(t *testing.T) { } func TestRandomIdentifierBuiltinsRejectsStalledEntropy(t *testing.T) { - engine := MustNewEngine(Config{RandomReader: bytes.NewReader(bytes.Repeat([]byte{0xFF}, 1024))}) - script, err := engine.Compile(` + script := compileScriptWithConfig(t, Config{RandomReader: bytes.NewReader(bytes.Repeat([]byte{0xFF}, 1024))}, ` def run() random_id(4) end `) - if err != nil { - t.Fatalf("compile error: %v", err) - } - _, err = script.Call(context.Background(), "run", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "random_id entropy source rejected too many bytes") { - t.Fatalf("expected stalled entropy error, got %v", err) - } + requireCallErrorContains(t, script, "run", nil, CallOptions{}, "random_id entropy source rejected too many bytes") } func TestNumericHelpers(t *testing.T) { @@ -2745,94 +2538,52 @@ func TestTypedFunctions(t *testing.T) { if !kwPos.Equal(NewInt(2)) { t.Fatalf("kw_only positional mismatch: %v", kwPos) } - _, err := script.Call(context.Background(), "kw_only", []Value{NewInt(1)}, CallOptions{ + requireCallErrorContains(t, script, "kw_only", []Value{NewInt(1)}, CallOptions{ Globals: map[string]Value{}, - }) - if err == nil || !strings.Contains(err.Error(), "missing argument m") { - t.Fatalf("expected kw_only missing arg error, got %v", err) - } + }, "missing argument m") mixedResult := callFunc(t, script, "mixed", []Value{NewInt(1), NewInt(2)}) if !mixedResult.Equal(NewInt(3)) { t.Fatalf("mixed result mismatch: %v", mixedResult) } - _, err = script.Call(context.Background(), "pick_second", []Value{NewString("bad"), NewInt(2)}, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument n expected int, got string") { - t.Fatalf("expected argument type error, got %v", err) - } + requireCallErrorContains(t, script, "pick_second", []Value{NewString("bad"), NewInt(2)}, CallOptions{}, "argument n expected int, got string") - _, err = script.Call(context.Background(), "bad_return", []Value{NewInt(1)}, CallOptions{}) - if err == nil { - res, _ := script.Call(context.Background(), "bad_return", []Value{NewInt(1)}, CallOptions{}) - t.Fatalf("expected return type error, got value %v (%v)", res, res.Kind()) - } - if !strings.Contains(err.Error(), "return value for bad_return expected int, got string") { - t.Fatalf("expected return type error, got %v", err) - } + requireCallErrorContains(t, script, "bad_return", []Value{NewInt(1)}, CallOptions{}, "return value for bad_return expected int, got string") - _, err = script.Call(context.Background(), "union_echo", []Value{NewBool(true)}, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument v expected int | string, got bool") { - t.Fatalf("expected union arg type error, got %v", err) - } + requireCallErrorContains(t, script, "union_echo", []Value{NewBool(true)}, CallOptions{}, "argument v expected int | string, got bool") - _, err = script.Call(context.Background(), "union_bad_return", nil, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "return value for union_bad_return expected int | string, got bool") { - t.Fatalf("expected union return type error, got %v", err) - } + requireCallErrorContains(t, script, "union_bad_return", nil, CallOptions{}, "return value for union_bad_return expected int | string, got bool") - _, err = script.Call(context.Background(), "ints_only", []Value{ + requireCallErrorContains(t, script, "ints_only", []Value{ NewArray([]Value{NewInt(1), NewString("oops")}), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument values expected array, got array") { - t.Fatalf("expected typed array arg error, got %v", err) - } + }, CallOptions{}, "argument values expected array, got array") - _, err = script.Call(context.Background(), "totals_by_player", []Value{ + requireCallErrorContains(t, script, "totals_by_player", []Value{ NewHash(map[string]Value{"alice": NewString("oops")}), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument totals expected hash, got { alice: string }") { - t.Fatalf("expected typed hash arg error, got %v", err) - } + }, CallOptions{}, "argument totals expected hash, got { alice: string }") - _, err = script.Call(context.Background(), "mixed_items", []Value{ + requireCallErrorContains(t, script, "mixed_items", []Value{ NewArray([]Value{NewBool(true)}), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument values expected array, got array") { - t.Fatalf("expected typed union array arg error, got %v", err) - } + }, CallOptions{}, "argument values expected array, got array") - _, err = script.Call(context.Background(), "player_payload", []Value{ + requireCallErrorContains(t, script, "player_payload", []Value{ NewHash(map[string]Value{ "id": NewString("p-1"), "score": NewInt(42), "role": NewString("captain"), }), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument payload expected { active: bool?, id: string, score: int }, got { id: string, role: string, score: int }") { - t.Fatalf("expected shape extra-field error, got %v", err) - } + }, CallOptions{}, "argument payload expected { active: bool?, id: string, score: int }, got { id: string, role: string, score: int }") - _, err = script.Call(context.Background(), "player_payload", []Value{ + requireCallErrorContains(t, script, "player_payload", []Value{ NewHash(map[string]Value{ "id": NewString("p-1"), "score": NewString("wrong"), "active": NewBool(true), }), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument payload expected { active: bool?, id: string, score: int }, got { active: bool, id: string, score: string }") { - t.Fatalf("expected shape field-type error, got %v", err) - } + }, CallOptions{}, "argument payload expected { active: bool?, id: string, score: int }, got { active: bool, id: string, score: string }") - _, err = script.Call(context.Background(), "shaped_rows", []Value{ + requireCallErrorContains(t, script, "shaped_rows", []Value{ NewArray([]Value{ NewHash(map[string]Value{ "id": NewString("p-1"), @@ -2841,11 +2592,7 @@ func TestTypedFunctions(t *testing.T) { }), }), }), - }, CallOptions{}) - if err == nil || - !strings.Contains(err.Error(), "argument rows expected array<{ id: string, stats: { wins: int } }>, got array<{ id: string, stats: { wins: string } }>") { - t.Fatalf("expected nested shape error, got %v", err) - } + }, CallOptions{}, "argument rows expected array<{ id: string, stats: { wins: int } }>, got array<{ id: string, stats: { wins: string } }>") } func TestTypeSemanticsContainersNullabilityCoercionAndKeywordStrictness(t *testing.T) { @@ -2902,12 +2649,9 @@ func TestTypeSemanticsContainersNullabilityCoercionAndKeywordStrictness(t *testi t.Fatalf("accepts_ints int-only mismatch: %v", got) } compareArrays(t, got, []Value{NewInt(1), NewInt(2)}) - _, err := script.Call(context.Background(), "accepts_ints", []Value{ + requireCallErrorContains(t, script, "accepts_ints", []Value{ NewArray([]Value{NewInt(1), NewFloat(2.5)}), - }, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument values expected array, got array") { - t.Fatalf("expected container element strictness error, got %v", err) - } + }, CallOptions{}, "argument values expected array, got array") if got := callFunc(t, script, "nullable_short", []Value{NewNil()}); got.Kind() != KindNil { t.Fatalf("nullable_short nil mismatch: %#v", got) @@ -2921,32 +2665,16 @@ func TestTypeSemanticsContainersNullabilityCoercionAndKeywordStrictness(t *testi if got := callFunc(t, script, "nullable_union", []Value{NewString("ok")}); got.Kind() != KindString || got.String() != "ok" { t.Fatalf("nullable_union string mismatch: %#v", got) } - _, err = script.Call(context.Background(), "nullable_short", []Value{NewInt(1)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected string?, got int") { - t.Fatalf("expected nullable shorthand mismatch, got %v", err) - } - _, err = script.Call(context.Background(), "nullable_union", []Value{NewInt(1)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected string | nil, got int") { - t.Fatalf("expected nullable union mismatch, got %v", err) - } - - _, err = script.Call(context.Background(), "takes_int", []Value{NewString("1")}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected int, got string") { - t.Fatalf("expected no-coercion mismatch, got %v", err) - } + requireCallErrorContains(t, script, "nullable_short", []Value{NewInt(1)}, CallOptions{}, "argument v expected string?, got int") + requireCallErrorContains(t, script, "nullable_union", []Value{NewInt(1)}, CallOptions{}, "argument v expected string | nil, got int") + requireCallErrorContains(t, script, "takes_int", []Value{NewString("1")}, CallOptions{}, "argument v expected int, got string") extraKw := map[string]Value{ "a": NewInt(1), "extra": NewInt(2), } - _, err = script.Call(context.Background(), "typed_kw", nil, CallOptions{Keywords: extraKw}) - if err == nil || !strings.Contains(err.Error(), "unexpected keyword argument extra") { - t.Fatalf("expected typed function unknown kwarg strictness, got %v", err) - } - _, err = script.Call(context.Background(), "untyped_kw", nil, CallOptions{Keywords: extraKw}) - if err == nil || !strings.Contains(err.Error(), "unexpected keyword argument extra") { - t.Fatalf("expected untyped function unknown kwarg strictness, got %v", err) - } + requireCallErrorContains(t, script, "typed_kw", nil, CallOptions{Keywords: extraKw}, "unexpected keyword argument extra") + requireCallErrorContains(t, script, "untyped_kw", nil, CallOptions{Keywords: extraKw}, "unexpected keyword argument extra") } func TestTypedFunctionsRegressionAnyAndNullableBehavior(t *testing.T) { @@ -2983,10 +2711,7 @@ func TestTypedFunctionsRegressionAnyAndNullableBehavior(t *testing.T) { if got := callFunc(t, script, "takes_nullable", []Value{NewString("ok")}); got.Kind() != KindString || got.String() != "ok" { t.Fatalf("takes_nullable string mismatch: %#v", got) } - _, err := script.Call(context.Background(), "takes_nullable", []Value{NewInt(1)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected string?, got int") { - t.Fatalf("expected nullable type mismatch, got %v", err) - } + requireCallErrorContains(t, script, "takes_nullable", []Value{NewInt(1)}, CallOptions{}, "argument v expected string?, got int") if got := callFunc(t, script, "takes_nullable_union", []Value{NewNil()}); got.Kind() != KindNil { t.Fatalf("takes_nullable_union nil mismatch: %#v", got) @@ -2994,10 +2719,7 @@ func TestTypedFunctionsRegressionAnyAndNullableBehavior(t *testing.T) { if got := callFunc(t, script, "takes_nullable_union", []Value{NewString("ok")}); got.Kind() != KindString || got.String() != "ok" { t.Fatalf("takes_nullable_union string mismatch: %#v", got) } - _, err = script.Call(context.Background(), "takes_nullable_union", []Value{NewInt(1)}, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "argument v expected string | nil, got int") { - t.Fatalf("expected nullable union mismatch, got %v", err) - } + requireCallErrorContains(t, script, "takes_nullable_union", []Value{NewInt(1)}, CallOptions{}, "argument v expected string | nil, got int") } func TestTypedFunctionsRejectCyclicHashInputWithoutInfiniteRecursion(t *testing.T) { @@ -3022,9 +2744,7 @@ func TestTypedFunctionsRejectCyclicHashInputWithoutInfiniteRecursion(t *testing. if err == nil { t.Fatalf("expected type validation error for cyclic payload") } - if !strings.Contains(err.Error(), "argument payload expected hash>") { - t.Fatalf("unexpected type error: %v", err) - } + requireErrorContains(t, err, "argument payload expected hash>") case <-time.After(2 * time.Second): t.Fatalf("type validation did not terminate for cyclic payload") } @@ -3217,18 +2937,9 @@ func TestArrayAndHashHelpers(t *testing.T) { t.Fatalf("amountCents mismatch: %v", event.Hash()["amountCents"]) } - _, err := script.Call(context.Background(), "bad_hash_remap", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "hash.remap_keys mapping values must be symbol or string") { - t.Fatalf("expected bad remap error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_deep_transform", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "hash.deep_transform_keys block must return symbol or string") { - t.Fatalf("expected bad deep transform error, got %v", err) - } - _, err = script.Call(context.Background(), "bad_deep_transform_cycle", nil, CallOptions{}) - if err == nil || !strings.Contains(err.Error(), "hash.deep_transform_keys does not support cyclic structures") { - t.Fatalf("expected cyclic deep transform error, got %v", err) - } + requireCallErrorContains(t, script, "bad_hash_remap", nil, CallOptions{}, "hash.remap_keys mapping values must be symbol or string") + requireCallErrorContains(t, script, "bad_deep_transform", nil, CallOptions{}, "hash.deep_transform_keys block must return symbol or string") + requireCallErrorContains(t, script, "bad_deep_transform_cycle", nil, CallOptions{}, "hash.deep_transform_keys does not support cyclic structures") } func TestStringHelpers(t *testing.T) { @@ -4089,9 +3800,7 @@ end`, if err == nil { t.Fatalf("expected error containing %q", tt.errMsg) } - if !strings.Contains(err.Error(), tt.errMsg) { - t.Fatalf("expected error containing %q, got: %v", tt.errMsg, err) - } + requireErrorContains(t, err, tt.errMsg) }) } } diff --git a/vibes/strict_effects_test.go b/vibes/strict_effects_test.go index 248ec1e..17f65cf 100644 --- a/vibes/strict_effects_test.go +++ b/vibes/strict_effects_test.go @@ -2,7 +2,6 @@ package vibes import ( "context" - "strings" "testing" ) @@ -22,16 +21,12 @@ func (c strictEffectsCapability) Bind(binding CapabilityBinding) (map[string]Val } func TestStrictEffectsRejectsCallableGlobals(t *testing.T) { - engine := MustNewEngine(Config{StrictEffects: true}) - script, err := engine.Compile(`def run() + script := compileScriptWithConfig(t, Config{StrictEffects: true}, `def run() db.save("player-1") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } called := false - _, err = script.Call(context.Background(), "run", nil, CallOptions{ + err := callScriptErr(t, context.Background(), script, "run", nil, CallOptions{ Globals: map[string]Value{ "db": NewObject(map[string]Value{ "save": NewBuiltin("db.save", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { @@ -41,57 +36,38 @@ end`) }), }, }) - if err == nil { - t.Fatalf("expected strict effects global validation error") - } - if got := err.Error(); !strings.Contains(got, "strict effects: global db must be data-only") { - t.Fatalf("unexpected error: %v", err) - } + requireErrorContains(t, err, "strict effects: global db must be data-only") if called { t.Fatalf("callable global should not execute when strict validation fails") } } func TestStrictEffectsAllowsDataGlobals(t *testing.T) { - engine := MustNewEngine(Config{StrictEffects: true}) - script, err := engine.Compile(`def run() + script := compileScriptWithConfig(t, Config{StrictEffects: true}, `def run() tenant end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } - result, err := script.Call(context.Background(), "run", nil, CallOptions{ + result := callScript(t, context.Background(), script, "run", nil, CallOptions{ Globals: map[string]Value{ "tenant": NewString("acme"), }, }) - if err != nil { - t.Fatalf("call failed: %v", err) - } if result.Kind() != KindString || result.String() != "acme" { t.Fatalf("unexpected result: %#v", result) } } func TestStrictEffectsAllowsCapabilities(t *testing.T) { - engine := MustNewEngine(Config{StrictEffects: true}) - script, err := engine.Compile(`def run() + script := compileScriptWithConfig(t, Config{StrictEffects: true}, `def run() db.save("player-1") end`) - if err != nil { - t.Fatalf("compile failed: %v", err) - } called := false - result, err := script.Call(context.Background(), "run", nil, CallOptions{ + result := callScript(t, context.Background(), script, "run", nil, CallOptions{ Capabilities: []CapabilityAdapter{ strictEffectsCapability{called: &called}, }, }) - if err != nil { - t.Fatalf("call failed: %v", err) - } if result.Kind() != KindString || result.String() != "saved" { t.Fatalf("unexpected result: %#v", result) } diff --git a/vibes/syntax_freeze_test.go b/vibes/syntax_freeze_test.go index ed7d5be..ba39eec 100644 --- a/vibes/syntax_freeze_test.go +++ b/vibes/syntax_freeze_test.go @@ -107,9 +107,7 @@ end`, for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if _, err := engine.Compile(tc.source); err != nil { - t.Fatalf("compile failed: %v", err) - } + _ = compileScriptWithEngine(t, engine, tc.source) }) } }