Commit c337a511 authored by Dylan Trotter's avatar Dylan Trotter Committed by GitHub

Add Param and ParamSpec types for arg validation (#191)

Replaced FunctionArg with Param and moved arg validation logic out of
Code and into new ParamSpec type. ParamSpec can then be made more
general purpose and used for builtin functions.
parent 92ad5eec
......@@ -400,17 +400,17 @@ class ExprVisitor(ast.NodeVisitor):
visitor._visit_each(node.body) # pylint: disable=protected-access
result = self.block.alloc_temp()
with self.block.alloc_temp('[]πg.FunctionArg') as func_args:
with self.block.alloc_temp('[]πg.Param') as func_args:
args = node.args
argc = len(args.args)
self.writer.write('{} = make([]πg.FunctionArg, {})'.format(
self.writer.write('{} = make([]πg.Param, {})'.format(
func_args.expr, argc))
# The list of defaults only contains args for which a default value is
# specified so pad it with None to make it the same length as args.
defaults = [None] * (argc - len(args.defaults)) + args.defaults
for i, (a, d) in enumerate(zip(args.args, defaults)):
with self.visit(d) if d else expr.nil_expr as default:
tmpl = '$args[$i] = πg.FunctionArg{Name: $name, Def: $default}'
tmpl = '$args[$i] = πg.Param{Name: $name, Def: $default}'
self.writer.write_tmpl(tmpl, args=func_args.expr, i=i,
name=util.go_str(a.id), default=default.expr)
flags = []
......
......@@ -15,7 +15,6 @@
package grumpy
import (
"fmt"
"reflect"
)
......@@ -38,31 +37,16 @@ type Code struct {
name string `attr:"co_name"`
filename string `attr:"co_filename"`
// argc is the number of positional arguments.
argc int `attr:"co_argcount"`
// minArgc is the number of positional non-keyword arguments (i.e. the
// minimum number of positional arguments that must be passed).
minArgc int
flags CodeFlag `attr:"co_flags"`
args []FunctionArg
fn func(*Frame, []*Object) (*Object, *BaseException)
argc int `attr:"co_argcount"`
flags CodeFlag `attr:"co_flags"`
paramSpec *ParamSpec
fn func(*Frame, []*Object) (*Object, *BaseException)
}
// NewCode creates a new Code object that executes the given fn.
func NewCode(name, filename string, args []FunctionArg, flags CodeFlag, fn func(*Frame, []*Object) (*Object, *BaseException)) *Code {
argc := len(args)
minArgc := 0
for ; minArgc < argc; minArgc++ {
if args[minArgc].Def != nil {
break
}
}
for _, arg := range args[minArgc:argc] {
if arg.Def == nil {
format := "%s() non-keyword arg %s after keyword arg"
logFatal(fmt.Sprintf(format, name, arg.Name))
}
}
return &Code{Object{typ: CodeType}, name, filename, argc, minArgc, flags, args, fn}
func NewCode(name, filename string, params []Param, flags CodeFlag, fn func(*Frame, []*Object) (*Object, *BaseException)) *Code {
s := NewParamSpec(name, params, flags&CodeFlagVarArg != 0, flags&CodeFlagKWArg != 0)
return &Code{Object{typ: CodeType}, name, filename, len(params), flags, s, fn}
}
func toCodeUnsafe(o *Object) *Code {
......@@ -71,75 +55,17 @@ func toCodeUnsafe(o *Object) *Code {
// Eval runs the code object c in the context of the given globals.
func (c *Code) Eval(f *Frame, globals *Dict, args Args, kwargs KWArgs) (*Object, *BaseException) {
// Validate parameters.
argc := len(args)
if argc > c.argc && c.flags&CodeFlagVarArg == 0 {
format := "%s() takes %d arguments (%d given)"
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf(format, c.name, c.argc, argc))
}
numBodyArgs := c.argc
varArgIndex, kwArgIndex := -1, -1
if c.flags&CodeFlagVarArg != 0 {
varArgIndex = numBodyArgs
numBodyArgs++
}
if c.flags&CodeFlagKWArg != 0 {
kwArgIndex = numBodyArgs
numBodyArgs++
}
bodyArgs := f.MakeArgs(numBodyArgs)
i := 0
for ; i < argc && i < c.argc; i++ {
bodyArgs[i] = args[i]
}
if varArgIndex != -1 {
bodyArgs[varArgIndex] = NewTuple(args[i:].makeCopy()...).ToObject()
}
var kwargDict *Dict
if kwArgIndex != -1 {
kwargDict = NewDict()
bodyArgs[kwArgIndex] = kwargDict.ToObject()
}
for _, kw := range kwargs {
name := kw.Name
j := 0
for ; j < c.argc; j++ {
if c.args[j].Name == name {
if bodyArgs[j] != nil {
format := "%s() got multiple values for keyword argument '%s'"
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf(format, c.name, name))
}
bodyArgs[j] = kw.Value
break
}
}
if j == c.argc {
if kwargDict == nil {
format := "%s() got an unexpected keyword argument '%s'"
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf(format, c.name, name))
}
if raised := kwargDict.SetItemString(f, name, kw.Value); raised != nil {
return nil, raised
}
}
}
for ; i < c.argc; i++ {
arg := c.args[i]
if bodyArgs[i] == nil {
if arg.Def == nil {
format := "%s() takes at least %d arguments (%d given)"
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf(format, c.name, c.minArgc, argc))
}
bodyArgs[i] = arg.Def
}
validated := f.MakeArgs(c.paramSpec.Count)
if raised := c.paramSpec.Validate(f, validated, args, kwargs); raised != nil {
return nil, raised
}
oldExc, oldTraceback := f.ExcInfo()
next := newChildFrame(f)
next.code = c
next.globals = globals
ret, raised := c.fn(next, bodyArgs)
ret, raised := c.fn(next, validated)
next.release()
f.FreeArgs(bodyArgs)
f.FreeArgs(validated)
if raised == nil {
// Restore exc_info to what it was when we left the previous
// frame.
......
......@@ -28,7 +28,7 @@ func TestNewCodeKeywordsCheck(t *testing.T) {
logFatal = func(msg string) {
got = msg
}
NewCode("foo", "foo.py", []FunctionArg{{"bar", None}, {"baz", nil}}, 0, nil)
NewCode("foo", "foo.py", []Param{{"bar", None}, {"baz", nil}}, 0, nil)
if want := "foo() non-keyword arg baz after keyword arg"; got != want {
t.Errorf("NewCode logged %q, want %q", got, want)
}
......@@ -49,23 +49,23 @@ func TestNewCode(t *testing.T) {
}
cases := []invokeTestCase{
invokeTestCase{args: wrapArgs(NewCode("f1", "foo.py", nil, 0, fn)), want: NewTuple().ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []FunctionArg{{"a", nil}}, 0, fn), 123), want: newTestTuple(123).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []FunctionArg{{"a", nil}}, 0, fn)), kwargs: wrapKWArgs("a", "apple"), want: newTestTuple("apple").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []FunctionArg{{"a", nil}}, 0, fn)), kwargs: wrapKWArgs("b", "bear"), wantExc: mustCreateException(TypeErrorType, "f2() got an unexpected keyword argument 'b'")},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []FunctionArg{{"a", nil}}, 0, fn)), wantExc: mustCreateException(TypeErrorType, "f2() takes at least 1 arguments (0 given)")},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []FunctionArg{{"a", nil}}, 0, fn), 1, 2, 3), wantExc: mustCreateException(TypeErrorType, "f2() takes 1 arguments (3 given)")},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []FunctionArg{{"a", nil}, {"b", nil}}, 0, fn), 1, 2), want: newTestTuple(1, 2).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []FunctionArg{{"a", nil}, {"b", nil}}, 0, fn), 1), kwargs: wrapKWArgs("b", "bear"), want: newTestTuple(1, "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []FunctionArg{{"a", nil}, {"b", nil}}, 0, fn)), kwargs: wrapKWArgs("b", "bear", "a", "apple"), want: newTestTuple("apple", "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []FunctionArg{{"a", nil}, {"b", nil}}, 0, fn), 1), kwargs: wrapKWArgs("a", "alpha"), wantExc: mustCreateException(TypeErrorType, "f3() got multiple values for keyword argument 'a'")},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []FunctionArg{{"a", nil}, {"b", None}}, 0, fn), 123), want: newTestTuple(123, None).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []FunctionArg{{"a", nil}, {"b", None}}, 0, fn), 123, "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []FunctionArg{{"a", nil}, {"b", None}}, 0, fn)), kwargs: wrapKWArgs("a", 123, "b", "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f5", "foo.py", []FunctionArg{{"a", nil}}, CodeFlagVarArg, fn), 1), want: newTestTuple(1, NewTuple()).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f5", "foo.py", []FunctionArg{{"a", nil}}, CodeFlagVarArg, fn), 1, 2, 3), want: newTestTuple(1, newTestTuple(2, 3)).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []FunctionArg{{"a", nil}}, CodeFlagKWArg, fn), "bar"), want: newTestTuple("bar", NewDict()).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []FunctionArg{{"a", nil}}, CodeFlagKWArg, fn)), kwargs: wrapKWArgs("a", "apple", "b", "bear"), want: newTestTuple("apple", newTestDict("b", "bear")).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []FunctionArg{{"a", nil}}, CodeFlagKWArg, fn), "bar"), kwargs: wrapKWArgs("b", "baz", "c", "qux"), want: newTestTuple("bar", newTestDict("b", "baz", "c", "qux")).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []Param{{"a", nil}}, 0, fn), 123), want: newTestTuple(123).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []Param{{"a", nil}}, 0, fn)), kwargs: wrapKWArgs("a", "apple"), want: newTestTuple("apple").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []Param{{"a", nil}}, 0, fn)), kwargs: wrapKWArgs("b", "bear"), wantExc: mustCreateException(TypeErrorType, "f2() got an unexpected keyword argument 'b'")},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []Param{{"a", nil}}, 0, fn)), wantExc: mustCreateException(TypeErrorType, "f2() takes at least 1 arguments (0 given)")},
invokeTestCase{args: wrapArgs(NewCode("f2", "foo.py", []Param{{"a", nil}}, 0, fn), 1, 2, 3), wantExc: mustCreateException(TypeErrorType, "f2() takes 1 arguments (3 given)")},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []Param{{"a", nil}, {"b", nil}}, 0, fn), 1, 2), want: newTestTuple(1, 2).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []Param{{"a", nil}, {"b", nil}}, 0, fn), 1), kwargs: wrapKWArgs("b", "bear"), want: newTestTuple(1, "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []Param{{"a", nil}, {"b", nil}}, 0, fn)), kwargs: wrapKWArgs("b", "bear", "a", "apple"), want: newTestTuple("apple", "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f3", "foo.py", []Param{{"a", nil}, {"b", nil}}, 0, fn), 1), kwargs: wrapKWArgs("a", "alpha"), wantExc: mustCreateException(TypeErrorType, "f3() got multiple values for keyword argument 'a'")},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []Param{{"a", nil}, {"b", None}}, 0, fn), 123), want: newTestTuple(123, None).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []Param{{"a", nil}, {"b", None}}, 0, fn), 123, "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f4", "foo.py", []Param{{"a", nil}, {"b", None}}, 0, fn)), kwargs: wrapKWArgs("a", 123, "b", "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f5", "foo.py", []Param{{"a", nil}}, CodeFlagVarArg, fn), 1), want: newTestTuple(1, NewTuple()).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f5", "foo.py", []Param{{"a", nil}}, CodeFlagVarArg, fn), 1, 2, 3), want: newTestTuple(1, newTestTuple(2, 3)).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []Param{{"a", nil}}, CodeFlagKWArg, fn), "bar"), want: newTestTuple("bar", NewDict()).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []Param{{"a", nil}}, CodeFlagKWArg, fn)), kwargs: wrapKWArgs("a", "apple", "b", "bear"), want: newTestTuple("apple", newTestDict("b", "bear")).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f6", "foo.py", []Param{{"a", nil}}, CodeFlagKWArg, fn), "bar"), kwargs: wrapKWArgs("b", "baz", "c", "qux"), want: newTestTuple("bar", newTestDict("b", "baz", "c", "qux")).ToObject()},
invokeTestCase{args: wrapArgs(NewCode("f7", "foo.py", nil, 0, nilFn)), want: None},
}
for _, cas := range cases {
......
......@@ -83,15 +83,6 @@ type Function struct {
globals *Dict `attr:"func_globals"`
}
// FunctionArg describes a parameter to a Python function.
type FunctionArg struct {
// Name is the argument name.
Name string
// Def is the default value to use if the argument is not provided. If
// no default is specified then Def is nil.
Def *Object
}
// NewFunction creates a function object corresponding to a Python function
// taking the given args, vararg and kwarg. When called, the arguments are
// validated before calling fn. This includes checking that an appropriate
......
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grumpy
import (
"fmt"
)
// Param describes a parameter to a Python function.
type Param struct {
// Name is the argument name.
Name string
// Def is the default value to use if the argument is not provided. If
// no default is specified then Def is nil.
Def *Object
}
// ParamSpec describes a Python function's parameters.
type ParamSpec struct {
Count int
name string
minArgs int
varArgIndex int
kwArgIndex int
params []Param
}
// NewParamSpec returns a new ParamSpec that accepts the given positional
// parameters and optional vararg and/or kwarg parameter.
func NewParamSpec(name string, params []Param, varArg bool, kwArg bool) *ParamSpec {
s := &ParamSpec{name: name, params: params, varArgIndex: -1, kwArgIndex: -1}
numParams := len(params)
for ; s.minArgs < numParams; s.minArgs++ {
if params[s.minArgs].Def != nil {
break
}
}
for _, p := range params[s.minArgs:numParams] {
if p.Def == nil {
format := "%s() non-keyword arg %s after keyword arg"
logFatal(fmt.Sprintf(format, name, p.Name))
}
}
s.Count = numParams
if varArg {
s.varArgIndex = s.Count
s.Count++
}
if kwArg {
s.kwArgIndex = s.Count
s.Count++
}
return s
}
// Validate ensures that a the args and kwargs passed are valid arguments for
// the param spec s. The validated parameters are output to the validated slice
// which must have len s.Count.
func (s *ParamSpec) Validate(f *Frame, validated []*Object, args Args, kwargs KWArgs) *BaseException {
if nv := len(validated); nv != s.Count {
format := "%s(): validated slice was incorrect size: %d, want %d"
return f.RaiseType(SystemErrorType, fmt.Sprintf(format, s.name, nv, s.Count))
}
numParams := len(s.params)
argc := len(args)
if argc > numParams && s.varArgIndex == -1 {
format := "%s() takes %d arguments (%d given)"
return f.RaiseType(TypeErrorType, fmt.Sprintf(format, s.name, numParams, argc))
}
i := 0
for ; i < argc && i < numParams; i++ {
validated[i] = args[i]
}
if s.varArgIndex != -1 {
validated[s.varArgIndex] = NewTuple(args[i:].makeCopy()...).ToObject()
}
var kwargDict *Dict
if s.kwArgIndex != -1 {
kwargDict = NewDict()
validated[s.kwArgIndex] = kwargDict.ToObject()
}
for _, kw := range kwargs {
name := kw.Name
j := 0
for ; j < numParams; j++ {
if s.params[j].Name == name {
if validated[j] != nil {
format := "%s() got multiple values for keyword argument '%s'"
return f.RaiseType(TypeErrorType, fmt.Sprintf(format, s.name, name))
}
validated[j] = kw.Value
break
}
}
if j == numParams {
if kwargDict == nil {
format := "%s() got an unexpected keyword argument '%s'"
return f.RaiseType(TypeErrorType, fmt.Sprintf(format, s.name, name))
}
if raised := kwargDict.SetItemString(f, name, kw.Value); raised != nil {
return raised
}
}
}
for ; i < numParams; i++ {
p := s.params[i]
if validated[i] == nil {
if p.Def == nil {
format := "%s() takes at least %d arguments (%d given)"
return f.RaiseType(TypeErrorType, fmt.Sprintf(format, s.name, s.minArgs, argc))
}
validated[i] = p.Def
}
}
return nil
}
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grumpy
import (
"testing"
)
func TestParamSpecValidate(t *testing.T) {
testFunc := newBuiltinFunction("TestParamSpecValidate", func(f *Frame, args Args, kwargs KWArgs) (*Object, *BaseException) {
if len(args) < 1 {
return nil, f.RaiseType(TypeErrorType, "not enough args")
}
val, raised := ToNative(f, args[0])
if raised != nil {
return nil, raised
}
s, ok := val.Interface().(*ParamSpec)
if !ok {
return nil, f.RaiseType(TypeErrorType, "expected ParamSpec arg")
}
validated := make([]*Object, s.Count)
if raised := s.Validate(f, validated, args[1:], kwargs); raised != nil {
return nil, raised
}
return NewTuple(validated...).ToObject(), nil
})
cases := []invokeTestCase{
invokeTestCase{args: wrapArgs(NewParamSpec("f1", nil, false, false)), want: NewTuple().ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f2", []Param{{"a", nil}}, false, false), 123), want: newTestTuple(123).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f2", []Param{{"a", nil}}, false, false)), kwargs: wrapKWArgs("a", "apple"), want: newTestTuple("apple").ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f2", []Param{{"a", nil}}, false, false)), kwargs: wrapKWArgs("b", "bear"), wantExc: mustCreateException(TypeErrorType, "f2() got an unexpected keyword argument 'b'")},
invokeTestCase{args: wrapArgs(NewParamSpec("f2", []Param{{"a", nil}}, false, false)), wantExc: mustCreateException(TypeErrorType, "f2() takes at least 1 arguments (0 given)")},
invokeTestCase{args: wrapArgs(NewParamSpec("f2", []Param{{"a", nil}}, false, false), 1, 2, 3), wantExc: mustCreateException(TypeErrorType, "f2() takes 1 arguments (3 given)")},
invokeTestCase{args: wrapArgs(NewParamSpec("f3", []Param{{"a", nil}, {"b", nil}}, false, false), 1, 2), want: newTestTuple(1, 2).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f3", []Param{{"a", nil}, {"b", nil}}, false, false), 1), kwargs: wrapKWArgs("b", "bear"), want: newTestTuple(1, "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f3", []Param{{"a", nil}, {"b", nil}}, false, false)), kwargs: wrapKWArgs("b", "bear", "a", "apple"), want: newTestTuple("apple", "bear").ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f3", []Param{{"a", nil}, {"b", nil}}, false, false), 1), kwargs: wrapKWArgs("a", "alpha"), wantExc: mustCreateException(TypeErrorType, "f3() got multiple values for keyword argument 'a'")},
invokeTestCase{args: wrapArgs(NewParamSpec("f4", []Param{{"a", nil}, {"b", None}}, false, false), 123), want: newTestTuple(123, None).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f4", []Param{{"a", nil}, {"b", None}}, false, false), 123, "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f4", []Param{{"a", nil}, {"b", None}}, false, false)), kwargs: wrapKWArgs("a", 123, "b", "bar"), want: newTestTuple(123, "bar").ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f5", []Param{{"a", nil}}, true, false), 1), want: newTestTuple(1, NewTuple()).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f5", []Param{{"a", nil}}, true, false), 1, 2, 3), want: newTestTuple(1, newTestTuple(2, 3)).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f6", []Param{{"a", nil}}, false, true), "bar"), want: newTestTuple("bar", NewDict()).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f6", []Param{{"a", nil}}, false, true)), kwargs: wrapKWArgs("a", "apple", "b", "bear"), want: newTestTuple("apple", newTestDict("b", "bear")).ToObject()},
invokeTestCase{args: wrapArgs(NewParamSpec("f6", []Param{{"a", nil}}, false, true), "bar"), kwargs: wrapKWArgs("b", "baz", "c", "qux"), want: newTestTuple("bar", newTestDict("b", "baz", "c", "qux")).ToObject()},
}
for _, cas := range cases {
if err := runInvokeTestCase(testFunc.ToObject(), &cas); err != "" {
t.Error(err)
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment