mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
test: add comprehensive VM opcode tests (#750)
Add extensive test coverage for VM opcodes including: - Basic operations (OpLoadEnv, OpTrue, OpFalse, OpNil) - Arithmetic operations (OpNegate, OpExponent) - String operations (OpEqualString, OpMatches) - Collection operations (OpIn, OpLen) - Type operations (OpCast) - Control flow (OpThrow) - Function calls (OpCall0-3, OpCallN) - Index and count manipulation (OpGetIndex, OpSetIndex) - Profiling operations (OpProfileStart, OpProfileEnd) Also add high-level expression tests for: - Arithmetic expressions - String operations - Data structure operations - Group and sort operations Each test case includes detailed comments and covers both success and error cases. These tests combined with a debugger helped me navigate the VM internals significantly. Signed-off-by: Ville Vesilehto <[email protected]>
This commit is contained in:
+932
@@ -259,3 +259,935 @@ func TestRun_OpInvalid(t *testing.T) {
|
||||
_, err := vm.Run(program, nil)
|
||||
require.EqualError(t, err, "invalid opcode")
|
||||
}
|
||||
|
||||
func TestVM_OpcodeOperations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
env map[string]any
|
||||
want any
|
||||
expectError string
|
||||
}{
|
||||
// Arithmetic Operations
|
||||
{
|
||||
name: "basic addition",
|
||||
expr: "2 + 3",
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "mixed type arithmetic",
|
||||
expr: "2.5 + 3",
|
||||
want: 5.5,
|
||||
},
|
||||
{
|
||||
name: "chained arithmetic",
|
||||
expr: "1 + 2 * 3 - 4 / 2",
|
||||
want: 5.0,
|
||||
},
|
||||
{
|
||||
name: "modulo operation",
|
||||
expr: "5 % 2",
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "exponent operation",
|
||||
expr: "2 ^ 3",
|
||||
want: 8.0,
|
||||
},
|
||||
{
|
||||
name: "negation",
|
||||
expr: "-5",
|
||||
want: -5,
|
||||
},
|
||||
|
||||
// String Operations
|
||||
{
|
||||
name: "string concatenation",
|
||||
expr: `"hello" + " " + "world"`,
|
||||
want: "hello world",
|
||||
},
|
||||
{
|
||||
name: "string starts with",
|
||||
expr: `"hello world" startsWith "hello"`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string ends with",
|
||||
expr: `"hello world" endsWith "world"`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string contains",
|
||||
expr: `"hello world" contains "lo wo"`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string matches regex",
|
||||
expr: `"hello123" matches "^hello\\d+$"`,
|
||||
want: true,
|
||||
},
|
||||
|
||||
// Data Structure Operations
|
||||
{
|
||||
name: "array creation and access",
|
||||
expr: "[1, 2, 3][1]",
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "map creation and access",
|
||||
expr: `{"a": 1, "b": 2}.b`,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "array length",
|
||||
expr: "len([1, 2, 3])",
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "array slice",
|
||||
expr: "[1, 2, 3, 4][1:3]",
|
||||
want: []any{2, 3},
|
||||
},
|
||||
{
|
||||
name: "array range",
|
||||
expr: "1..5",
|
||||
want: []int{1, 2, 3, 4, 5},
|
||||
},
|
||||
|
||||
// Error Cases
|
||||
{
|
||||
name: "invalid array index",
|
||||
expr: "[1,2,3][5]",
|
||||
expectError: "index out of range",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program, err := expr.Compile(tt.expr, expr.Env(tt.env))
|
||||
require.NoError(t, err)
|
||||
|
||||
testVM := &vm.VM{}
|
||||
got, err := testVM.Run(program, tt.env)
|
||||
|
||||
if tt.expectError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVM_GroupAndSortOperations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
env map[string]any
|
||||
want any
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "group by single field",
|
||||
expr: `groupBy([{"id": 1, "type": "a"}, {"id": 2, "type": "b"}, {"id": 3, "type": "a"}], #.type)`,
|
||||
want: map[any][]any{
|
||||
"a": {
|
||||
map[string]any{"id": 1, "type": "a"},
|
||||
map[string]any{"id": 3, "type": "a"},
|
||||
},
|
||||
"b": {
|
||||
map[string]any{"id": 2, "type": "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort by field ascending",
|
||||
expr: `sortBy([{"id": 3}, {"id": 1}, {"id": 2}], #.id)`,
|
||||
want: []any{
|
||||
map[string]any{"id": 1},
|
||||
map[string]any{"id": 2},
|
||||
map[string]any{"id": 3},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort by field descending",
|
||||
expr: `sortBy([{"id": 3}, {"id": 1}, {"id": 2}], #.id, "desc")`,
|
||||
want: []any{
|
||||
map[string]any{"id": 3},
|
||||
map[string]any{"id": 2},
|
||||
map[string]any{"id": 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort by computed value",
|
||||
expr: `sortBy([1, 2, 3, 4], # % 2)`,
|
||||
want: []any{2, 4, 1, 3},
|
||||
},
|
||||
{
|
||||
name: "group by with complex key",
|
||||
expr: `groupBy([1, 2, 3, 4, 5, 6], # % 2 == 0 ? "even" : "odd")`,
|
||||
want: map[any][]any{
|
||||
"even": {2, 4, 6},
|
||||
"odd": {1, 3, 5},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid sort order",
|
||||
expr: `sortBy([1, 2, 3], #, "invalid")`,
|
||||
expectError: "unknown order",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program, err := expr.Compile(tt.expr, expr.Env(tt.env))
|
||||
require.NoError(t, err)
|
||||
|
||||
testVM := &vm.VM{}
|
||||
got, err := testVM.Run(program, tt.env)
|
||||
|
||||
if tt.expectError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVM_ProfileOperations tests the profiling opcodes
|
||||
func TestVM_ProfileOperations(t *testing.T) {
|
||||
program := &vm.Program{
|
||||
Bytecode: []vm.Opcode{
|
||||
vm.OpProfileStart,
|
||||
vm.OpPush,
|
||||
vm.OpProfileEnd,
|
||||
},
|
||||
Arguments: []int{0, 0, 0},
|
||||
Constants: []any{
|
||||
&vm.Span{},
|
||||
},
|
||||
}
|
||||
|
||||
testVM := &vm.VM{}
|
||||
_, err := testVM.Run(program, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
span := program.Constants[0].(*vm.Span)
|
||||
require.True(t, span.Duration > 0, "Profile duration should be greater than 0")
|
||||
}
|
||||
|
||||
// TestVM_IndexOperations tests the index manipulation opcodes
|
||||
func TestVM_IndexOperations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "decrement index in loop",
|
||||
expr: "reduce([1,2,3], #acc + #, 0)",
|
||||
want: 6,
|
||||
},
|
||||
{
|
||||
name: "set index in loop",
|
||||
expr: "map([1,2,3], # * 2)",
|
||||
want: []any{2, 4, 6},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program, err := expr.Compile(tt.expr)
|
||||
require.NoError(t, err)
|
||||
|
||||
testVM := &vm.VM{}
|
||||
got, err := testVM.Run(program, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVM_DirectCallOpcodes tests the specialized call opcodes directly
|
||||
func TestVM_DirectCallOpcodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bytecode []vm.Opcode
|
||||
args []int
|
||||
consts []any
|
||||
funcs []vm.Function
|
||||
want any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "OpCall0",
|
||||
bytecode: []vm.Opcode{vm.OpCall0},
|
||||
args: []int{0},
|
||||
funcs: []vm.Function{
|
||||
func(args ...any) (any, error) {
|
||||
return 42, nil
|
||||
},
|
||||
},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "OpCall1",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush,
|
||||
vm.OpCall1,
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{10},
|
||||
funcs: []vm.Function{
|
||||
func(args ...any) (any, error) {
|
||||
return args[0].(int) * 2, nil
|
||||
},
|
||||
},
|
||||
want: 20,
|
||||
},
|
||||
{
|
||||
name: "OpCall2",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush,
|
||||
vm.OpPush,
|
||||
vm.OpCall2,
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{10, 5},
|
||||
funcs: []vm.Function{
|
||||
func(args ...any) (any, error) {
|
||||
return args[0].(int) + args[1].(int), nil
|
||||
},
|
||||
},
|
||||
want: 15,
|
||||
},
|
||||
{
|
||||
name: "OpCall3",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush,
|
||||
vm.OpPush,
|
||||
vm.OpPush,
|
||||
vm.OpCall3,
|
||||
},
|
||||
args: []int{0, 1, 2, 0},
|
||||
consts: []any{10, 5, 2},
|
||||
funcs: []vm.Function{
|
||||
func(args ...any) (any, error) {
|
||||
return args[0].(int) + args[1].(int) + args[2].(int), nil
|
||||
},
|
||||
},
|
||||
want: 17,
|
||||
},
|
||||
{
|
||||
name: "OpCallN with error",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpLoadFunc,
|
||||
vm.OpCallN,
|
||||
},
|
||||
args: []int{0, 0}, // Function index, number of args (0)
|
||||
funcs: []vm.Function{
|
||||
func(args ...any) (any, error) {
|
||||
return nil, fmt.Errorf("test error")
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program := vm.NewProgram(
|
||||
nil, // source
|
||||
nil, // node
|
||||
nil, // locations
|
||||
0, // variables
|
||||
tt.consts,
|
||||
tt.bytecode,
|
||||
tt.args,
|
||||
tt.funcs,
|
||||
nil, // debugInfo
|
||||
nil, // span
|
||||
)
|
||||
vm := &vm.VM{}
|
||||
got, err := vm.Run(program, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVM_CallN(t *testing.T) {
|
||||
input := `fn(1, 2, 3)`
|
||||
|
||||
tree, err := parser.Parse(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
env := map[string]any{
|
||||
"fn": func(args ...any) (any, error) {
|
||||
sum := 0
|
||||
for _, arg := range args {
|
||||
sum += arg.(int)
|
||||
}
|
||||
return sum, nil
|
||||
},
|
||||
}
|
||||
|
||||
config := conf.New(env)
|
||||
program, err := compiler.Compile(tree, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := vm.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, out)
|
||||
}
|
||||
|
||||
// TestVM_IndexAndCountOperations tests the index and count manipulation opcodes directly
|
||||
func TestVM_IndexAndCountOperations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bytecode []vm.Opcode
|
||||
args []int
|
||||
consts []any
|
||||
want any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "GetIndex",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array to stack
|
||||
vm.OpBegin, // Start scope
|
||||
vm.OpGetIndex, // Get current index
|
||||
},
|
||||
args: []int{0, 0, 0},
|
||||
consts: []any{[]any{1, 2, 3}}, // Array for scope
|
||||
want: 0, // Initial index is 0
|
||||
},
|
||||
{
|
||||
name: "DecrementIndex",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array to stack
|
||||
vm.OpBegin, // Start scope
|
||||
vm.OpDecrementIndex, // Decrement index
|
||||
vm.OpGetIndex, // Get current index
|
||||
},
|
||||
args: []int{0, 0, 0, 0},
|
||||
consts: []any{[]any{1, 2, 3}}, // Array for scope
|
||||
want: -1, // After decrement
|
||||
},
|
||||
{
|
||||
name: "GetCount",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array to stack
|
||||
vm.OpBegin, // Start scope
|
||||
vm.OpGetCount, // Get current count
|
||||
},
|
||||
args: []int{0, 0, 0},
|
||||
consts: []any{[]any{1, 2, 3}}, // Array for scope
|
||||
want: 0, // Initial count is 0
|
||||
},
|
||||
{
|
||||
name: "IncrementCount",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array to stack
|
||||
vm.OpBegin, // Start scope
|
||||
vm.OpIncrementCount, // Increment count
|
||||
vm.OpGetCount, // Get current count
|
||||
},
|
||||
args: []int{0, 0, 0, 0},
|
||||
consts: []any{[]any{1, 2, 3}}, // Array for scope
|
||||
want: 1, // After increment
|
||||
},
|
||||
{
|
||||
name: "Multiple operations",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array to stack
|
||||
vm.OpBegin, // Start scope
|
||||
vm.OpIncrementCount, // Count = 1
|
||||
vm.OpIncrementCount, // Count = 2
|
||||
vm.OpDecrementIndex, // Index = -1
|
||||
vm.OpDecrementIndex, // Index = -2
|
||||
vm.OpGetCount, // Push count (2)
|
||||
vm.OpGetIndex, // Push index (-2)
|
||||
vm.OpAdd, // Add them together
|
||||
},
|
||||
args: []int{0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
consts: []any{[]any{1, 2, 3}}, // Array for scope
|
||||
want: 0, // 2 + (-2) = 0
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program := vm.NewProgram(
|
||||
nil, // source
|
||||
nil, // node
|
||||
nil, // locations
|
||||
0, // variables
|
||||
tt.consts,
|
||||
tt.bytecode,
|
||||
tt.args,
|
||||
nil, // functions
|
||||
nil, // debugInfo
|
||||
nil, // span
|
||||
)
|
||||
vm := &vm.VM{}
|
||||
got, err := vm.Run(program, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVM_DirectBasicOpcodes tests basic opcodes directly
|
||||
func TestVM_DirectBasicOpcodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bytecode []vm.Opcode
|
||||
args []int
|
||||
consts []any
|
||||
env any
|
||||
want any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "OpLoadEnv",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpLoadEnv, // Load entire environment
|
||||
},
|
||||
args: []int{0},
|
||||
env: map[string]any{"key": "value"},
|
||||
want: map[string]any{"key": "value"},
|
||||
},
|
||||
{
|
||||
name: "OpTrue",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpTrue,
|
||||
},
|
||||
args: []int{0},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpFalse",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpFalse,
|
||||
},
|
||||
args: []int{0},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpNil",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpNil,
|
||||
},
|
||||
args: []int{0},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "OpNegate int",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push number
|
||||
vm.OpNegate, // Negate it
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{42},
|
||||
want: -42,
|
||||
},
|
||||
{
|
||||
name: "OpNegate float",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push number
|
||||
vm.OpNegate, // Negate it
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{42.5},
|
||||
want: -42.5,
|
||||
},
|
||||
{
|
||||
name: "OpNot true",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpTrue, // Push true
|
||||
vm.OpNot, // Negate it
|
||||
},
|
||||
args: []int{0, 0},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpNot false",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpFalse, // Push false
|
||||
vm.OpNot, // Negate it
|
||||
},
|
||||
args: []int{0, 0},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpNot error",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push non-bool
|
||||
vm.OpNot, // Try to negate it
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{"not a bool"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpEqualString equal",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push first string
|
||||
vm.OpPush, // Push second string
|
||||
vm.OpEqualString, // Compare strings
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"hello", "hello"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpEqualString not equal",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push first string
|
||||
vm.OpPush, // Push second string
|
||||
vm.OpEqualString, // Compare strings
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"hello", "world"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpEqualString with empty strings",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push first string
|
||||
vm.OpPush, // Push second string
|
||||
vm.OpEqualString, // Compare strings
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"", ""},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpEqualString type error",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push non-string
|
||||
vm.OpPush, // Push string
|
||||
vm.OpEqualString, // Try to compare
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{42, "hello"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpInt",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpInt, // Push int directly from args
|
||||
},
|
||||
args: []int{42}, // The value 42 is passed directly in args
|
||||
consts: []any{}, // No constants needed
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "OpInt negative",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpInt, // Push negative int directly from args
|
||||
},
|
||||
args: []int{-42}, // The value -42 is passed directly in args
|
||||
consts: []any{}, // No constants needed
|
||||
want: -42,
|
||||
},
|
||||
{
|
||||
name: "OpInt zero",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpInt, // Push zero directly from args
|
||||
},
|
||||
args: []int{0}, // The value 0 is passed directly in args
|
||||
consts: []any{}, // No constants needed
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "OpIn array true",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push element
|
||||
vm.OpPush, // Push array
|
||||
vm.OpIn, // Check if element is in array
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{2, []any{1, 2, 3}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpIn array false",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push element
|
||||
vm.OpPush, // Push array
|
||||
vm.OpIn, // Check if element is in array
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{4, []any{1, 2, 3}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpIn map true",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push key
|
||||
vm.OpPush, // Push map
|
||||
vm.OpIn, // Check if key is in map
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"b", map[string]any{"a": 1, "b": 2}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpIn map false",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push key
|
||||
vm.OpPush, // Push map
|
||||
vm.OpIn, // Check if key is in map
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"c", map[string]any{"a": 1, "b": 2}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpExponent integers",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push base
|
||||
vm.OpPush, // Push exponent
|
||||
vm.OpExponent, // Calculate power
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{2, 3},
|
||||
want: 8.0,
|
||||
},
|
||||
{
|
||||
name: "OpExponent floats",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push base
|
||||
vm.OpPush, // Push exponent
|
||||
vm.OpExponent, // Calculate power
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{2.0, 3.0},
|
||||
want: 8.0,
|
||||
},
|
||||
{
|
||||
name: "OpExponent negative exponent",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push base
|
||||
vm.OpPush, // Push exponent
|
||||
vm.OpExponent, // Calculate power
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{2.0, -2.0},
|
||||
want: 0.25,
|
||||
},
|
||||
{
|
||||
name: "OpMatches valid regex",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push string
|
||||
vm.OpPush, // Push pattern
|
||||
vm.OpMatches, // Match string against pattern
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"hello123", "^hello\\d+$"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OpMatches non-matching regex",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push string
|
||||
vm.OpPush, // Push pattern
|
||||
vm.OpMatches, // Match string against pattern
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"hello", "^\\d+$"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OpMatches invalid regex",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push string
|
||||
vm.OpPush, // Push pattern
|
||||
vm.OpMatches, // Match string against pattern
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{"hello", "[invalid"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpMatches type error",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push non-string
|
||||
vm.OpPush, // Push pattern
|
||||
vm.OpMatches, // Match against pattern
|
||||
},
|
||||
args: []int{0, 1, 0},
|
||||
consts: []any{42, "^\\d+$"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpCast int to float64",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push int
|
||||
vm.OpCast, // Cast to float64
|
||||
},
|
||||
args: []int{0, 2},
|
||||
consts: []any{42},
|
||||
want: float64(42),
|
||||
},
|
||||
{
|
||||
name: "OpCast int32 to int64",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push int32
|
||||
vm.OpCast, // Cast to int64
|
||||
},
|
||||
args: []int{0, 1},
|
||||
consts: []any{int32(42)},
|
||||
want: int64(42),
|
||||
},
|
||||
{
|
||||
name: "OpCast invalid type",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push string
|
||||
vm.OpCast, // Try to cast to float64
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{"not a number"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpLen array",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push array
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{[]any{1, 2, 3}},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "OpLen empty array",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push empty array
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{[]any{}},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "OpLen string",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push string
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{"hello"},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "OpLen empty string",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push empty string
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{""},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "OpLen map",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push map
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{map[string]any{"a": 1, "b": 2, "c": 3}},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "OpLen empty map",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push empty map
|
||||
vm.OpLen, // Get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{map[string]any{}},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "OpLen invalid type",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push number
|
||||
vm.OpLen, // Try to get length
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{42},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpThrow with string",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push error message
|
||||
vm.OpThrow, // Throw error
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{"test error"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpThrow with error",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpPush, // Push error
|
||||
vm.OpThrow, // Throw error
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{fmt.Errorf("test error")},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "OpDefault",
|
||||
bytecode: []vm.Opcode{
|
||||
vm.OpEnd + 1, // OpEnd is always last, this is anunknown opcode
|
||||
},
|
||||
args: []int{0, 0},
|
||||
consts: []any{fmt.Errorf("test error")},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program := vm.NewProgram(
|
||||
nil, // source
|
||||
nil, // node
|
||||
nil, // locations
|
||||
0, // variables
|
||||
tt.consts,
|
||||
tt.bytecode,
|
||||
tt.args,
|
||||
nil, // functions
|
||||
nil, // debugInfo
|
||||
nil, // span
|
||||
)
|
||||
vm := &vm.VM{}
|
||||
got, err := vm.Run(program, tt.env)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user