diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index 6b4d5aa2f..e2a31d913 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -499,3 +499,52 @@ callflags: "--config", cfgPath, "--out", "zzz") }) } + +func TestCompile_GuessEventTypes(t *testing.T) { + app := cli.NewApp() + app.Commands = NewCommands() + app.ExitErrHandler = func(*cli.Context, error) {} + + checkError := func(t *testing.T, msg string, args ...string) { + // cli.ExitError doesn't implement wraping properly, so we check for an error message. + err := app.Run(args) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), msg), "got: %v", err) + } + check := func(t *testing.T, source string, expectedErrText string) { + tmpDir := t.TempDir() + configFile := filepath.Join(source, "invalid.yml") + manifestF := filepath.Join(tmpDir, "invalid.manifest.json") + bindingF := filepath.Join(tmpDir, "invalid.binding.yml") + nefF := filepath.Join(tmpDir, "invalid.out.nef") + cmd := []string{"", "contract", "compile", + "--in", source, + "--config", configFile, + "--manifest", manifestF, + "--bindings", bindingF, + "--out", nefF, + "--guess-eventtypes", + } + checkError(t, expectedErrText, cmd...) + } + + t.Run("not declared in manifest", func(t *testing.T) { + check(t, filepath.Join("testdata", "invalid5"), "inconsistent usages of event `Non declared event`: not declared in the contract config") + }) + t.Run("invalid number of params", func(t *testing.T) { + check(t, filepath.Join("testdata", "invalid6"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") + }) + /* + // TODO: this on is a controversial one. If event information is provided in the config file, then conversion code + // will be emitted by the compiler according to the parameter type provided via config. Thus, we can be sure that + // either event parameter has the type specified in the config file or the execution of the contract will fail. + // Thus, this testcase is always failing (no compilation error occures). + // Question: do we want to compare `RealType` of the emitted parameter with the one expected in the manifest? + t.Run("SC parameter type mismatch", func(t *testing.T) { + check(t, filepath.Join("testdata", "invalid7"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") + }) + */ + t.Run("extended types mismatch", func(t *testing.T) { + check(t, filepath.Join("testdata", "invalid8"), "inconsistent usages of event `SomeEvent`: extended type of param #0 mismatch") + }) +} diff --git a/cli/smartcontract/testdata/invalid5/invalid.go b/cli/smartcontract/testdata/invalid5/invalid.go new file mode 100644 index 000000000..0cae2ed71 --- /dev/null +++ b/cli/smartcontract/testdata/invalid5/invalid.go @@ -0,0 +1,7 @@ +package invalid5 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +func Main() { + runtime.Notify("Non declared event") +} diff --git a/cli/smartcontract/testdata/invalid5/invalid.yml b/cli/smartcontract/testdata/invalid5/invalid.yml new file mode 100644 index 000000000..eda948267 --- /dev/null +++ b/cli/smartcontract/testdata/invalid5/invalid.yml @@ -0,0 +1 @@ +name: Test undeclared event \ No newline at end of file diff --git a/cli/smartcontract/testdata/invalid6/invalid.go b/cli/smartcontract/testdata/invalid6/invalid.go new file mode 100644 index 000000000..dd3a3ecdd --- /dev/null +++ b/cli/smartcontract/testdata/invalid6/invalid.go @@ -0,0 +1,7 @@ +package invalid6 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +func Main() { + runtime.Notify("SomeEvent", "p1", "p2") +} diff --git a/cli/smartcontract/testdata/invalid6/invalid.yml b/cli/smartcontract/testdata/invalid6/invalid.yml new file mode 100644 index 000000000..13933803e --- /dev/null +++ b/cli/smartcontract/testdata/invalid6/invalid.yml @@ -0,0 +1,6 @@ +name: Test undeclared event +events: + - name: SomeEvent + parameters: + - name: p1 + type: String \ No newline at end of file diff --git a/cli/smartcontract/testdata/invalid7/invalid.go b/cli/smartcontract/testdata/invalid7/invalid.go new file mode 100644 index 000000000..41bc20c47 --- /dev/null +++ b/cli/smartcontract/testdata/invalid7/invalid.go @@ -0,0 +1,7 @@ +package invalid7 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +func Main() { + runtime.Notify("SomeEvent", "p1", 5) +} diff --git a/cli/smartcontract/testdata/invalid7/invalid.yml b/cli/smartcontract/testdata/invalid7/invalid.yml new file mode 100644 index 000000000..a217deefd --- /dev/null +++ b/cli/smartcontract/testdata/invalid7/invalid.yml @@ -0,0 +1,8 @@ +name: Test undeclared event +events: + - name: SomeEvent + parameters: + - name: p1 + type: String + - name: p2 + type: String \ No newline at end of file diff --git a/cli/smartcontract/testdata/invalid8/invalid.go b/cli/smartcontract/testdata/invalid8/invalid.go new file mode 100644 index 000000000..dba9173ca --- /dev/null +++ b/cli/smartcontract/testdata/invalid8/invalid.go @@ -0,0 +1,17 @@ +package invalid8 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +type SomeStruct1 struct { + Field1 int +} + +type SomeStruct2 struct { + Field2 string +} + +func Main() { + // Inconsistent event params usages (different named types throughout the usages). + runtime.Notify("SomeEvent", SomeStruct1{Field1: 123}) + runtime.Notify("SomeEvent", SomeStruct2{Field2: "str"}) +} diff --git a/cli/smartcontract/testdata/invalid8/invalid.yml b/cli/smartcontract/testdata/invalid8/invalid.yml new file mode 100644 index 000000000..6c0c34748 --- /dev/null +++ b/cli/smartcontract/testdata/invalid8/invalid.yml @@ -0,0 +1,6 @@ +name: Test undeclared event +events: + - name: SomeEvent + parameters: + - name: p1 + type: Array \ No newline at end of file diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index eac2bdced..2b90ac07e 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -356,16 +356,43 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { if o.GuessEventTypes { if len(di.EmittedEvents) > 0 { for eventName, eventUsages := range di.EmittedEvents { + var manifestEvent HybridEvent + for _, e := range o.ContractEvents { + if e.Name == eventName { + manifestEvent = e + break + } + } + if len(manifestEvent.Name) == 0 { + return nil, fmt.Errorf("inconsistent usages of event `%s`: not declared in the contract config", eventName) + } + exampleUsage := eventUsages[0] + for _, usage := range eventUsages { + if len(usage.Params) != len(manifestEvent.Parameters) { + return nil, fmt.Errorf("inconsistent usages of event `%s` against config: number of params mismatch: %d vs %d", eventName, len(exampleUsage.Params), len(manifestEvent.Parameters)) + } + for i, actual := range usage.Params { + mParam := manifestEvent.Parameters[i] + // TODO: see the TestCompile_GuessEventTypes, "SC parameter type mismatch" section, + // do we want to compare with actual.RealType? The conversion code is emitted by the + // compiler for it, so we expect the parameter to be of the proper type. + if !(mParam.Type == smartcontract.AnyType || actual.TypeSC == mParam.Type) { + return nil, fmt.Errorf("inconsistent usages of event `%s` against config: SC type of param #%d mismatch: %s vs %s", eventName, i, actual.TypeSC, mParam.Type) + } + expected := exampleUsage.Params[i] + if !actual.ExtendedType.Equals(expected.ExtendedType) { + return nil, fmt.Errorf("inconsistent usages of event `%s`: extended type of param #%d mismatch", eventName, i) + } + } + } eBindingName := rpcbinding.ToEventBindingName(eventName) - // Take into account the first usage only. - // TODO: extend it to the rest of invocations. - for typeName, extType := range eventUsages[0].ExtTypes { + for typeName, extType := range exampleUsage.ExtTypes { if _, ok := cfg.NamedTypes[typeName]; !ok { cfg.NamedTypes[typeName] = extType } } - for _, p := range eventUsages[0].Params { + for _, p := range exampleUsage.Params { pBindingName := rpcbinding.ToParameterBindingName(p.Name) pname := eBindingName + "." + pBindingName if p.RealType.TypeName != "" { diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index 33bb21dd2..bd7d42cbc 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -265,3 +265,34 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract func upperFirst(s string) string { return strings.ToUpper(s[0:1]) + s[1:] } + +// Equals compares two extended types field-by-field and returns true if they are +// equal. +func (e *ExtendedType) Equals(other *ExtendedType) bool { + if e == nil && other == nil { + return true + } + if e != nil && other == nil || + e == nil && other != nil { + return false + } + if !((e.Base == other.Base || (e.Base == smartcontract.ByteArrayType || e.Base == smartcontract.StringType) && + (other.Base == smartcontract.ByteArrayType || other.Base == smartcontract.StringType)) && + e.Name == other.Name && + e.Interface == other.Interface && + e.Key == other.Key) { + return false + } + if len(e.Fields) != len(other.Fields) { + return false + } + for i := range e.Fields { + if e.Fields[i].Field != other.Fields[i].Field { + return false + } + if !e.Fields[i].ExtendedType.Equals(&other.Fields[i].ExtendedType) { + return false + } + } + return (e.Value == nil && other.Value == nil) || (e.Value != nil && other.Value != nil && e.Value.Equals(other.Value)) +} diff --git a/pkg/smartcontract/binding/generate_test.go b/pkg/smartcontract/binding/generate_test.go new file mode 100644 index 000000000..64522d28c --- /dev/null +++ b/pkg/smartcontract/binding/generate_test.go @@ -0,0 +1,229 @@ +package binding + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/stretchr/testify/assert" +) + +func TestExtendedType_Equals(t *testing.T) { + crazyT := ExtendedType{ + Base: smartcontract.StringType, + Name: "qwertyu", + Interface: "qwerty", + Key: smartcontract.BoolType, + Value: &ExtendedType{ + Base: smartcontract.IntegerType, + }, + Fields: []FieldExtendedType{ + { + Field: "qwe", + ExtendedType: ExtendedType{ + Base: smartcontract.IntegerType, + Name: "qwer", + Interface: "qw", + Key: smartcontract.ArrayType, + Fields: []FieldExtendedType{ + { + Field: "as", + }, + }, + }, + }, + { + Field: "asf", + ExtendedType: ExtendedType{ + Base: smartcontract.BoolType, + }, + }, + { + Field: "sffg", + ExtendedType: ExtendedType{ + Base: smartcontract.AnyType, + }, + }, + }, + } + tcs := map[string]struct { + a *ExtendedType + b *ExtendedType + expectedRes bool + }{ + "both nil": { + a: nil, + b: nil, + expectedRes: true, + }, + "a is nil": { + a: nil, + b: &ExtendedType{}, + expectedRes: false, + }, + "b is nil": { + a: &ExtendedType{}, + b: nil, + expectedRes: false, + }, + "base mismatch": { + a: &ExtendedType{ + Base: smartcontract.StringType, + }, + b: &ExtendedType{ + Base: smartcontract.IntegerType, + }, + expectedRes: false, + }, + "name mismatch": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + Name: "q", + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + Name: "w", + }, + expectedRes: false, + }, + "number of fields mismatch": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + Name: "q", + Fields: []FieldExtendedType{ + { + Field: "IntField", + ExtendedType: ExtendedType{Base: smartcontract.IntegerType}, + }, + }, + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + Name: "w", + Fields: []FieldExtendedType{ + { + Field: "IntField", + ExtendedType: ExtendedType{Base: smartcontract.IntegerType}, + }, + { + Field: "BoolField", + ExtendedType: ExtendedType{Base: smartcontract.BoolType}, + }, + }, + }, + expectedRes: false, + }, + "field names mismatch": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + Fields: []FieldExtendedType{ + { + Field: "IntField", + ExtendedType: ExtendedType{Base: smartcontract.IntegerType}, + }, + }, + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + Fields: []FieldExtendedType{ + { + Field: "BoolField", + ExtendedType: ExtendedType{Base: smartcontract.BoolType}, + }, + }, + }, + expectedRes: false, + }, + "field types mismatch": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + Fields: []FieldExtendedType{ + { + Field: "Field", + ExtendedType: ExtendedType{Base: smartcontract.IntegerType}, + }, + }, + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + Fields: []FieldExtendedType{ + { + Field: "Field", + ExtendedType: ExtendedType{Base: smartcontract.BoolType}, + }, + }, + }, + expectedRes: false, + }, + "interface mismatch": { + a: &ExtendedType{Interface: "iterator"}, + b: &ExtendedType{Interface: "unknown"}, + expectedRes: false, + }, + "value is nil": { + a: &ExtendedType{ + Base: smartcontract.StringType, + }, + b: &ExtendedType{ + Base: smartcontract.StringType, + }, + expectedRes: true, + }, + "a value is not nil": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + Value: &ExtendedType{}, + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + }, + expectedRes: false, + }, + "b value is not nil": { + a: &ExtendedType{ + Base: smartcontract.ArrayType, + }, + b: &ExtendedType{ + Base: smartcontract.ArrayType, + Value: &ExtendedType{}, + }, + expectedRes: false, + }, + "byte array tolerance for a": { + a: &ExtendedType{ + Base: smartcontract.StringType, + }, + b: &ExtendedType{ + Base: smartcontract.ByteArrayType, + }, + expectedRes: true, + }, + "byte array tolerance for b": { + a: &ExtendedType{ + Base: smartcontract.ByteArrayType, + }, + b: &ExtendedType{ + Base: smartcontract.StringType, + }, + expectedRes: true, + }, + "key mismatch": { + a: &ExtendedType{ + Key: smartcontract.StringType, + }, + b: &ExtendedType{ + Key: smartcontract.IntegerType, + }, + expectedRes: false, + }, + "good nested": { + a: &crazyT, + b: &crazyT, + expectedRes: true, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.expectedRes, tc.a.Equals(tc.b)) + }) + } +}