From 3dadaa9029296aefccc0f4b80437cd345452fefc Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 14 Sep 2023 14:57:44 +0300 Subject: [PATCH 01/20] Refine API of the testing framework These changes will allow testing providers to unify unit & integration tests. --- runtime/stdlib/contracts/test.cdc | 19 +- runtime/stdlib/test-framework.go | 5 +- runtime/stdlib/test_contract.go | 48 ++++ runtime/stdlib/test_emulatorbackend.go | 77 +++++- runtime/stdlib/test_test.go | 340 ++++++++++++++++++++----- 5 files changed, 399 insertions(+), 90 deletions(-) diff --git a/runtime/stdlib/contracts/test.cdc b/runtime/stdlib/contracts/test.cdc index b6041f7b9e..458e138db7 100644 --- a/runtime/stdlib/contracts/test.cdc +++ b/runtime/stdlib/contracts/test.cdc @@ -27,6 +27,12 @@ pub contract Test { return self.backend.createAccount() } + /// Returns the account for the given address. + /// + pub fun getAccount(_ address: Address): Account { + return self.backend.getAccount(address) + } + /// Add a transaction to the current block. /// pub fun addTransaction(_ tx: Transaction) { @@ -77,14 +83,12 @@ pub contract Test { /// pub fun deployContract( name: String, - code: String, - account: Account, + path: String, arguments: [AnyStruct] ): Error? { return self.backend.deployContract( name: name, - code: code, - account: account, + path: path, arguments: arguments ) } @@ -297,6 +301,10 @@ pub contract Test { /// pub fun createAccount(): Account + /// Returns the account for the given address. + /// + pub fun getAccount(_ address: Address): Account + /// Add a transaction to the current block. /// pub fun addTransaction(_ tx: Transaction) @@ -315,8 +323,7 @@ pub contract Test { /// pub fun deployContract( name: String, - code: String, - account: Account, + path: String, arguments: [AnyStruct] ): Error? diff --git a/runtime/stdlib/test-framework.go b/runtime/stdlib/test-framework.go index 3e8d0523c2..facb02145c 100644 --- a/runtime/stdlib/test-framework.go +++ b/runtime/stdlib/test-framework.go @@ -43,6 +43,8 @@ type Blockchain interface { CreateAccount() (*Account, error) + GetAccount(interpreter.AddressValue) (*Account, error) + AddTransaction( inter *interpreter.Interpreter, code string, @@ -58,8 +60,7 @@ type Blockchain interface { DeployContract( inter *interpreter.Interpreter, name string, - code string, - account *Account, + path string, arguments []interpreter.Value, ) error diff --git a/runtime/stdlib/test_contract.go b/runtime/stdlib/test_contract.go index 2f3baf41e4..f4faa2b7c0 100644 --- a/runtime/stdlib/test_contract.go +++ b/runtime/stdlib/test_contract.go @@ -48,6 +48,14 @@ type TestContractType struct { expectFailureFunction interpreter.FunctionValue } +// 'Test.blockchain' public field + +const testTypeBlockchainFieldName = "blockchain" + +const testTypeBlockchainFieldDocString = ` +An emulator-backed blockchain. +` + // 'Test.assert' function const testTypeAssertFunctionDocString = ` @@ -1072,6 +1080,16 @@ func newTestContractType() *TestContractType { blockchainType := ty.blockchainType() + compositeType.Fields = []string{testTypeBlockchainFieldName} + blockchainField := sema.NewPublicConstantFieldMember( + nil, + compositeType, + testTypeBlockchainFieldName, + blockchainType, + testTypeBlockchainFieldDocString, + ) + compositeType.Members.Set(testTypeBlockchainFieldName, blockchainField) + // Test.assert() compositeType.Members.Set( testTypeAssertFunctionName, @@ -1368,5 +1386,35 @@ func (t *TestContractType) NewTestContract( compositeValue.Functions[testTypeBeLessThanFunctionName] = t.beLessThanFunction compositeValue.Functions[testExpectFailureFunctionName] = t.expectFailureFunction + // Create an `EmulatorBackend` + emulatorBackend := t.emulatorBackendType.newEmulatorBackend( + inter, + testFramework.NewEmulatorBackend(), + interpreter.EmptyLocationRange, + ) + + // Create a 'Blockchain' struct value, that wraps the emulator backend, + // by calling the constructor of 'Blockchain'. + blockchainConstructor := getNestedTypeConstructorValue( + compositeValue, + testBlockchainTypeName, + ) + blockchain, err := inter.InvokeExternally( + blockchainConstructor, + blockchainConstructor.Type, + []interpreter.Value{ + emulatorBackend, + }, + ) + if err != nil { + return nil, err + } + compositeValue.SetMember( + inter, + interpreter.EmptyLocationRange, + testTypeBlockchainFieldName, + blockchain, + ) + return compositeValue, nil } diff --git a/runtime/stdlib/test_emulatorbackend.go b/runtime/stdlib/test_emulatorbackend.go index aca8706e56..876e6c6858 100644 --- a/runtime/stdlib/test_emulatorbackend.go +++ b/runtime/stdlib/test_emulatorbackend.go @@ -20,6 +20,8 @@ package stdlib import ( + "fmt" + "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/interpreter" @@ -49,6 +51,7 @@ type testEmulatorBackendType struct { moveTimeFunctionType *sema.FunctionType createSnapshotFunctionType *sema.FunctionType loadSnapshotFunctionType *sema.FunctionType + getAccountFunctionType *sema.FunctionType } func newTestEmulatorBackendType( @@ -124,6 +127,11 @@ func newTestEmulatorBackendType( testEmulatorBackendTypeLoadSnapshotFunctionName, ) + getAccountFunctionType := interfaceFunctionType( + blockchainBackendInterfaceType, + testEmulatorBackendTypeGetAccountFunctionName, + ) + compositeType := &sema.CompositeType{ Identifier: testEmulatorBackendTypeName, Kind: common.CompositeKindStructure, @@ -218,6 +226,12 @@ func newTestEmulatorBackendType( loadSnapshotFunctionType, testEmulatorBackendTypeLoadSnapshotFunctionDocString, ), + sema.NewUnmeteredPublicFunctionMember( + compositeType, + testEmulatorBackendTypeGetAccountFunctionName, + getAccountFunctionType, + testEmulatorBackendTypeGetAccountFunctionDocString, + ), } compositeType.Members = sema.MembersAsMap(members) @@ -239,6 +253,7 @@ func newTestEmulatorBackendType( moveTimeFunctionType: moveTimeFunctionType, createSnapshotFunctionType: createSnapshotFunctionType, loadSnapshotFunctionType: loadSnapshotFunctionType, + getAccountFunctionType: getAccountFunctionType, } } @@ -348,6 +363,47 @@ func newTestAccountValue( return accountValue } +// 'EmulatorBackend.getAccount' function + +const testEmulatorBackendTypeGetAccountFunctionName = "getAccount" + +const testEmulatorBackendTypeGetAccountFunctionDocString = ` +Returns the account for the given address. +` + +func (t *testEmulatorBackendType) newGetAccountFunction( + blockchain Blockchain, +) *interpreter.HostFunctionValue { + return interpreter.NewUnmeteredHostFunctionValue( + t.getAccountFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + address, ok := invocation.Arguments[0].(interpreter.AddressValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + account, err := blockchain.GetAccount(address) + if err != nil { + msg := fmt.Sprintf("account with address: %s was not found", address) + panic(PanicError{ + Message: msg, + LocationRange: invocation.LocationRange, + }) + } + + inter := invocation.Interpreter + locationRange := invocation.LocationRange + + return newTestAccountValue( + blockchain, + inter, + locationRange, + account, + ) + }, + ) +} + // 'EmulatorBackend.addTransaction' function const testEmulatorBackendTypeAddTransactionFunctionName = "addTransaction" @@ -509,22 +565,14 @@ func (t *testEmulatorBackendType) newDeployContractFunction( panic(errors.NewUnreachableError()) } - // Contract code - code, ok := invocation.Arguments[1].(*interpreter.StringValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - // authorizer - accountValue, ok := invocation.Arguments[2].(interpreter.MemberAccessibleValue) + // Contract file path + path, ok := invocation.Arguments[1].(*interpreter.StringValue) if !ok { panic(errors.NewUnreachableError()) } - account := accountFromValue(inter, accountValue, invocation.LocationRange) - // Contract init arguments - args, err := arrayValueToSlice(inter, invocation.Arguments[3]) + args, err := arrayValueToSlice(inter, invocation.Arguments[2]) if err != nil { panic(err) } @@ -532,8 +580,7 @@ func (t *testEmulatorBackendType) newDeployContractFunction( err = blockchain.DeployContract( inter, name.Str, - code.Str, - account, + path.Str, args, ) @@ -878,6 +925,10 @@ func (t *testEmulatorBackendType) newEmulatorBackend( Name: testEmulatorBackendTypeLoadSnapshotFunctionName, Value: t.newLoadSnapshotFunction(blockchain), }, + { + Name: testEmulatorBackendTypeGetAccountFunctionName, + Value: t.newGetAccountFunction(blockchain), + }, } // TODO: Use SimpleCompositeValue diff --git a/runtime/stdlib/test_test.go b/runtime/stdlib/test_test.go index beaa3a2f68..17a3eb95bd 100644 --- a/runtime/stdlib/test_test.go +++ b/runtime/stdlib/test_test.go @@ -16,7 +16,8 @@ * limitations under the License. */ -package stdlib +// This is in order to avoid cyclic import errors with runtime package +package stdlib_test import ( "errors" @@ -26,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/activations" "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" @@ -33,18 +35,24 @@ import ( "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/runtime/stdlib" "github.com/onflow/cadence/runtime/tests/checker" "github.com/onflow/cadence/runtime/tests/utils" ) func newTestContractInterpreter(t *testing.T, code string) (*interpreter.Interpreter, error) { - return newTestContractInterpreterWithTestFramework(t, code, nil) + testFramework := &mockedTestFramework{ + newEmulatorBackend: func() stdlib.Blockchain { + return &mockedBlockchain{} + }, + } + return newTestContractInterpreterWithTestFramework(t, code, testFramework) } func newTestContractInterpreterWithTestFramework( t *testing.T, code string, - testFramework TestFramework, + testFramework stdlib.TestFramework, ) (*interpreter.Interpreter, error) { program, err := parser.ParseProgram( nil, @@ -54,8 +62,8 @@ func newTestContractInterpreterWithTestFramework( require.NoError(t, err) activation := sema.NewVariableActivation(sema.BaseValueActivation) - activation.DeclareValue(AssertFunction) - activation.DeclareValue(PanicFunction) + activation.DeclareValue(stdlib.AssertFunction) + activation.DeclareValue(stdlib.PanicFunction) checker, err := sema.NewChecker( program, @@ -72,15 +80,15 @@ func newTestContractInterpreterWithTestFramework( sema.Import, error, ) { - if importedLocation == TestContractLocation { + if importedLocation == stdlib.TestContractLocation { return sema.ElaborationImport{ - Elaboration: GetTestContractType().Checker.Elaboration, + Elaboration: stdlib.GetTestContractType().Checker.Elaboration, }, nil } return nil, errors.New("invalid import") }, - ContractValueHandler: TestCheckerContractValueHandler, + ContractValueHandler: stdlib.TestCheckerContractValueHandler, }, ) require.NoError(t, err) @@ -90,13 +98,13 @@ func newTestContractInterpreterWithTestFramework( return nil, err } - storage := newUnmeteredInMemoryStorage() + storage := interpreter.NewInMemoryStorage(nil) var uuid uint64 = 0 baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) - interpreter.Declare(baseActivation, AssertFunction) - interpreter.Declare(baseActivation, PanicFunction) + interpreter.Declare(baseActivation, stdlib.AssertFunction) + interpreter.Declare(baseActivation, stdlib.PanicFunction) inter, err := interpreter.NewInterpreter( interpreter.ProgramFromChecker(checker), @@ -105,8 +113,8 @@ func newTestContractInterpreterWithTestFramework( Storage: storage, BaseActivation: baseActivation, ImportLocationHandler: func(inter *interpreter.Interpreter, location common.Location) interpreter.Import { - if location == TestContractLocation { - program := interpreter.ProgramFromChecker(GetTestContractType().Checker) + if location == stdlib.TestContractLocation { + program := interpreter.ProgramFromChecker(stdlib.GetTestContractType().Checker) subInterpreter, err := inter.NewSubInterpreter(program, location) if err != nil { panic(err) @@ -118,7 +126,7 @@ func newTestContractInterpreterWithTestFramework( return nil }, - ContractValueHandler: NewTestInterpreterContractValueHandler(testFramework), + ContractValueHandler: stdlib.NewTestInterpreterContractValueHandler(testFramework), UUIDHandler: func() (uint64, error) { uuid++ return uuid, nil @@ -690,7 +698,7 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("test") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, @@ -714,7 +722,7 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("test") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, @@ -749,7 +757,7 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("testNotEqual") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, @@ -792,7 +800,7 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("testNotEqual") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, @@ -827,7 +835,7 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("testNotEqual") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, @@ -862,11 +870,11 @@ func TestAssertEqual(t *testing.T) { _, err = inter.Invoke("testNotEqual") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) assert.ErrorContains( t, err, - "not equal: expected: {2: false, 1: true}, actual: {2: true, 1: true}", + "not equal: expected: {2: false, 1: true}, actual: {1: true, 2: true}", ) }) @@ -1721,7 +1729,7 @@ func TestTestExpect(t *testing.T) { _, err = inter.Invoke("test") require.Error(t, err) - assertionErr := &AssertionError{} + assertionErr := &stdlib.AssertionError{} assert.ErrorAs(t, err, assertionErr) assert.Equal(t, "given value is: \"this string\"", assertionErr.Message) assert.Equal(t, "test", assertionErr.LocationRange.Location.String()) @@ -1744,7 +1752,7 @@ func TestTestExpect(t *testing.T) { _, err = inter.Invoke("test") require.Error(t, err) - assert.ErrorAs(t, err, &AssertionError{}) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) }) t.Run("with explicit types", func(t *testing.T) { @@ -2053,7 +2061,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain let events = blockchain.events() Test.expect(events, Test.beEmpty()) @@ -2063,7 +2071,7 @@ func TestBlockchain(t *testing.T) { eventsInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ events: func(inter *interpreter.Interpreter, eventType interpreter.StaticType) interpreter.Value { eventsInvoked = true @@ -2097,7 +2105,7 @@ func TestBlockchain(t *testing.T) { pub struct Foo {} pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain // 'Foo' is not an event-type. // But we just need to test the API, so it doesn't really matter. @@ -2112,7 +2120,7 @@ func TestBlockchain(t *testing.T) { eventsInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ events: func(inter *interpreter.Interpreter, eventType interpreter.StaticType) interpreter.Value { eventsInvoked = true @@ -2149,7 +2157,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.reset(to: 5) } ` @@ -2157,7 +2165,7 @@ func TestBlockchain(t *testing.T) { resetInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ reset: func(height uint64) { resetInvoked = true @@ -2183,7 +2191,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.reset(to: 5.5) } ` @@ -2191,7 +2199,7 @@ func TestBlockchain(t *testing.T) { resetInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ reset: func(height uint64) { resetInvoked = true @@ -2213,7 +2221,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveForward() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain // timeDelta is the representation of 35 days, // in the form of seconds. let timeDelta = Fix64(35 * 24 * 60 * 60) @@ -2224,7 +2232,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2250,7 +2258,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveBackward() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain // timeDelta is the representation of 35 days, // in the form of seconds. let timeDelta = Fix64(35 * 24 * 60 * 60) * -1.0 @@ -2261,7 +2269,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2287,7 +2295,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveTime() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.moveTime(by: 3000) } ` @@ -2295,7 +2303,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2310,14 +2318,14 @@ func TestBlockchain(t *testing.T) { assert.False(t, moveTimeInvoked) }) - t.Run("newEmulatorBackend", func(t *testing.T) { + t.Run("blockchain", func(t *testing.T) { t.Parallel() const script = ` import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain Test.assertEqual(Type(), blockchain.getType()) } ` @@ -2325,7 +2333,7 @@ func TestBlockchain(t *testing.T) { newEmulatorBackendInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { newEmulatorBackendInvoked = true return &mockedBlockchain{} }, @@ -2347,7 +2355,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.createSnapshot(name: "adminCreated") } ` @@ -2355,7 +2363,7 @@ func TestBlockchain(t *testing.T) { createSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { createSnapshotInvoked = true @@ -2383,7 +2391,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.createSnapshot(name: "adminCreated") } ` @@ -2391,7 +2399,7 @@ func TestBlockchain(t *testing.T) { createSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { createSnapshotInvoked = true @@ -2419,7 +2427,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.createSnapshot(name: "adminCreated") blockchain.loadSnapshot(name: "adminCreated") } @@ -2428,7 +2436,7 @@ func TestBlockchain(t *testing.T) { loadSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { assert.Equal(t, "adminCreated", name) @@ -2461,7 +2469,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.newEmulatorBlockchain() + let blockchain = Test.blockchain blockchain.createSnapshot(name: "adminCreated") blockchain.loadSnapshot(name: "contractDeployed") } @@ -2470,7 +2478,7 @@ func TestBlockchain(t *testing.T) { loadSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() Blockchain { + newEmulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { assert.Equal(t, "adminCreated", name) @@ -2496,17 +2504,203 @@ func TestBlockchain(t *testing.T) { assert.True(t, loadSnapshotInvoked) }) + t.Run("deployContract", func(t *testing.T) { + t.Parallel() + + const script = ` + import Test + + pub fun test() { + let blockchain = Test.blockchain + let err = blockchain.deployContract( + name: "FooContract", + path: "./contracts/FooContract.cdc", + arguments: ["Hey, there!"] + ) + + Test.expect(err, Test.beNil()) + } + ` + + deployContractInvoked := false + + testFramework := &mockedTestFramework{ + newEmulatorBackend: func() stdlib.Blockchain { + return &mockedBlockchain{ + deployContract: func( + inter *interpreter.Interpreter, + name string, + path string, + arguments []interpreter.Value, + ) error { + deployContractInvoked = true + assert.Equal(t, "FooContract", name) + assert.Equal(t, "./contracts/FooContract.cdc", path) + assert.Equal(t, 1, len(arguments)) + argument := arguments[0].(*interpreter.StringValue) + assert.Equal(t, "Hey, there!", argument.Str) + + return nil + }, + } + }, + } + + inter, err := newTestContractInterpreterWithTestFramework(t, script, testFramework) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + assert.True(t, deployContractInvoked) + }) + + t.Run("deployContract with failure", func(t *testing.T) { + t.Parallel() + + const script = ` + import Test + + pub fun test() { + let blockchain = Test.blockchain + let err = blockchain.deployContract( + name: "FooContract", + path: "./contracts/FooContract.cdc", + arguments: ["Hey, there!"] + ) + + Test.assertEqual( + "failed to deploy contract: FooContract", + err!.message + ) + } + ` + + deployContractInvoked := false + + testFramework := &mockedTestFramework{ + newEmulatorBackend: func() stdlib.Blockchain { + return &mockedBlockchain{ + deployContract: func( + inter *interpreter.Interpreter, + name string, + path string, + arguments []interpreter.Value, + ) error { + deployContractInvoked = true + + return fmt.Errorf("failed to deploy contract: %s", name) + }, + } + }, + } + + inter, err := newTestContractInterpreterWithTestFramework(t, script, testFramework) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + assert.True(t, deployContractInvoked) + }) + + t.Run("getAccount", func(t *testing.T) { + t.Parallel() + + const script = ` + import Test + + pub fun test() { + let blockchain = Test.blockchain + let account = blockchain.getAccount(0x0000000000000009) + } + ` + + getAccountInvoked := false + + testFramework := &mockedTestFramework{ + newEmulatorBackend: func() stdlib.Blockchain { + return &mockedBlockchain{ + getAccount: func(address interpreter.AddressValue) (*stdlib.Account, error) { + getAccountInvoked = true + assert.Equal(t, "0000000000000009", address.Hex()) + addr := common.Address(address) + + return &stdlib.Account{ + Address: addr, + PublicKey: &stdlib.PublicKey{ + PublicKey: []byte{1, 2, 3}, + SignAlgo: sema.SignatureAlgorithmECDSA_P256, + }, + }, nil + }, + stdlibHandler: func() stdlib.StandardLibraryHandler { + return runtime.NewBaseInterpreterEnvironment(runtime.Config{}) + }, + } + }, + } + + inter, err := newTestContractInterpreterWithTestFramework(t, script, testFramework) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + assert.True(t, getAccountInvoked) + }) + + t.Run("getAccount with failure", func(t *testing.T) { + t.Parallel() + + const script = ` + import Test + + pub fun test() { + let blockchain = Test.blockchain + let account = blockchain.getAccount(0x0000000000000009) + } + ` + + getAccountInvoked := false + + testFramework := &mockedTestFramework{ + newEmulatorBackend: func() stdlib.Blockchain { + return &mockedBlockchain{ + getAccount: func(address interpreter.AddressValue) (*stdlib.Account, error) { + getAccountInvoked = true + assert.Equal(t, "0000000000000009", address.Hex()) + + return nil, fmt.Errorf("failed to retrieve account with address: %s", address) + }, + stdlibHandler: func() stdlib.StandardLibraryHandler { + return runtime.NewBaseInterpreterEnvironment(runtime.Config{}) + }, + } + }, + } + + inter, err := newTestContractInterpreterWithTestFramework(t, script, testFramework) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.Error(t, err) + assert.ErrorContains(t, err, "account with address: 0x0000000000000009 was not found") + + assert.True(t, getAccountInvoked) + }) + // TODO: Add more tests for the remaining functions. } type mockedTestFramework struct { - newEmulatorBackend func() Blockchain + newEmulatorBackend func() stdlib.Blockchain readFile func(s string) (string, error) } -var _ TestFramework = &mockedTestFramework{} +var _ stdlib.TestFramework = &mockedTestFramework{} -func (m mockedTestFramework) NewEmulatorBackend() Blockchain { +func (m mockedTestFramework) NewEmulatorBackend() stdlib.Blockchain { if m.newEmulatorBackend == nil { panic("'NewEmulatorBackend' is not implemented") } @@ -2524,15 +2718,16 @@ func (m mockedTestFramework) ReadFile(fileName string) (string, error) { type mockedBlockchain struct { runScript func(inter *interpreter.Interpreter, code string, arguments []interpreter.Value) - createAccount func() (*Account, error) - addTransaction func(inter *interpreter.Interpreter, code string, authorizers []common.Address, signers []*Account, arguments []interpreter.Value) error - executeTransaction func() *TransactionResult + createAccount func() (*stdlib.Account, error) + getAccount func(interpreter.AddressValue) (*stdlib.Account, error) + addTransaction func(inter *interpreter.Interpreter, code string, authorizers []common.Address, signers []*stdlib.Account, arguments []interpreter.Value) error + executeTransaction func() *stdlib.TransactionResult commitBlock func() error - deployContract func(inter *interpreter.Interpreter, name string, code string, account *Account, arguments []interpreter.Value) error - useConfiguration func(configuration *Configuration) - stdlibHandler func() StandardLibraryHandler + deployContract func(inter *interpreter.Interpreter, name string, path string, arguments []interpreter.Value) error + useConfiguration func(configuration *stdlib.Configuration) + stdlibHandler func() stdlib.StandardLibraryHandler logs func() []string - serviceAccount func() (*Account, error) + serviceAccount func() (*stdlib.Account, error) events func(inter *interpreter.Interpreter, eventType interpreter.StaticType) interpreter.Value reset func(uint64) moveTime func(int64) @@ -2540,13 +2735,13 @@ type mockedBlockchain struct { loadSnapshot func(string) error } -var _ Blockchain = &mockedBlockchain{} +var _ stdlib.Blockchain = &mockedBlockchain{} func (m mockedBlockchain) RunScript( inter *interpreter.Interpreter, code string, arguments []interpreter.Value, -) *ScriptResult { +) *stdlib.ScriptResult { if m.runScript == nil { panic("'RunScript' is not implemented") } @@ -2554,7 +2749,7 @@ func (m mockedBlockchain) RunScript( return m.RunScript(inter, code, arguments) } -func (m mockedBlockchain) CreateAccount() (*Account, error) { +func (m mockedBlockchain) CreateAccount() (*stdlib.Account, error) { if m.createAccount == nil { panic("'CreateAccount' is not implemented") } @@ -2562,11 +2757,19 @@ func (m mockedBlockchain) CreateAccount() (*Account, error) { return m.createAccount() } +func (m mockedBlockchain) GetAccount(address interpreter.AddressValue) (*stdlib.Account, error) { + if m.getAccount == nil { + panic("'getAccount' is not implemented") + } + + return m.getAccount(address) +} + func (m mockedBlockchain) AddTransaction( inter *interpreter.Interpreter, code string, authorizers []common.Address, - signers []*Account, + signers []*stdlib.Account, arguments []interpreter.Value, ) error { if m.addTransaction == nil { @@ -2576,7 +2779,7 @@ func (m mockedBlockchain) AddTransaction( return m.addTransaction(inter, code, authorizers, signers, arguments) } -func (m mockedBlockchain) ExecuteNextTransaction() *TransactionResult { +func (m mockedBlockchain) ExecuteNextTransaction() *stdlib.TransactionResult { if m.executeTransaction == nil { panic("'ExecuteNextTransaction' is not implemented") } @@ -2595,18 +2798,17 @@ func (m mockedBlockchain) CommitBlock() error { func (m mockedBlockchain) DeployContract( inter *interpreter.Interpreter, name string, - code string, - account *Account, + path string, arguments []interpreter.Value, ) error { if m.deployContract == nil { panic("'DeployContract' is not implemented") } - return m.deployContract(inter, name, code, account, arguments) + return m.deployContract(inter, name, path, arguments) } -func (m mockedBlockchain) UseConfiguration(configuration *Configuration) { +func (m mockedBlockchain) UseConfiguration(configuration *stdlib.Configuration) { if m.useConfiguration == nil { panic("'UseConfiguration' is not implemented") } @@ -2614,7 +2816,7 @@ func (m mockedBlockchain) UseConfiguration(configuration *Configuration) { m.useConfiguration(configuration) } -func (m mockedBlockchain) StandardLibraryHandler() StandardLibraryHandler { +func (m mockedBlockchain) StandardLibraryHandler() stdlib.StandardLibraryHandler { if m.stdlibHandler == nil { panic("'StandardLibraryHandler' is not implemented") } @@ -2630,7 +2832,7 @@ func (m mockedBlockchain) Logs() []string { return m.logs() } -func (m mockedBlockchain) ServiceAccount() (*Account, error) { +func (m mockedBlockchain) ServiceAccount() (*stdlib.Account, error) { if m.serviceAccount == nil { panic("'ServiceAccount' is not implemented") } From 24d9c4bca06d59a16bf1b0cdef2d87a3ca01e21f Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 15 Sep 2023 14:54:35 +0300 Subject: [PATCH 02/20] Remove entirely the 'Test.Blockchain' struct --- runtime/stdlib/contracts/test.cdc | 263 +++++++++++++++--------------- runtime/stdlib/test_contract.go | 175 +++----------------- runtime/stdlib/test_test.go | 83 +++------- 3 files changed, 168 insertions(+), 353 deletions(-) diff --git a/runtime/stdlib/contracts/test.cdc b/runtime/stdlib/contracts/test.cdc index 458e138db7..04fe12aa36 100644 --- a/runtime/stdlib/contracts/test.cdc +++ b/runtime/stdlib/contracts/test.cdc @@ -2,164 +2,161 @@ /// pub contract Test { - /// Blockchain emulates a real network. + /// backend emulates a real network. /// - pub struct Blockchain { + pub let backend: AnyStruct{BlockchainBackend} - pub let backend: AnyStruct{BlockchainBackend} + init(backend: AnyStruct{BlockchainBackend}) { + self.backend = backend + } - init(backend: AnyStruct{BlockchainBackend}) { - self.backend = backend - } + /// Executes a script and returns the script return value and the status. + /// `returnValue` field of the result will be `nil` if the script failed. + /// + pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult { + return self.backend.executeScript(script, arguments) + } - /// Executes a script and returns the script return value and the status. - /// `returnValue` field of the result will be `nil` if the script failed. - /// - pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult { - return self.backend.executeScript(script, arguments) - } + /// Creates a signer account by submitting an account creation transaction. + /// The transaction is paid by the service account. + /// The returned account can be used to sign and authorize transactions. + /// + pub fun createAccount(): Account { + return self.backend.createAccount() + } - /// Creates a signer account by submitting an account creation transaction. - /// The transaction is paid by the service account. - /// The returned account can be used to sign and authorize transactions. - /// - pub fun createAccount(): Account { - return self.backend.createAccount() - } + /// Returns the account for the given address. + /// + pub fun getAccount(_ address: Address): Account { + return self.backend.getAccount(address) + } - /// Returns the account for the given address. - /// - pub fun getAccount(_ address: Address): Account { - return self.backend.getAccount(address) - } + /// Add a transaction to the current block. + /// + pub fun addTransaction(_ tx: Transaction) { + self.backend.addTransaction(tx) + } - /// Add a transaction to the current block. - /// - pub fun addTransaction(_ tx: Transaction) { - self.backend.addTransaction(tx) - } + /// Executes the next transaction in the block, if any. + /// Returns the result of the transaction, or nil if no transaction was scheduled. + /// + pub fun executeNextTransaction(): TransactionResult? { + return self.backend.executeNextTransaction() + } - /// Executes the next transaction in the block, if any. - /// Returns the result of the transaction, or nil if no transaction was scheduled. - /// - pub fun executeNextTransaction(): TransactionResult? { - return self.backend.executeNextTransaction() - } + /// Commit the current block. + /// Committing will fail if there are un-executed transactions in the block. + /// + pub fun commitBlock() { + self.backend.commitBlock() + } - /// Commit the current block. - /// Committing will fail if there are un-executed transactions in the block. - /// - pub fun commitBlock() { - self.backend.commitBlock() - } + /// Executes a given transaction and commit the current block. + /// + pub fun executeTransaction(_ tx: Transaction): TransactionResult { + self.addTransaction(tx) + let txResult = self.executeNextTransaction()! + self.commitBlock() + return txResult + } - /// Executes a given transaction and commit the current block. - /// - pub fun executeTransaction(_ tx: Transaction): TransactionResult { + /// Executes a given set of transactions and commit the current block. + /// + pub fun executeTransactions(_ transactions: [Transaction]): [TransactionResult] { + for tx in transactions { self.addTransaction(tx) - let txResult = self.executeNextTransaction()! - self.commitBlock() - return txResult } - /// Executes a given set of transactions and commit the current block. - /// - pub fun executeTransactions(_ transactions: [Transaction]): [TransactionResult] { - for tx in transactions { - self.addTransaction(tx) - } - - var results: [TransactionResult] = [] - for tx in transactions { - let txResult = self.executeNextTransaction()! - results.append(txResult) - } - - self.commitBlock() - return results + var results: [TransactionResult] = [] + for tx in transactions { + let txResult = self.executeNextTransaction()! + results.append(txResult) } - /// Deploys a given contract, and initilizes it with the arguments. - /// - pub fun deployContract( - name: String, - path: String, - arguments: [AnyStruct] - ): Error? { - return self.backend.deployContract( - name: name, - path: path, - arguments: arguments - ) - } + self.commitBlock() + return results + } - /// Set the configuration to be used by the blockchain. - /// Overrides any existing configuration. - /// - pub fun useConfiguration(_ configuration: Configuration) { - self.backend.useConfiguration(configuration) - } + /// Deploys a given contract, and initilizes it with the arguments. + /// + pub fun deployContract( + name: String, + path: String, + arguments: [AnyStruct] + ): Error? { + return self.backend.deployContract( + name: name, + path: path, + arguments: arguments + ) + } - /// Returns all the logs from the blockchain, up to the calling point. - /// - pub fun logs(): [String] { - return self.backend.logs() - } + /// Set the configuration to be used by the blockchain. + /// Overrides any existing configuration. + /// + pub fun useConfiguration(_ configuration: Configuration) { + self.backend.useConfiguration(configuration) + } - /// Returns the service account of the blockchain. Can be used to sign - /// transactions with this account. - /// - pub fun serviceAccount(): Account { - return self.backend.serviceAccount() - } + /// Returns all the logs from the blockchain, up to the calling point. + /// + pub fun logs(): [String] { + return self.backend.logs() + } - /// Returns all events emitted from the blockchain. - /// - pub fun events(): [AnyStruct] { - return self.backend.events(nil) - } + /// Returns the service account of the blockchain. Can be used to sign + /// transactions with this account. + /// + pub fun serviceAccount(): Account { + return self.backend.serviceAccount() + } - /// Returns all events emitted from the blockchain, - /// filtered by type. - /// - pub fun eventsOfType(_ type: Type): [AnyStruct] { - return self.backend.events(type) - } + /// Returns all events emitted from the blockchain. + /// + pub fun events(): [AnyStruct] { + return self.backend.events(nil) + } - /// Resets the state of the blockchain to the given height. - /// - pub fun reset(to height: UInt64) { - self.backend.reset(to: height) - } + /// Returns all events emitted from the blockchain, + /// filtered by type. + /// + pub fun eventsOfType(_ type: Type): [AnyStruct] { + return self.backend.events(type) + } - /// Moves the time of the blockchain by the given delta, - /// which should be passed in the form of seconds. - /// - pub fun moveTime(by delta: Fix64) { - self.backend.moveTime(by: delta) - } + /// Resets the state of the blockchain to the given height. + /// + pub fun reset(to height: UInt64) { + self.backend.reset(to: height) + } - /// Creates a snapshot of the blockchain, at the - /// current ledger state, with the given name. - /// - access(all) - fun createSnapshot(name: String) { - let err = self.backend.createSnapshot(name: name) - if err != nil { - panic(err!.message) - } + /// Moves the time of the blockchain by the given delta, + /// which should be passed in the form of seconds. + /// + pub fun moveTime(by delta: Fix64) { + self.backend.moveTime(by: delta) + } + + /// Creates a snapshot of the blockchain, at the + /// current ledger state, with the given name. + /// + access(all) + fun createSnapshot(name: String) { + let err = self.backend.createSnapshot(name: name) + if err != nil { + panic(err!.message) } + } - /// Loads a snapshot of the blockchain, with the - /// given name, and updates the current ledger - /// state. - /// - access(all) - fun loadSnapshot(name: String) { - let err = self.backend.loadSnapshot(name: name) - if err != nil { - panic(err!.message) - } + /// Loads a snapshot of the blockchain, with the + /// given name, and updates the current ledger + /// state. + /// + access(all) + fun loadSnapshot(name: String) { + let err = self.backend.loadSnapshot(name: name) + if err != nil { + panic(err!.message) } } diff --git a/runtime/stdlib/test_contract.go b/runtime/stdlib/test_contract.go index f4faa2b7c0..0cd3db214c 100644 --- a/runtime/stdlib/test_contract.go +++ b/runtime/stdlib/test_contract.go @@ -32,30 +32,21 @@ import ( ) type TestContractType struct { - Checker *sema.Checker - CompositeType *sema.CompositeType - InitializerTypes []sema.Type - emulatorBackendType *testEmulatorBackendType - newEmulatorBlockchainFunctionType *sema.FunctionType - expectFunction interpreter.FunctionValue - newMatcherFunction interpreter.FunctionValue - haveElementCountFunction interpreter.FunctionValue - beEmptyFunction interpreter.FunctionValue - equalFunction interpreter.FunctionValue - beGreaterThanFunction interpreter.FunctionValue - containFunction interpreter.FunctionValue - beLessThanFunction interpreter.FunctionValue - expectFailureFunction interpreter.FunctionValue + Checker *sema.Checker + CompositeType *sema.CompositeType + InitializerTypes []sema.Type + emulatorBackendType *testEmulatorBackendType + expectFunction interpreter.FunctionValue + newMatcherFunction interpreter.FunctionValue + haveElementCountFunction interpreter.FunctionValue + beEmptyFunction interpreter.FunctionValue + equalFunction interpreter.FunctionValue + beGreaterThanFunction interpreter.FunctionValue + containFunction interpreter.FunctionValue + beLessThanFunction interpreter.FunctionValue + expectFailureFunction interpreter.FunctionValue } -// 'Test.blockchain' public field - -const testTypeBlockchainFieldName = "blockchain" - -const testTypeBlockchainFieldDocString = ` -An emulator-backed blockchain. -` - // 'Test.assert' function const testTypeAssertFunctionDocString = ` @@ -388,65 +379,6 @@ func newTestTypeReadFileFunction(testFramework TestFramework) *interpreter.HostF ) } -// 'Test.newEmulatorBlockchain' function - -const testTypeNewEmulatorBlockchainFunctionDocString = ` -Creates a blockchain which is backed by a new emulator instance. -` - -const testTypeNewEmulatorBlockchainFunctionName = "newEmulatorBlockchain" - -const testBlockchainTypeName = "Blockchain" - -func newTestTypeNewEmulatorBlockchainFunctionType(blockchainType *sema.CompositeType) *sema.FunctionType { - return &sema.FunctionType{ - ReturnTypeAnnotation: sema.NewTypeAnnotation( - blockchainType, - ), - } -} - -func (t *TestContractType) newNewEmulatorBlockchainFunction( - testFramework TestFramework, -) *interpreter.HostFunctionValue { - return interpreter.NewUnmeteredHostFunctionValue( - t.newEmulatorBlockchainFunctionType, - func(invocation interpreter.Invocation) interpreter.Value { - inter := invocation.Interpreter - locationRange := invocation.LocationRange - - // Create an `EmulatorBackend` - emulatorBackend := t.emulatorBackendType.newEmulatorBackend( - inter, - testFramework.NewEmulatorBackend(), - locationRange, - ) - - // Create a 'Blockchain' struct value, that wraps the emulator backend, - // by calling the constructor of 'Blockchain'. - - blockchainConstructor := getNestedTypeConstructorValue( - *invocation.Self, - testBlockchainTypeName, - ) - - blockchain, err := inter.InvokeExternally( - blockchainConstructor, - blockchainConstructor.Type, - []interpreter.Value{ - emulatorBackend, - }, - ) - - if err != nil { - panic(err) - } - - return blockchain - }, - ) -} - // 'Test.NewMatcher' function. // Constructs a matcher that test only 'AnyStruct'. // Accepts test function that accepts subtype of 'AnyStruct'. @@ -1078,18 +1010,6 @@ func newTestContractType() *TestContractType { matcherType := ty.matcherType() matcherTestFunctionType := compositeFunctionType(matcherType, matcherTestFunctionName) - blockchainType := ty.blockchainType() - - compositeType.Fields = []string{testTypeBlockchainFieldName} - blockchainField := sema.NewPublicConstantFieldMember( - nil, - compositeType, - testTypeBlockchainFieldName, - blockchainType, - testTypeBlockchainFieldDocString, - ) - compositeType.Members.Set(testTypeBlockchainFieldName, blockchainField) - // Test.assert() compositeType.Members.Set( testTypeAssertFunctionName, @@ -1123,19 +1043,6 @@ func newTestContractType() *TestContractType { ), ) - // Test.newEmulatorBlockchain() - newEmulatorBlockchainFunctionType := newTestTypeNewEmulatorBlockchainFunctionType(blockchainType) - compositeType.Members.Set( - testTypeNewEmulatorBlockchainFunctionName, - sema.NewUnmeteredPublicFunctionMember( - compositeType, - testTypeNewEmulatorBlockchainFunctionName, - newEmulatorBlockchainFunctionType, - testTypeNewEmulatorBlockchainFunctionDocString, - ), - ) - ty.newEmulatorBlockchainFunctionType = newEmulatorBlockchainFunctionType - // Test.readFile() compositeType.Members.Set( testTypeReadFileFunctionName, @@ -1326,23 +1233,6 @@ func (t *TestContractType) matcherType() *sema.CompositeType { return matcherType } -func (t *TestContractType) blockchainType() *sema.CompositeType { - typ, ok := t.CompositeType.NestedTypes.Get(testBlockchainTypeName) - if !ok { - panic(typeNotFoundError(testContractTypeName, testBlockchainTypeName)) - } - - matcherType, ok := typ.(*sema.CompositeType) - if !ok || matcherType.Kind != common.CompositeKindStructure { - panic(errors.NewUnexpectedError( - "invalid type for '%s'. expected struct type", - testMatcherTypeName, - )) - } - - return matcherType -} - func (t *TestContractType) NewTestContract( inter *interpreter.Interpreter, testFramework TestFramework, @@ -1353,9 +1243,14 @@ func (t *TestContractType) NewTestContract( error, ) { initializerTypes := t.InitializerTypes + emulatorBackend := t.emulatorBackendType.newEmulatorBackend( + inter, + testFramework.NewEmulatorBackend(), + interpreter.EmptyLocationRange, + ) value, err := inter.InvokeFunctionValue( constructor, - nil, + []interpreter.Value{emulatorBackend}, initializerTypes, initializerTypes, invocationRange, @@ -1371,8 +1266,6 @@ func (t *TestContractType) NewTestContract( compositeValue.Functions[testTypeAssertEqualFunctionName] = testTypeAssertEqualFunction compositeValue.Functions[testTypeFailFunctionName] = testTypeFailFunction compositeValue.Functions[testTypeExpectFunctionName] = t.expectFunction - compositeValue.Functions[testTypeNewEmulatorBlockchainFunctionName] = - t.newNewEmulatorBlockchainFunction(testFramework) compositeValue.Functions[testTypeReadFileFunctionName] = newTestTypeReadFileFunction(testFramework) @@ -1386,35 +1279,5 @@ func (t *TestContractType) NewTestContract( compositeValue.Functions[testTypeBeLessThanFunctionName] = t.beLessThanFunction compositeValue.Functions[testExpectFailureFunctionName] = t.expectFailureFunction - // Create an `EmulatorBackend` - emulatorBackend := t.emulatorBackendType.newEmulatorBackend( - inter, - testFramework.NewEmulatorBackend(), - interpreter.EmptyLocationRange, - ) - - // Create a 'Blockchain' struct value, that wraps the emulator backend, - // by calling the constructor of 'Blockchain'. - blockchainConstructor := getNestedTypeConstructorValue( - compositeValue, - testBlockchainTypeName, - ) - blockchain, err := inter.InvokeExternally( - blockchainConstructor, - blockchainConstructor.Type, - []interpreter.Value{ - emulatorBackend, - }, - ) - if err != nil { - return nil, err - } - compositeValue.SetMember( - inter, - interpreter.EmptyLocationRange, - testTypeBlockchainFieldName, - blockchain, - ) - return compositeValue, nil } diff --git a/runtime/stdlib/test_test.go b/runtime/stdlib/test_test.go index 17a3eb95bd..a47bfbea94 100644 --- a/runtime/stdlib/test_test.go +++ b/runtime/stdlib/test_test.go @@ -874,7 +874,7 @@ func TestAssertEqual(t *testing.T) { assert.ErrorContains( t, err, - "not equal: expected: {2: false, 1: true}, actual: {1: true, 2: true}", + "not equal: expected: {1: true, 2: false}, actual: {2: true, 1: true}", ) }) @@ -2061,8 +2061,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - let events = blockchain.events() + let events = Test.events() Test.expect(events, Test.beEmpty()) } @@ -2105,13 +2104,11 @@ func TestBlockchain(t *testing.T) { pub struct Foo {} pub fun test() { - let blockchain = Test.blockchain - // 'Foo' is not an event-type. // But we just need to test the API, so it doesn't really matter. let typ = Type() - let events = blockchain.eventsOfType(typ) + let events = Test.eventsOfType(typ) Test.expect(events, Test.beEmpty()) } @@ -2157,8 +2154,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.reset(to: 5) + Test.reset(to: 5) } ` @@ -2191,8 +2187,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.reset(to: 5.5) + Test.reset(to: 5.5) } ` @@ -2221,11 +2216,10 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveForward() { - let blockchain = Test.blockchain // timeDelta is the representation of 35 days, // in the form of seconds. let timeDelta = Fix64(35 * 24 * 60 * 60) - blockchain.moveTime(by: timeDelta) + Test.moveTime(by: timeDelta) } ` @@ -2258,11 +2252,10 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveBackward() { - let blockchain = Test.blockchain // timeDelta is the representation of 35 days, // in the form of seconds. let timeDelta = Fix64(35 * 24 * 60 * 60) * -1.0 - blockchain.moveTime(by: timeDelta) + Test.moveTime(by: timeDelta) } ` @@ -2295,8 +2288,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun testMoveTime() { - let blockchain = Test.blockchain - blockchain.moveTime(by: 3000) + Test.moveTime(by: 3000) } ` @@ -2318,36 +2310,6 @@ func TestBlockchain(t *testing.T) { assert.False(t, moveTimeInvoked) }) - t.Run("blockchain", func(t *testing.T) { - t.Parallel() - - const script = ` - import Test - - pub fun test() { - let blockchain = Test.blockchain - Test.assertEqual(Type(), blockchain.getType()) - } - ` - - newEmulatorBackendInvoked := false - - testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { - newEmulatorBackendInvoked = true - return &mockedBlockchain{} - }, - } - - inter, err := newTestContractInterpreterWithTestFramework(t, script, testFramework) - require.NoError(t, err) - - _, err = inter.Invoke("test") - require.NoError(t, err) - - assert.True(t, newEmulatorBackendInvoked) - }) - t.Run("createSnapshot", func(t *testing.T) { t.Parallel() @@ -2355,8 +2317,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.createSnapshot(name: "adminCreated") + Test.createSnapshot(name: "adminCreated") } ` @@ -2391,8 +2352,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.createSnapshot(name: "adminCreated") + Test.createSnapshot(name: "adminCreated") } ` @@ -2427,9 +2387,8 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.createSnapshot(name: "adminCreated") - blockchain.loadSnapshot(name: "adminCreated") + Test.createSnapshot(name: "adminCreated") + Test.loadSnapshot(name: "adminCreated") } ` @@ -2469,9 +2428,8 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - blockchain.createSnapshot(name: "adminCreated") - blockchain.loadSnapshot(name: "contractDeployed") + Test.createSnapshot(name: "adminCreated") + Test.loadSnapshot(name: "contractDeployed") } ` @@ -2511,8 +2469,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - let err = blockchain.deployContract( + let err = Test.deployContract( name: "FooContract", path: "./contracts/FooContract.cdc", arguments: ["Hey, there!"] @@ -2562,8 +2519,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - let err = blockchain.deployContract( + let err = Test.deployContract( name: "FooContract", path: "./contracts/FooContract.cdc", arguments: ["Hey, there!"] @@ -2611,8 +2567,8 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - let account = blockchain.getAccount(0x0000000000000009) + let account = Test.getAccount(0x0000000000000009) + Test.assertEqual(0x0000000000000009 as Address, account.address) } ` @@ -2657,8 +2613,7 @@ func TestBlockchain(t *testing.T) { import Test pub fun test() { - let blockchain = Test.blockchain - let account = blockchain.getAccount(0x0000000000000009) + let account = Test.getAccount(0x0000000000000009) } ` From d39fa9eb8b1edecba066ea4c8d51a5768cc846a3 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 15 Sep 2023 16:16:01 +0300 Subject: [PATCH 03/20] Caching of initializeMemberResolvers seems to cause a bug For contracts that contain both a .cdc source file, with Cadence code, and with some functions being implemented natively, the GetMembers() does not return the natively implemented members. --- runtime/sema/type.go | 14 ++++++++++++-- runtime/stdlib/test_contract.go | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/runtime/sema/type.go b/runtime/sema/type.go index 9e2ab8b422..821b8550b4 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -4262,7 +4262,11 @@ func (t *CompositeType) GetMembers() map[string]MemberResolver { } func (t *CompositeType) initializeMemberResolvers() { - t.memberResolversOnce.Do(func() { + t.memberResolversOnce.Do(t.initializerMemberResolversFunc()) +} + +func (t *CompositeType) initializerMemberResolversFunc() func() { + return func() { memberResolvers := MembersMapAsResolvers(t.Members) // Check conformances. @@ -4281,7 +4285,13 @@ func (t *CompositeType) initializeMemberResolvers() { }) t.memberResolvers = withBuiltinMembers(t, memberResolvers) - }) + } +} + +func (t *CompositeType) ResolveMembers() { + if t.Members.Len() != len(t.GetMembers()) { + t.initializerMemberResolversFunc()() + } } func (t *CompositeType) FieldPosition(name string, declaration ast.CompositeLikeDeclaration) ast.Position { diff --git a/runtime/stdlib/test_contract.go b/runtime/stdlib/test_contract.go index 0cd3db214c..36cb2980df 100644 --- a/runtime/stdlib/test_contract.go +++ b/runtime/stdlib/test_contract.go @@ -1193,6 +1193,7 @@ func newTestContractType() *TestContractType { ty.expectFailureFunction = newTestTypeExpectFailureFunction( expectFailureFunctionType, ) + compositeType.ResolveMembers() return ty } From b1b8bdd207de5d5f4d33119591589dcc4ca7200a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 19 Sep 2023 12:02:49 +0300 Subject: [PATCH 04/20] Remove entirely the 'Test.useConfiguration' function --- runtime/stdlib/contracts/test.cdc | 12 ---- runtime/stdlib/test-framework.go | 6 -- runtime/stdlib/test.go | 2 - runtime/stdlib/test_emulatorbackend.go | 76 -------------------------- runtime/stdlib/test_test.go | 9 --- 5 files changed, 105 deletions(-) diff --git a/runtime/stdlib/contracts/test.cdc b/runtime/stdlib/contracts/test.cdc index 04fe12aa36..1f9972fb1e 100644 --- a/runtime/stdlib/contracts/test.cdc +++ b/runtime/stdlib/contracts/test.cdc @@ -91,13 +91,6 @@ pub contract Test { ) } - /// Set the configuration to be used by the blockchain. - /// Overrides any existing configuration. - /// - pub fun useConfiguration(_ configuration: Configuration) { - self.backend.useConfiguration(configuration) - } - /// Returns all the logs from the blockchain, up to the calling point. /// pub fun logs(): [String] { @@ -324,11 +317,6 @@ pub contract Test { arguments: [AnyStruct] ): Error? - /// Set the configuration to be used by the blockchain. - /// Overrides any existing configuration. - /// - pub fun useConfiguration(_ configuration: Configuration) - /// Returns all the logs from the blockchain, up to the calling point. /// pub fun logs(): [String] diff --git a/runtime/stdlib/test-framework.go b/runtime/stdlib/test-framework.go index facb02145c..c8b68cedc1 100644 --- a/runtime/stdlib/test-framework.go +++ b/runtime/stdlib/test-framework.go @@ -64,8 +64,6 @@ type Blockchain interface { arguments []interpreter.Value, ) error - UseConfiguration(configuration *Configuration) - StandardLibraryHandler() StandardLibraryHandler Logs() []string @@ -99,7 +97,3 @@ type Account struct { PublicKey *PublicKey Address common.Address } - -type Configuration struct { - Addresses map[string]common.Address -} diff --git a/runtime/stdlib/test.go b/runtime/stdlib/test.go index db3b08ff59..f4866afc23 100644 --- a/runtime/stdlib/test.go +++ b/runtime/stdlib/test.go @@ -48,8 +48,6 @@ const accountAddressFieldName = "address" const matcherTestFunctionName = "test" -const addressesFieldName = "addresses" - const TestContractLocation = common.IdentifierLocation(testContractTypeName) var testOnce sync.Once diff --git a/runtime/stdlib/test_emulatorbackend.go b/runtime/stdlib/test_emulatorbackend.go index 876e6c6858..1e087139ea 100644 --- a/runtime/stdlib/test_emulatorbackend.go +++ b/runtime/stdlib/test_emulatorbackend.go @@ -43,7 +43,6 @@ type testEmulatorBackendType struct { executeNextTransactionFunctionType *sema.FunctionType commitBlockFunctionType *sema.FunctionType deployContractFunctionType *sema.FunctionType - useConfigFunctionType *sema.FunctionType logsFunctionType *sema.FunctionType serviceAccountFunctionType *sema.FunctionType eventsFunctionType *sema.FunctionType @@ -87,11 +86,6 @@ func newTestEmulatorBackendType( testEmulatorBackendTypeDeployContractFunctionName, ) - useConfigFunctionType := interfaceFunctionType( - blockchainBackendInterfaceType, - testEmulatorBackendTypeUseConfigFunctionName, - ) - logsFunctionType := interfaceFunctionType( blockchainBackendInterfaceType, testEmulatorBackendTypeLogsFunctionName, @@ -178,12 +172,6 @@ func newTestEmulatorBackendType( deployContractFunctionType, testEmulatorBackendTypeDeployContractFunctionDocString, ), - sema.NewUnmeteredPublicFunctionMember( - compositeType, - testEmulatorBackendTypeUseConfigFunctionName, - useConfigFunctionType, - testEmulatorBackendTypeUseConfigFunctionDocString, - ), sema.NewUnmeteredPublicFunctionMember( compositeType, testEmulatorBackendTypeLogsFunctionName, @@ -245,7 +233,6 @@ func newTestEmulatorBackendType( executeNextTransactionFunctionType: executeNextTransactionFunctionType, commitBlockFunctionType: commitBlockFunctionType, deployContractFunctionType: deployContractFunctionType, - useConfigFunctionType: useConfigFunctionType, logsFunctionType: logsFunctionType, serviceAccountFunctionType: serviceAccountFunctionType, eventsFunctionType: eventsFunctionType, @@ -589,65 +576,6 @@ func (t *testEmulatorBackendType) newDeployContractFunction( ) } -// 'EmulatorBackend.useConfiguration' function - -const testEmulatorBackendTypeUseConfigFunctionName = "useConfiguration" - -const testEmulatorBackendTypeUseConfigFunctionDocString = ` -Set the configuration to be used by the blockchain. -Overrides any existing configuration. -` - -func (t *testEmulatorBackendType) newUseConfigFunction( - blockchain Blockchain, -) *interpreter.HostFunctionValue { - return interpreter.NewUnmeteredHostFunctionValue( - t.useConfigFunctionType, - func(invocation interpreter.Invocation) interpreter.Value { - inter := invocation.Interpreter - - // configurations - configsValue, ok := invocation.Arguments[0].(*interpreter.CompositeValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - addresses, ok := configsValue.GetMember( - inter, - invocation.LocationRange, - addressesFieldName, - ).(*interpreter.DictionaryValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - mapping := make(map[string]common.Address, addresses.Count()) - - addresses.Iterate(inter, func(locationValue, addressValue interpreter.Value) bool { - location, ok := locationValue.(*interpreter.StringValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - address, ok := addressValue.(interpreter.AddressValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - mapping[location.Str] = common.Address(address) - - return true - }) - - blockchain.UseConfiguration(&Configuration{ - Addresses: mapping, - }) - - return interpreter.Void - }, - ) -} - // 'EmulatorBackend.logs' function const testEmulatorBackendTypeLogsFunctionName = "logs" @@ -893,10 +821,6 @@ func (t *testEmulatorBackendType) newEmulatorBackend( Name: testEmulatorBackendTypeDeployContractFunctionName, Value: t.newDeployContractFunction(blockchain), }, - { - Name: testEmulatorBackendTypeUseConfigFunctionName, - Value: t.newUseConfigFunction(blockchain), - }, { Name: testEmulatorBackendTypeLogsFunctionName, Value: t.newLogsFunction(blockchain), diff --git a/runtime/stdlib/test_test.go b/runtime/stdlib/test_test.go index a47bfbea94..734c3a66d7 100644 --- a/runtime/stdlib/test_test.go +++ b/runtime/stdlib/test_test.go @@ -2679,7 +2679,6 @@ type mockedBlockchain struct { executeTransaction func() *stdlib.TransactionResult commitBlock func() error deployContract func(inter *interpreter.Interpreter, name string, path string, arguments []interpreter.Value) error - useConfiguration func(configuration *stdlib.Configuration) stdlibHandler func() stdlib.StandardLibraryHandler logs func() []string serviceAccount func() (*stdlib.Account, error) @@ -2763,14 +2762,6 @@ func (m mockedBlockchain) DeployContract( return m.deployContract(inter, name, path, arguments) } -func (m mockedBlockchain) UseConfiguration(configuration *stdlib.Configuration) { - if m.useConfiguration == nil { - panic("'UseConfiguration' is not implemented") - } - - m.useConfiguration(configuration) -} - func (m mockedBlockchain) StandardLibraryHandler() stdlib.StandardLibraryHandler { if m.stdlibHandler == nil { panic("'StandardLibraryHandler' is not implemented") From 34f6ebea1c4e89e48a9ade63e4722cbbe6bbb2f3 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 22 Sep 2023 12:20:55 +0300 Subject: [PATCH 05/20] Remove left-over Test.Configuration struct --- runtime/stdlib/contracts/test.cdc | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/runtime/stdlib/contracts/test.cdc b/runtime/stdlib/contracts/test.cdc index 1f9972fb1e..b2e6ba9698 100644 --- a/runtime/stdlib/contracts/test.cdc +++ b/runtime/stdlib/contracts/test.cdc @@ -249,17 +249,6 @@ pub contract Test { } } - /// Configuration to be used by the blockchain. - /// Can be used to set the address mappings. - /// - pub struct Configuration { - pub let addresses: {String: Address} - - init(addresses: {String: Address}) { - self.addresses = addresses - } - } - /// Transaction that can be submitted and executed on the blockchain. /// pub struct Transaction { From 72bbfe1e5d4a5ed438762cfe05a6c7b1c1f731ef Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Sep 2023 12:42:56 +0300 Subject: [PATCH 06/20] Use new access modifiers in Test contract --- runtime/stdlib/contracts/test.cdc | 160 ++++++++++++++++++------------ 1 file changed, 97 insertions(+), 63 deletions(-) diff --git a/runtime/stdlib/contracts/test.cdc b/runtime/stdlib/contracts/test.cdc index b2e6ba9698..8e7a39da7d 100644 --- a/runtime/stdlib/contracts/test.cdc +++ b/runtime/stdlib/contracts/test.cdc @@ -1,10 +1,10 @@ /// Test contract is the standard library that provides testing functionality in Cadence. /// -pub contract Test { +access(all) contract Test { /// backend emulates a real network. /// - pub let backend: AnyStruct{BlockchainBackend} + access(self) let backend: AnyStruct{BlockchainBackend} init(backend: AnyStruct{BlockchainBackend}) { self.backend = backend @@ -13,7 +13,8 @@ pub contract Test { /// Executes a script and returns the script return value and the status. /// `returnValue` field of the result will be `nil` if the script failed. /// - pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult { + access(all) + fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult { return self.backend.executeScript(script, arguments) } @@ -21,39 +22,45 @@ pub contract Test { /// The transaction is paid by the service account. /// The returned account can be used to sign and authorize transactions. /// - pub fun createAccount(): Account { + access(all) + fun createAccount(): Account { return self.backend.createAccount() } /// Returns the account for the given address. /// - pub fun getAccount(_ address: Address): Account { + access(all) + fun getAccount(_ address: Address): Account { return self.backend.getAccount(address) } /// Add a transaction to the current block. /// - pub fun addTransaction(_ tx: Transaction) { + access(all) + fun addTransaction(_ tx: Transaction) { self.backend.addTransaction(tx) } /// Executes the next transaction in the block, if any. /// Returns the result of the transaction, or nil if no transaction was scheduled. /// - pub fun executeNextTransaction(): TransactionResult? { + access(all) + fun executeNextTransaction(): TransactionResult? { return self.backend.executeNextTransaction() } /// Commit the current block. /// Committing will fail if there are un-executed transactions in the block. /// - pub fun commitBlock() { + access(all) + fun commitBlock() { self.backend.commitBlock() } /// Executes a given transaction and commit the current block. /// - pub fun executeTransaction(_ tx: Transaction): TransactionResult { + access(all) + fun executeTransaction(_ tx: Transaction): TransactionResult { self.addTransaction(tx) let txResult = self.executeNextTransaction()! self.commitBlock() @@ -62,7 +69,8 @@ pub contract Test { /// Executes a given set of transactions and commit the current block. /// - pub fun executeTransactions(_ transactions: [Transaction]): [TransactionResult] { + access(all) + fun executeTransactions(_ transactions: [Transaction]): [TransactionResult] { for tx in transactions { self.addTransaction(tx) } @@ -79,7 +87,8 @@ pub contract Test { /// Deploys a given contract, and initilizes it with the arguments. /// - pub fun deployContract( + access(all) + fun deployContract( name: String, path: String, arguments: [AnyStruct] @@ -93,40 +102,46 @@ pub contract Test { /// Returns all the logs from the blockchain, up to the calling point. /// - pub fun logs(): [String] { + access(all) + fun logs(): [String] { return self.backend.logs() } /// Returns the service account of the blockchain. Can be used to sign /// transactions with this account. /// - pub fun serviceAccount(): Account { + access(all) + fun serviceAccount(): Account { return self.backend.serviceAccount() } /// Returns all events emitted from the blockchain. /// - pub fun events(): [AnyStruct] { + access(all) + fun events(): [AnyStruct] { return self.backend.events(nil) } /// Returns all events emitted from the blockchain, /// filtered by type. /// - pub fun eventsOfType(_ type: Type): [AnyStruct] { + access(all) + fun eventsOfType(_ type: Type): [AnyStruct] { return self.backend.events(type) } /// Resets the state of the blockchain to the given height. /// - pub fun reset(to height: UInt64) { + access(all) + fun reset(to height: UInt64) { self.backend.reset(to: height) } /// Moves the time of the blockchain by the given delta, /// which should be passed in the form of seconds. /// - pub fun moveTime(by delta: Fix64) { + access(all) + fun moveTime(by delta: Fix64) { self.backend.moveTime(by: delta) } @@ -153,18 +168,19 @@ pub contract Test { } } - pub struct Matcher { + access(all) struct Matcher { - pub let test: ((AnyStruct): Bool) + access(all) let test: ((AnyStruct): Bool) - pub init(test: ((AnyStruct): Bool)) { + init(test: ((AnyStruct): Bool)) { self.test = test } /// Combine this matcher with the given matcher. /// Returns a new matcher that succeeds if this and the given matcher succeed. /// - pub fun and(_ other: Matcher): Matcher { + access(all) + fun and(_ other: Matcher): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return self.test(value) && other.test(value) }) @@ -174,7 +190,8 @@ pub contract Test { /// Returns a new matcher that succeeds if this or the given matcher succeed. /// If this matcher succeeds, then the other matcher would not be tested. /// - pub fun or(_ other: Matcher): Matcher { + access(all) + fun or(_ other: Matcher): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return self.test(value) || other.test(value) }) @@ -183,29 +200,29 @@ pub contract Test { /// ResultStatus indicates status of a transaction or script execution. /// - pub enum ResultStatus: UInt8 { - pub case succeeded - pub case failed + access(all) enum ResultStatus: UInt8 { + access(all) case succeeded + access(all) case failed } /// Result is the interface to be implemented by the various execution /// operations, such as transactions and scripts. /// - pub struct interface Result { + access(all) struct interface Result { /// The result status of an executed operation. /// - pub let status: ResultStatus + access(all) let status: ResultStatus /// The optional error of an executed operation. /// - pub let error: Error? + access(all) let error: Error? } /// The result of a transaction execution. /// - pub struct TransactionResult: Result { - pub let status: ResultStatus - pub let error: Error? + access(all) struct TransactionResult: Result { + access(all) let status: ResultStatus + access(all) let error: Error? init(status: ResultStatus, error: Error?) { self.status = status @@ -215,10 +232,10 @@ pub contract Test { /// The result of a script execution. /// - pub struct ScriptResult: Result { - pub let status: ResultStatus - pub let returnValue: AnyStruct? - pub let error: Error? + access(all) struct ScriptResult: Result { + access(all) let status: ResultStatus + access(all) let returnValue: AnyStruct? + access(all) let error: Error? init(status: ResultStatus, returnValue: AnyStruct?, error: Error?) { self.status = status @@ -229,8 +246,8 @@ pub contract Test { // Error is returned if something has gone wrong. // - pub struct Error { - pub let message: String + access(all) struct Error { + access(all) let message: String init(_ message: String) { self.message = message @@ -239,9 +256,9 @@ pub contract Test { /// Account represents info about the account created on the blockchain. /// - pub struct Account { - pub let address: Address - pub let publicKey: PublicKey + access(all) struct Account { + access(all) let address: Address + access(all) let publicKey: PublicKey init(address: Address, publicKey: PublicKey) { self.address = address @@ -251,11 +268,11 @@ pub contract Test { /// Transaction that can be submitted and executed on the blockchain. /// - pub struct Transaction { - pub let code: String - pub let authorizers: [Address] - pub let signers: [Account] - pub let arguments: [AnyStruct] + access(all) struct Transaction { + access(all) let code: String + access(all) let authorizers: [Address] + access(all) let signers: [Account] + access(all) let arguments: [AnyStruct] init(code: String, authorizers: [Address], signers: [Account], arguments: [AnyStruct]) { self.code = code @@ -267,40 +284,47 @@ pub contract Test { /// BlockchainBackend is the interface to be implemented by the backend providers. /// - pub struct interface BlockchainBackend { + access(all) struct interface BlockchainBackend { /// Executes a script and returns the script return value and the status. /// `returnValue` field of the result will be `nil` if the script failed. /// - pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult + access(all) + fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult /// Creates a signer account by submitting an account creation transaction. /// The transaction is paid by the service account. /// The returned account can be used to sign and authorize transactions. /// - pub fun createAccount(): Account + access(all) + fun createAccount(): Account /// Returns the account for the given address. /// - pub fun getAccount(_ address: Address): Account + access(all) + fun getAccount(_ address: Address): Account /// Add a transaction to the current block. /// - pub fun addTransaction(_ tx: Transaction) + access(all) + fun addTransaction(_ tx: Transaction) /// Executes the next transaction in the block, if any. /// Returns the result of the transaction, or nil if no transaction was scheduled. /// - pub fun executeNextTransaction(): TransactionResult? + access(all) + fun executeNextTransaction(): TransactionResult? /// Commit the current block. /// Committing will fail if there are un-executed transactions in the block. /// - pub fun commitBlock() + access(all) + fun commitBlock() /// Deploys a given contract, and initilizes it with the arguments. /// - pub fun deployContract( + access(all) + fun deployContract( name: String, path: String, arguments: [AnyStruct] @@ -308,26 +332,31 @@ pub contract Test { /// Returns all the logs from the blockchain, up to the calling point. /// - pub fun logs(): [String] + access(all) + fun logs(): [String] /// Returns the service account of the blockchain. Can be used to sign /// transactions with this account. /// - pub fun serviceAccount(): Account + access(all) + fun serviceAccount(): Account /// Returns all events emitted from the blockchain, optionally filtered /// by type. /// - pub fun events(_ type: Type?): [AnyStruct] + access(all) + fun events(_ type: Type?): [AnyStruct] /// Resets the state of the blockchain to the given height. /// - pub fun reset(to height: UInt64) + access(all) + fun reset(to height: UInt64) /// Moves the time of the blockchain by the given delta, /// which should be passed in the form of seconds. /// - pub fun moveTime(by delta: Fix64) + access(all) + fun moveTime(by delta: Fix64) /// Creates a snapshot of the blockchain, at the /// current ledger state, with the given name. @@ -345,7 +374,8 @@ pub contract Test { /// Returns a new matcher that negates the test of the given matcher. /// - pub fun not(_ matcher: Matcher): Matcher { + access(all) + fun not(_ matcher: Matcher): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return !matcher.test(value) }) @@ -355,7 +385,8 @@ pub contract Test { /// a ScriptResult or TransactionResult and the ResultStatus is succeeded. /// Returns false in any other case. /// - pub fun beSucceeded(): Matcher { + access(all) + fun beSucceeded(): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return (value as! {Result}).status == ResultStatus.succeeded }) @@ -365,7 +396,8 @@ pub contract Test { /// a ScriptResult or TransactionResult and the ResultStatus is failed. /// Returns false in any other case. /// - pub fun beFailed(): Matcher { + access(all) + fun beFailed(): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return (value as! {Result}).status == ResultStatus.failed }) @@ -373,7 +405,8 @@ pub contract Test { /// Returns a new matcher that checks if the given test value is nil. /// - pub fun beNil(): Matcher { + access(all) + fun beNil(): Matcher { return Matcher(test: fun (value: AnyStruct): Bool { return value == nil }) @@ -383,7 +416,8 @@ pub contract Test { /// a script or transaction, has failed and contains the given error /// message. /// - pub fun assertError(_ result: {Result}, errorMessage: String) { + access(all) + fun assertError(_ result: {Result}, errorMessage: String) { pre { result.status == ResultStatus.failed: "no error was found" } From 20a58ec3dbb9637bccb42422949a4661bda287d5 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Sep 2023 12:44:02 +0300 Subject: [PATCH 07/20] Rename TestFramework's NewEmulatorBackend function to EmulatorBackend --- runtime/stdlib/test-framework.go | 2 +- runtime/stdlib/test_contract.go | 2 +- runtime/stdlib/test_test.go | 42 ++++++++++++++++---------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/runtime/stdlib/test-framework.go b/runtime/stdlib/test-framework.go index c8b68cedc1..6496ee98fb 100644 --- a/runtime/stdlib/test-framework.go +++ b/runtime/stdlib/test-framework.go @@ -30,7 +30,7 @@ import ( // This is used as a way to inject test provider dependencies dynamically. type TestFramework interface { - NewEmulatorBackend() Blockchain + EmulatorBackend() Blockchain ReadFile(string) (string, error) } diff --git a/runtime/stdlib/test_contract.go b/runtime/stdlib/test_contract.go index 36cb2980df..3a7bcd12fb 100644 --- a/runtime/stdlib/test_contract.go +++ b/runtime/stdlib/test_contract.go @@ -1246,7 +1246,7 @@ func (t *TestContractType) NewTestContract( initializerTypes := t.InitializerTypes emulatorBackend := t.emulatorBackendType.newEmulatorBackend( inter, - testFramework.NewEmulatorBackend(), + testFramework.EmulatorBackend(), interpreter.EmptyLocationRange, ) value, err := inter.InvokeFunctionValue( diff --git a/runtime/stdlib/test_test.go b/runtime/stdlib/test_test.go index 734c3a66d7..de5545174a 100644 --- a/runtime/stdlib/test_test.go +++ b/runtime/stdlib/test_test.go @@ -42,7 +42,7 @@ import ( func newTestContractInterpreter(t *testing.T, code string) (*interpreter.Interpreter, error) { testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{} }, } @@ -2070,7 +2070,7 @@ func TestBlockchain(t *testing.T) { eventsInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ events: func(inter *interpreter.Interpreter, eventType interpreter.StaticType) interpreter.Value { eventsInvoked = true @@ -2117,7 +2117,7 @@ func TestBlockchain(t *testing.T) { eventsInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ events: func(inter *interpreter.Interpreter, eventType interpreter.StaticType) interpreter.Value { eventsInvoked = true @@ -2161,7 +2161,7 @@ func TestBlockchain(t *testing.T) { resetInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ reset: func(height uint64) { resetInvoked = true @@ -2194,7 +2194,7 @@ func TestBlockchain(t *testing.T) { resetInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ reset: func(height uint64) { resetInvoked = true @@ -2226,7 +2226,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2262,7 +2262,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2295,7 +2295,7 @@ func TestBlockchain(t *testing.T) { moveTimeInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ moveTime: func(timeDelta int64) { moveTimeInvoked = true @@ -2324,7 +2324,7 @@ func TestBlockchain(t *testing.T) { createSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { createSnapshotInvoked = true @@ -2359,7 +2359,7 @@ func TestBlockchain(t *testing.T) { createSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { createSnapshotInvoked = true @@ -2395,7 +2395,7 @@ func TestBlockchain(t *testing.T) { loadSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { assert.Equal(t, "adminCreated", name) @@ -2436,7 +2436,7 @@ func TestBlockchain(t *testing.T) { loadSnapshotInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ createSnapshot: func(name string) error { assert.Equal(t, "adminCreated", name) @@ -2482,7 +2482,7 @@ func TestBlockchain(t *testing.T) { deployContractInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ deployContract: func( inter *interpreter.Interpreter, @@ -2535,7 +2535,7 @@ func TestBlockchain(t *testing.T) { deployContractInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ deployContract: func( inter *interpreter.Interpreter, @@ -2575,7 +2575,7 @@ func TestBlockchain(t *testing.T) { getAccountInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ getAccount: func(address interpreter.AddressValue) (*stdlib.Account, error) { getAccountInvoked = true @@ -2620,7 +2620,7 @@ func TestBlockchain(t *testing.T) { getAccountInvoked := false testFramework := &mockedTestFramework{ - newEmulatorBackend: func() stdlib.Blockchain { + emulatorBackend: func() stdlib.Blockchain { return &mockedBlockchain{ getAccount: func(address interpreter.AddressValue) (*stdlib.Account, error) { getAccountInvoked = true @@ -2649,18 +2649,18 @@ func TestBlockchain(t *testing.T) { } type mockedTestFramework struct { - newEmulatorBackend func() stdlib.Blockchain - readFile func(s string) (string, error) + emulatorBackend func() stdlib.Blockchain + readFile func(s string) (string, error) } var _ stdlib.TestFramework = &mockedTestFramework{} -func (m mockedTestFramework) NewEmulatorBackend() stdlib.Blockchain { - if m.newEmulatorBackend == nil { +func (m mockedTestFramework) EmulatorBackend() stdlib.Blockchain { + if m.emulatorBackend == nil { panic("'NewEmulatorBackend' is not implemented") } - return m.newEmulatorBackend() + return m.emulatorBackend() } func (m mockedTestFramework) ReadFile(fileName string) (string, error) { From 4d0836ee61c8c2aafd0f9b2e6633c251d8065147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 27 Sep 2023 16:00:55 -0700 Subject: [PATCH 08/20] check native function declarations --- runtime/sema/check_function.go | 17 +++++- runtime/sema/errors.go | 17 ++++++ runtime/tests/checker/function_test.go | 76 ++++++++++++++++++++++---- 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/runtime/sema/check_function.go b/runtime/sema/check_function.go index 8797b50f37..46d2d0392a 100644 --- a/runtime/sema/check_function.go +++ b/runtime/sema/check_function.go @@ -76,6 +76,21 @@ func (checker *Checker) visitFunctionDeclaration( declaration.Identifier, ) + functionBlock := declaration.FunctionBlock + + if declaration.IsNative() { + if !functionBlock.IsEmpty() { + checker.report(&NativeFunctionWithImplementationError{ + Range: ast.NewRangeFromPositioned( + checker.memoryGauge, + functionBlock, + ), + }) + } + + functionBlock = nil + } + // global functions were previously declared, see `declareFunctionDeclaration` functionType := checker.Elaboration.FunctionDeclarationFunctionType(declaration) @@ -93,7 +108,7 @@ func (checker *Checker) visitFunctionDeclaration( declaration.ParameterList, declaration.ReturnTypeAnnotation, functionType, - declaration.FunctionBlock, + functionBlock, options.mustExit, nil, options.checkResourceLoss, diff --git a/runtime/sema/errors.go b/runtime/sema/errors.go index 958ab6009f..7d1d793c55 100644 --- a/runtime/sema/errors.go +++ b/runtime/sema/errors.go @@ -878,6 +878,23 @@ func (e *InvalidNativeModifierError) Error() string { return "invalid native modifier for declaration" } +// NativeFunctionWithImplementationError + +type NativeFunctionWithImplementationError struct { + ast.Range +} + +var _ SemanticError = &NativeFunctionWithImplementationError{} +var _ errors.UserError = &NativeFunctionWithImplementationError{} + +func (*NativeFunctionWithImplementationError) isSemanticError() {} + +func (*NativeFunctionWithImplementationError) IsUserError() {} + +func (e *NativeFunctionWithImplementationError) Error() string { + return "native function may not have an implementation" +} + // InvalidNameError type InvalidNameError struct { diff --git a/runtime/tests/checker/function_test.go b/runtime/tests/checker/function_test.go index 4ce60c282d..29a6066029 100644 --- a/runtime/tests/checker/function_test.go +++ b/runtime/tests/checker/function_test.go @@ -440,20 +440,74 @@ func TestCheckNativeFunctionDeclaration(t *testing.T) { t.Parallel() - _, err := ParseAndCheckWithOptions(t, - ` - native fun test() {} - `, - ParseAndCheckOptions{ - ParseOptions: parser.Config{ - NativeModifierEnabled: true, + t.Run("disabled", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheckWithOptions(t, + ` + native fun test(): Int {} + `, + ParseAndCheckOptions{ + ParseOptions: parser.Config{ + NativeModifierEnabled: true, + }, + Config: &sema.Config{ + AllowNativeDeclarations: false, + }, }, - }, - ) + ) - errs := RequireCheckerErrors(t, err, 1) + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.InvalidNativeModifierError{}, errs[0]) + }) + + t.Run("enabled, valid", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheckWithOptions(t, + ` + native fun test(): Int {} + `, + ParseAndCheckOptions{ + ParseOptions: parser.Config{ + NativeModifierEnabled: true, + }, + Config: &sema.Config{ + AllowNativeDeclarations: true, + }, + }, + ) + + require.NoError(t, err) + }) + + t.Run("enabled, invalid", func(t *testing.T) { - assert.IsType(t, &sema.InvalidNativeModifierError{}, errs[0]) + t.Parallel() + + _, err := ParseAndCheckWithOptions(t, + ` + native fun test(): Int { + return 1 + } + `, + ParseAndCheckOptions{ + ParseOptions: parser.Config{ + NativeModifierEnabled: true, + }, + Config: &sema.Config{ + AllowNativeDeclarations: true, + }, + }, + ) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.NativeFunctionWithImplementationError{}, errs[0]) + }) } func TestCheckResultVariable(t *testing.T) { From 5f185e0d3f54a6b0d037fa40818b5ce5c7ac287b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 27 Sep 2023 16:20:00 -0700 Subject: [PATCH 09/20] improve error message Co-authored-by: Supun Setunga --- runtime/sema/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/sema/errors.go b/runtime/sema/errors.go index 7d1d793c55..5fc1424792 100644 --- a/runtime/sema/errors.go +++ b/runtime/sema/errors.go @@ -892,7 +892,7 @@ func (*NativeFunctionWithImplementationError) isSemanticError() {} func (*NativeFunctionWithImplementationError) IsUserError() {} func (e *NativeFunctionWithImplementationError) Error() string { - return "native function may not have an implementation" + return "native function must not have an implementation" } // InvalidNameError From d174f7be8598456f5c126c11ccb742b193208391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 27 Sep 2023 16:24:54 -0700 Subject: [PATCH 10/20] add test case for declaring native function in composite --- runtime/tests/checker/function_test.go | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/runtime/tests/checker/function_test.go b/runtime/tests/checker/function_test.go index 29a6066029..15aec78eea 100644 --- a/runtime/tests/checker/function_test.go +++ b/runtime/tests/checker/function_test.go @@ -24,6 +24,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/sema" ) @@ -508,6 +510,55 @@ func TestCheckNativeFunctionDeclaration(t *testing.T) { assert.IsType(t, &sema.NativeFunctionWithImplementationError{}, errs[0]) }) + + t.Run("enabled, composite", func(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheckWithOptions(t, + ` + struct S { + native fun test(foo: String): Int {} + } + `, + ParseAndCheckOptions{ + ParseOptions: parser.Config{ + NativeModifierEnabled: true, + }, + Config: &sema.Config{ + AllowNativeDeclarations: true, + }, + }, + ) + require.NoError(t, err) + + sType := RequireGlobalType(t, checker.Elaboration, "S") + require.NotNil(t, sType) + + const testFunctionIdentifier = "test" + testMemberResolver, ok := sType.GetMembers()[testFunctionIdentifier] + require.True(t, ok) + + assert.Equal(t, + common.DeclarationKindFunction, + testMemberResolver.Kind, + ) + + member := testMemberResolver.Resolve(nil, testFunctionIdentifier, ast.EmptyRange, nil) + + assert.Equal(t, + sema.NewTypeAnnotation(&sema.FunctionType{ + Parameters: []sema.Parameter{ + { + Identifier: "foo", + TypeAnnotation: sema.NewTypeAnnotation(sema.StringType), + }, + }, + ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.IntType), + }), + member.TypeAnnotation, + ) + }) } func TestCheckResultVariable(t *testing.T) { From 88f2e1d70c767a9f6217f80efa7fa48563dc16d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 1 Aug 2023 17:08:58 -0700 Subject: [PATCH 11/20] fix swapping in resource array: insert temporary placeholder --- runtime/interpreter/interpreter_expression.go | 28 +++-- runtime/interpreter/value.go | 3 + runtime/interpreter/value_placeholder.go | 104 ++++++++++++++++ runtime/tests/interpreter/interpreter_test.go | 112 ++++++++++++++++++ 4 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 runtime/interpreter/value_placeholder.go diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index edb24e1d0c..70514e4687 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -138,21 +138,25 @@ func (interpreter *Interpreter) valueIndexExpressionGetterSetter(indexExpression isNestedResourceMove := elaboration.IsNestedResourceMoveExpression(indexExpression) + var get func(allowMissing bool) Value + + if isNestedResourceMove { + get = func(_ bool) Value { + value := target.RemoveKey(interpreter, locationRange, transferredIndexingValue) + target.InsertKey(interpreter, locationRange, transferredIndexingValue, placeholder) + return value + } + } else { + get = func(_ bool) Value { + return target.GetKey(interpreter, locationRange, transferredIndexingValue) + } + } + return getterSetter{ target: target, - get: func(_ bool) Value { - if isNestedResourceMove { - return target.RemoveKey(interpreter, locationRange, transferredIndexingValue) - } else { - return target.GetKey(interpreter, locationRange, transferredIndexingValue) - } - }, + get: get, set: func(value Value) { - if isNestedResourceMove { - target.InsertKey(interpreter, locationRange, transferredIndexingValue, value) - } else { - target.SetKey(interpreter, locationRange, transferredIndexingValue, value) - } + target.SetKey(interpreter, locationRange, transferredIndexingValue, value) }, } } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 8a7aa890d2..4e34139977 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -18153,6 +18153,9 @@ func (v *DictionaryValue) SetKey( case NilValue: _ = v.Remove(interpreter, locationRange, keyValue) + case placeholderValue: + // NO-OP + default: panic(errors.NewUnreachableError()) } diff --git a/runtime/interpreter/value_placeholder.go b/runtime/interpreter/value_placeholder.go new file mode 100644 index 0000000000..24572a7620 --- /dev/null +++ b/runtime/interpreter/value_placeholder.go @@ -0,0 +1,104 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * 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 interpreter + +import ( + "github.com/onflow/atree" + + "github.com/onflow/cadence/runtime/common" +) + +// placeholderValue +type placeholderValue struct{} + +var placeholder Value = placeholderValue{} + +var _ Value = placeholderValue{} + +func (placeholderValue) isValue() {} + +func (f placeholderValue) String() string { + return f.RecursiveString(SeenReferences{}) +} + +func (f placeholderValue) RecursiveString(_ SeenReferences) string { + return "" +} + +func (f placeholderValue) MeteredString(_ common.MemoryGauge, _ SeenReferences) string { + return "" +} + +func (f placeholderValue) Accept(_ *Interpreter, _ Visitor) { + // NO-OP +} + +func (f placeholderValue) Walk(_ *Interpreter, _ func(Value)) { + // NO-OP +} + +func (f placeholderValue) StaticType(_ *Interpreter) StaticType { + return PrimitiveStaticTypeNever +} + +func (placeholderValue) IsImportable(_ *Interpreter) bool { + return false +} + +func (f placeholderValue) ConformsToStaticType( + _ *Interpreter, + _ LocationRange, + _ TypeConformanceResults, +) bool { + return true +} + +func (f placeholderValue) Storable(_ atree.SlabStorage, _ atree.Address, _ uint64) (atree.Storable, error) { + return NonStorable{Value: f}, nil +} + +func (placeholderValue) NeedsStoreTo(_ atree.Address) bool { + return false +} + +func (placeholderValue) IsResourceKinded(_ *Interpreter) bool { + return false +} + +func (f placeholderValue) Transfer( + interpreter *Interpreter, + _ LocationRange, + _ atree.Address, + remove bool, + storable atree.Storable, +) Value { + // TODO: actually not needed, value is not storable + if remove { + interpreter.RemoveReferencedSlab(storable) + } + return f +} + +func (f placeholderValue) Clone(_ *Interpreter) Value { + return f +} + +func (placeholderValue) DeepRemove(_ *Interpreter) { + // NO-OP +} diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 92dbf988a8..ec45874fbc 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12011,3 +12011,115 @@ func TestInterpretConditionsWrapperFunctionType(t *testing.T) { require.NoError(t, err) }) } + +func TestInterpretSwapInSameArray(t *testing.T) { + + t.Parallel() + + t.Run("resources", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + resource R { + let value: Int + + init(value: Int) { + self.value = value + } + } + + fun test(): [Int] { + let rs <- [ + <- create R(value: 0), + <- create R(value: 1), + <- create R(value: 2) + ] + + // We swap only '0' and '1' + rs[0] <-> rs[1] + + let values = [ + rs[0].value, + rs[1].value, + rs[2].value + ] + + destroy rs + + return values + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + }, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(1), + interpreter.NewUnmeteredIntValueFromInt64(0), + interpreter.NewUnmeteredIntValueFromInt64(2), + ), + value, + ) + }) + + t.Run("structs", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + struct S { + let value: Int + + init(value: Int) { + self.value = value + } + } + + fun test(): [Int] { + let structs = [ + S(value: 0), + S(value: 1), + S(value: 2) + ] + + // We swap only '0' and '1' + structs[0] <-> structs[1] + + return [ + structs[0].value, + structs[1].value, + structs[2].value + ] + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + }, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(1), + interpreter.NewUnmeteredIntValueFromInt64(0), + interpreter.NewUnmeteredIntValueFromInt64(2), + ), + value, + ) + }) +} From de1ec1f7cb6fec16e3f9f50ee8ac0dc28edc19d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 1 Aug 2023 17:13:40 -0700 Subject: [PATCH 12/20] add test case for same indices --- runtime/tests/interpreter/interpreter_test.go | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index ec45874fbc..8e6529abcc 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12016,7 +12016,7 @@ func TestInterpretSwapInSameArray(t *testing.T) { t.Parallel() - t.Run("resources", func(t *testing.T) { + t.Run("resources, different indices", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` @@ -12071,6 +12071,62 @@ func TestInterpretSwapInSameArray(t *testing.T) { ) }) + t.Run("resources, same indices", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + resource R { + let value: Int + + init(value: Int) { + self.value = value + } + } + + fun test(): [Int] { + let rs <- [ + <- create R(value: 0), + <- create R(value: 1), + <- create R(value: 2) + ] + + // We swap only '1' + rs[1] <-> rs[1] + + let values = [ + rs[0].value, + rs[1].value, + rs[2].value + ] + + destroy rs + + return values + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + }, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(0), + interpreter.NewUnmeteredIntValueFromInt64(1), + interpreter.NewUnmeteredIntValueFromInt64(2), + ), + value, + ) + }) + t.Run("structs", func(t *testing.T) { t.Parallel() From 405464a6b9d696b93d6d7c0922178323e38b2d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 2 Aug 2023 13:44:06 -0700 Subject: [PATCH 13/20] add an explanation for the placeholder insertion --- runtime/interpreter/interpreter_expression.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index 70514e4687..96420aa4cd 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -136,6 +136,29 @@ func (interpreter *Interpreter) valueIndexExpressionGetterSetter(indexExpression }, ) + // Normally, moves of nested resources (e.g `let r <- rs[0]`) are statically rejected. + // + // However, there are cases in which we do allow moves of nested resources: + // + // - In a swap statement (e.g. `rs[0] <-> rs[1]`) + // - In a variable declaration with two values/assignments (e.g. `let r <- rs["foo"] <- nil`) + // + // In both cases we know that a move of the nested resource is immediately followed by a replacement. + // This notion of an expression that moves a nested resource is tracked in the elaboration. + // + // When indexing is a move of a nested resource, we need to remove the key/value from the container. + // However, for some containers, like arrays, the removal influences other values in the container. + // In case of an array, the removal of an element shifts all following elements. + // + // A removal alone would thus result in subsequent code being executed incorrectly. + // For example, in the case where a swap operation through indexing is performed on the same array, + // e.g. `rs[0] <-> rs[1]`, once the first removal was performed, the second operates on a modified container. + // + // Prevent this problem by temporarily writing a placeholder value after the removal. + // Only perform the replacement with a placeholder in the case of a nested resource move. + // We know that in that case the get operation will be followed by a set operation, + // which will replace the temporary placeholder. + isNestedResourceMove := elaboration.IsNestedResourceMoveExpression(indexExpression) var get func(allowMissing bool) Value From f1a8ff118cff59e22425defd7bff209abeba461a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 2 Oct 2023 12:01:38 -0700 Subject: [PATCH 14/20] add new parameter --- runtime/interpreter/value_placeholder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/interpreter/value_placeholder.go b/runtime/interpreter/value_placeholder.go index 24572a7620..6cc6184852 100644 --- a/runtime/interpreter/value_placeholder.go +++ b/runtime/interpreter/value_placeholder.go @@ -87,6 +87,7 @@ func (f placeholderValue) Transfer( _ atree.Address, remove bool, storable atree.Storable, + _ map[atree.StorageID]struct{}, ) Value { // TODO: actually not needed, value is not storable if remove { From c6e7139acc0cdf488a1ddc74164d711e45f4d532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 27 Sep 2023 14:32:21 -0700 Subject: [PATCH 15/20] fix swap statement evaluation evaluate all parts (left target, left key, right target, right key), before getting and setting --- runtime/interpreter/interpreter_statement.go | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/runtime/interpreter/interpreter_statement.go b/runtime/interpreter/interpreter_statement.go index 171748e389..dd6090c68c 100644 --- a/runtime/interpreter/interpreter_statement.go +++ b/runtime/interpreter/interpreter_statement.go @@ -153,8 +153,10 @@ func (interpreter *Interpreter) visitIfStatementWithVariableDeclaration( // If the resource was not moved ou of the container, // its contents get deleted. + getterSetter := interpreter.assignmentGetterSetter(declaration.Value) + const allowMissing = false - value := interpreter.assignmentGetterSetter(declaration.Value).get(allowMissing) + value := getterSetter.get(allowMissing) if value == nil { panic(errors.NewUnreachableError()) } @@ -498,8 +500,10 @@ func (interpreter *Interpreter) visitVariableDeclaration( // If the resource was not moved ou of the container, // its contents get deleted. + getterSetter := interpreter.assignmentGetterSetter(declaration.Value) + const allowMissing = false - result := interpreter.assignmentGetterSetter(declaration.Value).get(allowMissing) + result := getterSetter.get(allowMissing) if result == nil { panic(errors.NewUnreachableError()) } @@ -552,23 +556,32 @@ func (interpreter *Interpreter) VisitAssignmentStatement(assignment *ast.Assignm } func (interpreter *Interpreter) VisitSwapStatement(swap *ast.SwapStatement) StatementResult { + + // Get type information + swapStatementTypes := interpreter.Program.Elaboration.SwapStatementTypes(swap) leftType := swapStatementTypes.LeftType rightType := swapStatementTypes.RightType - const allowMissing = false + // Evaluate the left side (target and key) - // Evaluate the left expression leftGetterSetter := interpreter.assignmentGetterSetter(swap.Left) + + // Evaluate the right side (target and key) + + rightGetterSetter := interpreter.assignmentGetterSetter(swap.Right) + + // Get left and right values + + const allowMissing = false + leftValue := leftGetterSetter.get(allowMissing) interpreter.checkSwapValue(leftValue, swap.Left) - // Evaluate the right expression - rightGetterSetter := interpreter.assignmentGetterSetter(swap.Right) rightValue := rightGetterSetter.get(allowMissing) interpreter.checkSwapValue(rightValue, swap.Right) - // Set right value to left target + // Set right value to left target, // and left value to right target locationRange := LocationRange{ From 6748a7e82651bc018ec99fbf239936c3010e7c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 27 Sep 2023 11:21:23 -0700 Subject: [PATCH 16/20] add tests --- runtime/tests/interpreter/interpreter_test.go | 310 +++++++++++++----- 1 file changed, 235 insertions(+), 75 deletions(-) diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 8e6529abcc..595ae93000 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -73,6 +73,65 @@ func parseCheckAndInterpretWithOptions( return parseCheckAndInterpretWithOptionsAndMemoryMetering(t, code, options, nil) } +func parseCheckAndInterpretWithLogs( + tb testing.TB, + code string, +) ( + inter *interpreter.Interpreter, + getLogs func() []string, + err error, +) { + var logs []string + + logFunction := stdlib.NewStandardLibraryFunction( + "log", + &sema.FunctionType{ + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "value", + TypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), + }, + }, + ReturnTypeAnnotation: sema.NewTypeAnnotation( + sema.VoidType, + ), + }, + ``, + func(invocation interpreter.Invocation) interpreter.Value { + message := invocation.Arguments[0].String() + logs = append(logs, message) + return interpreter.Void + }, + ) + + baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) + baseValueActivation.DeclareValue(logFunction) + + baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) + interpreter.Declare(baseActivation, logFunction) + + result, err := parseCheckAndInterpretWithOptions( + tb, + code, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + BaseActivation: baseActivation, + }, + CheckerConfig: &sema.Config{ + BaseValueActivation: baseValueActivation, + }, + HandleCheckerError: nil, + }, + ) + + getLogs = func() []string { + return logs + } + + return result, getLogs, err +} + func parseCheckAndInterpretWithMemoryMetering( t testing.TB, code string, @@ -9969,90 +10028,49 @@ func TestInterpretNestedDestroy(t *testing.T) { t.Parallel() - var logs []string - - logFunction := stdlib.NewStandardLibraryFunction( - "log", - &sema.FunctionType{ - Parameters: []sema.Parameter{ - { - Label: sema.ArgumentLabelNotRequired, - Identifier: "value", - TypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), - }, - }, - ReturnTypeAnnotation: sema.NewTypeAnnotation( - sema.VoidType, - ), - }, - ``, - func(invocation interpreter.Invocation) interpreter.Value { - message := invocation.Arguments[0].String() - logs = append(logs, message) - return interpreter.Void - }, - ) - - baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(logFunction) - - baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) - interpreter.Declare(baseActivation, logFunction) - - inter, err := parseCheckAndInterpretWithOptions(t, - ` - resource B { - let id: Int + inter, getLogs, err := parseCheckAndInterpretWithLogs(t, ` + resource B { + let id: Int - init(_ id: Int){ - self.id = id - } + init(_ id: Int){ + self.id = id + } - destroy(){ - log("destroying B with id:") - log(self.id) - } + destroy(){ + log("destroying B with id:") + log(self.id) } + } - resource A { - let id: Int - let bs: @[B] + resource A { + let id: Int + let bs: @[B] - init(_ id: Int){ - self.id = id - self.bs <- [] - } + init(_ id: Int){ + self.id = id + self.bs <- [] + } - fun add(_ b: @B){ - self.bs.append(<-b) - } + fun add(_ b: @B){ + self.bs.append(<-b) + } - destroy() { - log("destroying A with id:") - log(self.id) - destroy self.bs - } + destroy() { + log("destroying A with id:") + log(self.id) + destroy self.bs } + } - fun test() { - let a <- create A(1) - a.add(<- create B(2)) - a.add(<- create B(3)) - a.add(<- create B(4)) + fun test() { + let a <- create A(1) + a.add(<- create B(2)) + a.add(<- create B(3)) + a.add(<- create B(4)) - destroy a - } - `, - ParseCheckAndInterpretOptions{ - Config: &interpreter.Config{ - BaseActivation: baseActivation, - }, - CheckerConfig: &sema.Config{ - BaseValueActivation: baseValueActivation, - }, - HandleCheckerError: nil, - }, - ) + destroy a + } + `) require.NoError(t, err) value, err := inter.Invoke("test") @@ -10076,7 +10094,7 @@ func TestInterpretNestedDestroy(t *testing.T) { `"destroying B with id:"`, "4", }, - logs, + getLogs(), ) } @@ -12179,3 +12197,145 @@ func TestInterpretSwapInSameArray(t *testing.T) { ) }) } + +func TestInterpretSwapDictionaryKeysWithSideEffects(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + t.Parallel() + + inter, getLogs, err := parseCheckAndInterpretWithLogs(t, ` + let xs: [{Int: String}] = [{2: "x"}, {3: "y"}] + + fun a(): Int { + log("a") + return 0 + } + + fun b(): Int { + log("b") + return 2 + } + + fun c(): Int { + log("c") + return 1 + } + + fun d(): Int { + log("d") + return 3 + } + + fun test() { + log(xs) + xs[a()][b()] <-> xs[c()][d()] + log(xs) + } + `) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + assert.Equal(t, + []string{ + `[{2: "x"}, {3: "y"}]`, + `"a"`, + `"b"`, + `"c"`, + `"d"`, + `[{2: "y"}, {3: "x"}]`, + }, + getLogs(), + ) + + }) + + t.Run("resources", func(t *testing.T) { + t.Parallel() + + inter, getLogs, err := parseCheckAndInterpretWithLogs(t, ` + resource Resource { + var value: Int + + init(_ value: Int) { + log( + "Creating resource with UUID " + .concat(self.uuid.toString()) + .concat(" and value ") + .concat(value.toString()) + ) + self.value = value + } + + destroy() { + log( + "Destroying resource with UUID " + .concat(self.uuid.toString()) + .concat(" and value ") + .concat(self.value.toString()) + ) + } + } + + resource ResourceLoser { + var dict: @{Int: Resource} + var toBeLost: @Resource + + init(_ victim: @Resource) { + self.dict <- {1: <- create Resource(2)} + + self.toBeLost <- victim + + // Magic happens during the swap below. + self.dict[1] <-> self.dict[self.shenanigans()] + } + + fun shenanigans(): Int { + var d <- create Resource(3) + + self.toBeLost <-> d + + // This takes advantage of the fact that self.dict[1] has been + // temporarily removed at the point of the swap when this gets called + // We take advantage of this window of opportunity to + // insert the "to-be-lost" resource in its place. The swap implementation + // will blindly overwrite it. + var old <- self.dict.insert(key: 1, <- d) + + // "old" will be nil here thanks to the removal done by the swap + // implementation. We have to destroy it to please sema checker. + destroy old + + return 1 + } + + destroy() { + destroy self.dict + destroy self.toBeLost + } + } + + fun test() { + destroy <- create ResourceLoser(<- create Resource(1)) + } + `) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + assert.Equal(t, + []string{ + `"Creating resource with UUID 1 and value 1"`, + `"Creating resource with UUID 3 and value 2"`, + `"Creating resource with UUID 4 and value 3"`, + `"Destroying resource with UUID 3 and value 2"`, + `"Destroying resource with UUID 1 and value 1"`, + `"Destroying resource with UUID 4 and value 3"`, + }, + getLogs(), + ) + }) +} From 6c6f491a98ec355cd5f62fdee0104b82f0583e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 2 Oct 2023 12:43:55 -0700 Subject: [PATCH 17/20] lint --- runtime/tests/interpreter/interpreter_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 595ae93000..6f6157bab5 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12234,6 +12234,7 @@ func TestInterpretSwapDictionaryKeysWithSideEffects(t *testing.T) { log(xs) } `) + require.NoError(t, err) _, err = inter.Invoke("test") require.NoError(t, err) From 96e74ddb652958d8ae1f82a49c77124baa3d7afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 2 Oct 2023 11:11:50 -0700 Subject: [PATCH 18/20] prevent re-deploy in same transaction --- runtime/contract_test.go | 24 ++++--- runtime/contract_update_test.go | 108 +++++++++++++++++++++++++++++++- runtime/environment.go | 4 ++ runtime/stdlib/account.go | 11 +++- runtime/storage.go | 11 ++++ 5 files changed, 142 insertions(+), 16 deletions(-) diff --git a/runtime/contract_test.go b/runtime/contract_test.go index 21d2d08540..f242fb59ac 100644 --- a/runtime/contract_test.go +++ b/runtime/contract_test.go @@ -307,6 +307,8 @@ func TestRuntimeContract(t *testing.T) { ) RequireError(t, err) + require.ErrorContains(t, err, "cannot overwrite existing contract") + // the deployed code should not have been updated, // and no events should have been emitted, // as the deployment should fail @@ -448,6 +450,8 @@ func TestRuntimeContract(t *testing.T) { } else { RequireError(t, err) + require.ErrorContains(t, err, "cannot overwrite existing contract") + require.Empty(t, deployedCode) require.Empty(t, events) require.Empty(t, loggedMessages) @@ -473,9 +477,11 @@ func TestRuntimeContract(t *testing.T) { Location: nextTransactionLocation(), }, ) - require.NoError(t, err) + RequireError(t, err) - require.Equal(t, []byte(tc.code2), deployedCode) + require.ErrorContains(t, err, "cannot overwrite existing contract") + + require.Empty(t, deployedCode) require.Equal(t, []string{ @@ -484,20 +490,18 @@ func TestRuntimeContract(t *testing.T) { `"Test"`, codeArrayString, `nil`, - `"Test"`, - code2ArrayString, - `"Test"`, - code2ArrayString, }, loggedMessages, ) - require.Len(t, events, 2) - assert.EqualValues(t, stdlib.AccountContractRemovedEventType.ID(), events[0].Type().ID()) - assert.EqualValues(t, stdlib.AccountContractAddedEventType.ID(), events[1].Type().ID()) + require.Len(t, events, 1) + assert.EqualValues(t, + stdlib.AccountContractRemovedEventType.ID(), + events[0].Type().ID(), + ) contractValueExists := getContractValueExists() - + // contract still exists (from previous transaction), if not interface if tc.isInterface { require.False(t, contractValueExists) } else { diff --git a/runtime/contract_update_test.go b/runtime/contract_update_test.go index 0bb7509449..b65ee3c662 100644 --- a/runtime/contract_update_test.go +++ b/runtime/contract_update_test.go @@ -29,7 +29,7 @@ import ( "github.com/onflow/cadence/encoding/json" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" - "github.com/onflow/cadence/runtime/tests/utils" + . "github.com/onflow/cadence/runtime/tests/utils" ) func TestContractUpdateWithDependencies(t *testing.T) { @@ -142,7 +142,7 @@ func TestContractUpdateWithDependencies(t *testing.T) { err := runtime.ExecuteTransaction( Script{ - Source: utils.DeploymentTransaction( + Source: DeploymentTransaction( "Foo", []byte(fooContractV1), ), @@ -163,7 +163,7 @@ func TestContractUpdateWithDependencies(t *testing.T) { err = runtime.ExecuteTransaction( Script{ - Source: utils.DeploymentTransaction( + Source: DeploymentTransaction( "Bar", []byte(barContractV1), ), @@ -235,3 +235,105 @@ func TestContractUpdateWithDependencies(t *testing.T) { ) require.NoError(t, err) } + +func TestRuntimeInvalidContractRedeploy(t *testing.T) { + + t.Parallel() + + foo1 := []byte(` + access(all) + contract Foo { + + access(all) + resource R { + + access(all) + var x: Int + + init() { + self.x = 0 + } + } + + access(all) + fun createR(): @R { + return <-create R() + } + } + `) + + foo2 := []byte(` + access(all) + contract Foo { + + access(all) + struct R { + access(all) + var x: Int + + init() { + self.x = 0 + } + } + } + `) + + tx := []byte(` + transaction(foo1: String, foo2: String) { + prepare(signer: AuthAccount) { + signer.contracts.add(name: "Foo", code: foo1.utf8) + signer.contracts.add(name: "Foo", code: foo2.utf8) + } + } + `) + + runtime := newTestInterpreterRuntime() + runtime.defaultConfig.AtreeValidationEnabled = false + + address := common.MustBytesToAddress([]byte{0x1}) + + var events []cadence.Event + + runtimeInterface := &testRuntimeInterface{ + storage: newTestLedger(nil, nil), + getSigningAccounts: func() ([]Address, error) { + return []Address{address}, nil + }, + getAccountContractCode: func(location common.AddressLocation) ([]byte, error) { + return nil, nil + }, + resolveLocation: singleIdentifierLocationResolver(t), + updateAccountContractCode: func(location common.AddressLocation, code []byte) error { + // "delay" + return nil + }, + emitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + decodeArgument: func(b []byte, t cadence.Type) (value cadence.Value, err error) { + return json.Decode(nil, b) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + // Deploy + + err := runtime.ExecuteTransaction( + Script{ + Source: tx, + Arguments: encodeArgs([]cadence.Value{ + cadence.String(foo1), + cadence.String(foo2), + }), + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + RequireError(t, err) + + require.ErrorContains(t, err, "cannot overwrite existing contract") +} diff --git a/runtime/environment.go b/runtime/environment.go index daa4e5440f..df4b5e8f7b 100644 --- a/runtime/environment.go +++ b/runtime/environment.go @@ -340,6 +340,10 @@ func (e *interpreterEnvironment) RecordContractUpdate( e.storage.recordContractUpdate(location, contractValue) } +func (e *interpreterEnvironment) ContractUpdateRecorded(location common.AddressLocation) bool { + return e.storage.contractUpdateRecorded(location) +} + func (e *interpreterEnvironment) TemporarilyRecordCode(location common.AddressLocation, code []byte) { e.codesAndPrograms.setCode(location, code) } diff --git a/runtime/stdlib/account.go b/runtime/stdlib/account.go index 1577c34c9e..2437f40be7 100644 --- a/runtime/stdlib/account.go +++ b/runtime/stdlib/account.go @@ -1431,7 +1431,11 @@ type AccountContractAdditionHandler interface { ) (*interpreter.Program, error) // UpdateAccountContractCode updates the code associated with an account contract. UpdateAccountContractCode(location common.AddressLocation, code []byte) error - RecordContractUpdate(location common.AddressLocation, value *interpreter.CompositeValue) + RecordContractUpdate( + location common.AddressLocation, + value *interpreter.CompositeValue, + ) + ContractUpdateRecorded(location common.AddressLocation) bool InterpretContract( location common.AddressLocation, program *interpreter.Program, @@ -1514,9 +1518,10 @@ func newAuthAccountContractsChangeFunction( } else { // We are adding a new contract. - // Ensure that no contract/contract interface with the given name exists already + // Ensure that no contract/contract interface with the given name exists already, + // and no contract deploy or update was recorded before - if len(existingCode) > 0 { + if len(existingCode) > 0 || handler.ContractUpdateRecorded(location) { panic(errors.NewDefaultUserError( "cannot overwrite existing contract with name %q in account %s", contractName, diff --git a/runtime/storage.go b/runtime/storage.go index dd4618a747..965e3c6720 100644 --- a/runtime/storage.go +++ b/runtime/storage.go @@ -175,6 +175,17 @@ func (s *Storage) recordContractUpdate( s.contractUpdates.Set(key, contractValue) } +func (s *Storage) contractUpdateRecorded( + location common.AddressLocation, +) bool { + if s.contractUpdates == nil { + return false + } + + key := interpreter.NewStorageKey(s.memoryGauge, location.Address, location.Name) + return s.contractUpdates.Contains(key) +} + type ContractUpdate struct { ContractValue *interpreter.CompositeValue Key interpreter.StorageKey From ce7bc999cb09592a2f024bad38e010e968c544cf Mon Sep 17 00:00:00 2001 From: turbolent Date: Thu, 5 Oct 2023 20:56:11 +0000 Subject: [PATCH 19/20] v0.42.0 --- npm-packages/cadence-parser/package.json | 2 +- version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/npm-packages/cadence-parser/package.json b/npm-packages/cadence-parser/package.json index 4bc2b2b221..73b0b9de55 100644 --- a/npm-packages/cadence-parser/package.json +++ b/npm-packages/cadence-parser/package.json @@ -1,6 +1,6 @@ { "name": "@onflow/cadence-parser", - "version": "0.41.1", + "version": "0.42.0", "description": "The Cadence parser", "homepage": "https://github.com/onflow/cadence", "repository": { diff --git a/version.go b/version.go index e9056ef2c4..9ee7be9668 100644 --- a/version.go +++ b/version.go @@ -21,4 +21,4 @@ package cadence -const Version = "v0.41.1" +const Version = "v0.42.0" From cd98a9656a58c17682277511bef6f443892b9d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 5 Oct 2023 15:49:47 -0700 Subject: [PATCH 20/20] add new file --- runtime/interpreter/value_placeholder.go | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 runtime/interpreter/value_placeholder.go diff --git a/runtime/interpreter/value_placeholder.go b/runtime/interpreter/value_placeholder.go new file mode 100644 index 0000000000..6cc6184852 --- /dev/null +++ b/runtime/interpreter/value_placeholder.go @@ -0,0 +1,105 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * 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 interpreter + +import ( + "github.com/onflow/atree" + + "github.com/onflow/cadence/runtime/common" +) + +// placeholderValue +type placeholderValue struct{} + +var placeholder Value = placeholderValue{} + +var _ Value = placeholderValue{} + +func (placeholderValue) isValue() {} + +func (f placeholderValue) String() string { + return f.RecursiveString(SeenReferences{}) +} + +func (f placeholderValue) RecursiveString(_ SeenReferences) string { + return "" +} + +func (f placeholderValue) MeteredString(_ common.MemoryGauge, _ SeenReferences) string { + return "" +} + +func (f placeholderValue) Accept(_ *Interpreter, _ Visitor) { + // NO-OP +} + +func (f placeholderValue) Walk(_ *Interpreter, _ func(Value)) { + // NO-OP +} + +func (f placeholderValue) StaticType(_ *Interpreter) StaticType { + return PrimitiveStaticTypeNever +} + +func (placeholderValue) IsImportable(_ *Interpreter) bool { + return false +} + +func (f placeholderValue) ConformsToStaticType( + _ *Interpreter, + _ LocationRange, + _ TypeConformanceResults, +) bool { + return true +} + +func (f placeholderValue) Storable(_ atree.SlabStorage, _ atree.Address, _ uint64) (atree.Storable, error) { + return NonStorable{Value: f}, nil +} + +func (placeholderValue) NeedsStoreTo(_ atree.Address) bool { + return false +} + +func (placeholderValue) IsResourceKinded(_ *Interpreter) bool { + return false +} + +func (f placeholderValue) Transfer( + interpreter *Interpreter, + _ LocationRange, + _ atree.Address, + remove bool, + storable atree.Storable, + _ map[atree.StorageID]struct{}, +) Value { + // TODO: actually not needed, value is not storable + if remove { + interpreter.RemoveReferencedSlab(storable) + } + return f +} + +func (f placeholderValue) Clone(_ *Interpreter) Value { + return f +} + +func (placeholderValue) DeepRemove(_ *Interpreter) { + // NO-OP +}