diff --git a/plugin/go.go b/plugin/go.go index b2ad53022..7d5dd110e 100644 --- a/plugin/go.go +++ b/plugin/go.go @@ -202,11 +202,25 @@ func (p *Go) evaluate(vm *interp.Interpreter) (res any, err error) { func (p *Go) setParam(vm *interp.Interpreter) func(param string, val any) error { return func(param string, val any) error { - _, err := vm.Eval(fmt.Sprintf("%s := %#v;", param, val)) + _, err := vm.Eval(fmt.Sprintf("%s := %s;", param, goLiteral(val))) return err } } +// goLiteral renders val as Go source. Floats need their type spelled out: +// %#v prints float64(100) as `100`, which the interpreter infers as int. Any +// arithmetic mixing the parameter with a float constant then fails to compile. +func goLiteral(val any) string { + switch v := val.(type) { + case float32: + return fmt.Sprintf("float32(%v)", v) + case float64: + return fmt.Sprintf("float64(%v)", v) + default: + return fmt.Sprintf("%#v", val) + } +} + var _ IntSetter = (*Go)(nil) // IntSetter sends int request diff --git a/plugin/go_test.go b/plugin/go_test.go new file mode 100644 index 000000000..3733b79e5 --- /dev/null +++ b/plugin/go_test.go @@ -0,0 +1,71 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// A float parameter must keep its type inside the interpreter even when the +// value is a whole number. Otherwise arithmetic mixing it with a float constant +// fails with "invalid operation: mismatched types int and untyped float". +func TestGoFloatParam(t *testing.T) { + for _, tc := range []struct { + value float64 + want int64 + }{ + {11000, 73}, + {15000, 100}, + {0, 0}, + {11000.5, 73}, + } { + p, err := NewGoPluginFromConfig(t.Context(), map[string]any{ + "in": []map[string]any{{ + "name": "limit", + "type": "float", + "config": map[string]any{"source": "const", "value": tc.value}, + }}, + "script": "int(limit*100/15000 + 0.5)", + }) + assert.NoError(t, err) + + g, err := p.(IntGetter).IntGetter() + assert.NoError(t, err) + + v, err := g() + assert.NoError(t, err) + assert.Equal(t, tc.want, v, tc.value) + } +} + +// int parameters keep integer semantics +func TestGoIntParam(t *testing.T) { + p, err := NewGoPluginFromConfig(t.Context(), map[string]any{ + "in": []map[string]any{{ + "name": "mode", + "type": "int", + "config": map[string]any{"source": "const", "value": 3}, + }}, + "script": "mode & 1", + }) + assert.NoError(t, err) + + g, err := p.(IntGetter).IntGetter() + assert.NoError(t, err) + + v, err := g() + assert.NoError(t, err) + assert.Equal(t, int64(1), v) +} + +// the value handed to a setter is a parameter, too +func TestGoFloatSetter(t *testing.T) { + p, err := NewGoPluginFromConfig(t.Context(), map[string]any{ + "script": "int(limit*100/15000 + 0.5)", + }) + assert.NoError(t, err) + + s, err := p.(FloatSetter).FloatSetter("limit") + assert.NoError(t, err) + assert.NoError(t, s(11000)) +}