From fc8b69063a39da6901ec88d8f499e187126a6c2c Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 6 Jun 2023 17:30:33 +0300 Subject: [PATCH] Fetch events from Blockchain and add support for importing on-chain types --- test/emulator_backend.go | 66 +++++++++++++++- test/test_framework_test.go | 147 ++++++++++++++++++++++++++++++++++++ test/test_runner.go | 56 +++++++++++++- 3 files changed, 264 insertions(+), 5 deletions(-) diff --git a/test/emulator_backend.go b/test/emulator_backend.go index d3c6afaf..50901e72 100644 --- a/test/emulator_backend.go +++ b/test/emulator_backend.go @@ -565,9 +565,69 @@ func (e *EmulatorBackend) Reset() { e.blockOffset = 0 } -func (e *EmulatorBackend) Events(_ *interpreter.Interpreter, _ interpreter.StaticType) interpreter.Value { - // TODO: - return nil +// Events returns all the emitted events up until the latest block, +// optionally filtered by event type. +func (e *EmulatorBackend) Events( + inter *interpreter.Interpreter, + eventType interpreter.StaticType, +) interpreter.Value { + latestBlock, err := e.blockchain.GetLatestBlock() + if err != nil { + panic(err) + } + + latestBlockHeight := latestBlock.Header.Height + height := uint64(0) + values := make([]interpreter.Value, 0) + evtType, _ := eventType.(interpreter.CompositeStaticType) + + for height <= latestBlockHeight { + events, err := e.blockchain.GetEventsByHeight( + height, + evtType.String(), + ) + if err != nil { + panic(err) + } + sdkEvents, err := convert.FlowEventsToSDK(events) + if err != nil { + panic(err) + } + for _, event := range sdkEvents { + if strings.Contains(event.Type, "flow.") { + continue + } + value, err := runtime.ImportValue( + inter, + interpreter.EmptyLocationRange, + e.stdlibHandler, + event.Value, + nil, + ) + if err != nil { + panic(err) + } + values = append(values, value) + + } + height += 1 + } + + arrayType := interpreter.NewVariableSizedStaticType( + inter, + interpreter.NewPrimitiveStaticType( + inter, + interpreter.PrimitiveStaticTypeAnyStruct, + ), + ) + + return interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + values..., + ) } // excludeCommonLocations excludes the common contracts from appearing diff --git a/test/test_framework_test.go b/test/test_framework_test.go index 321e4181..4e717caf 100644 --- a/test/test_framework_test.go +++ b/test/test_framework_test.go @@ -3687,3 +3687,150 @@ func TestRetrieveEmptyLogsFromIntegrationTests(t *testing.T) { require.NoError(t, result.Error) } } + +func TestGetEventsFromIntegrationTests(t *testing.T) { + t.Parallel() + + const contractCode = ` + pub contract FooContract { + pub let specialNumbers: {Int: String} + + pub event ContractInitialized() + pub event NumberAdded(n: Int, trait: String) + + init() { + self.specialNumbers = {1729: "Harshad"} + emit ContractInitialized() + } + + pub fun addSpecialNumber(_ n: Int, _ trait: String) { + self.specialNumbers[n] = trait + emit NumberAdded(n: n, trait: trait) + } + + pub fun getIntegerTrait(_ n: Int): String { + if self.specialNumbers.containsKey(n) { + return self.specialNumbers[n]! + } + + return "Unknown" + } + } + ` + + const scriptCode = ` + import FooContract from "../contracts/FooContract.cdc" + + pub fun main(): Bool { + // Act + let trait = FooContract.getIntegerTrait(1729) + + // Assert + assert(trait == "Harshad") + return true + } + ` + + const testCode = ` + import Test + + pub let blockchain = Test.newEmulatorBlockchain() + pub let account = blockchain.createAccount() + + pub fun setup() { + let contractCode = Test.readFile("../contracts/FooContract.cdc") + let err = blockchain.deployContract( + name: "FooContract", + code: contractCode, + account: account, + arguments: [] + ) + + if err != nil { + panic(err!.message) + } + + blockchain.useConfiguration(Test.Configuration({ + "../contracts/FooContract.cdc": account.address + })) + } + + pub fun testGetIntegerTrait() { + let script = Test.readFile("../scripts/get_integer_traits.cdc") + let result = blockchain.executeScript(script, []) + + if result.status != Test.ResultStatus.succeeded { + panic(result.error!.message) + } + assert(result.returnValue! as! Bool) + + let typ = CompositeType("A.01cf0e2f2f715450.FooContract.ContractInitialized")! + let events = blockchain.eventsOfType(typ) + Test.assert(events.length == 1) + } + + pub fun testAddSpecialNumber() { + let code = Test.readFile("../transactions/add_special_number.cdc") + let tx = Test.Transaction( + code: code, + authorizers: [account.address], + signers: [account], + arguments: [78557, "Sierpinski"] + ) + + let result = blockchain.executeTransaction(tx) + assert(result.status == Test.ResultStatus.succeeded) + + let typ = CompositeType("A.01cf0e2f2f715450.FooContract.NumberAdded")! + let events = blockchain.eventsOfType(typ) + Test.assert(events.length == 1) + + let evts = blockchain.events() + log(evts[9]) + Test.assert(evts.length == 10) + } + ` + + const transactionCode = ` + import FooContract from "../contracts/FooContract.cdc" + + transaction(number: Int, trait: String) { + prepare(acct: AuthAccount) {} + + execute { + // Act + FooContract.addSpecialNumber(number, trait) + + // Assert + assert(trait == FooContract.getIntegerTrait(number)) + } + } + ` + + fileResolver := func(path string) (string, error) { + switch path { + case "../contracts/FooContract.cdc": + return contractCode, nil + case "../scripts/get_integer_traits.cdc": + return scriptCode, nil + case "../transactions/add_special_number.cdc": + return transactionCode, nil + default: + return "", fmt.Errorf("cannot find import location: %s", path) + } + } + + importResolver := func(location common.Location) (string, error) { + return "", nil + } + + runner := NewTestRunner(). + WithFileResolver(fileResolver). + WithImportResolver(importResolver) + + results, err := runner.RunTests(testCode) + require.NoError(t, err) + for _, result := range results { + require.NoError(t, result.Error) + } +} diff --git a/test/test_runner.go b/test/test_runner.go index eab14753..f913deb6 100644 --- a/test/test_runner.go +++ b/test/test_runner.go @@ -30,6 +30,7 @@ import ( "github.com/onflow/flow-go/engine/execution/testutil" "github.com/onflow/flow-go/fvm" "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/model/flow" "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/ast" @@ -131,6 +132,8 @@ type TestRunner struct { // the script environment, in order to aggregate and expose // log messages from test cases and contracts. logCollection *LogCollectionHook + + backend *EmulatorBackend } func NewTestRunner() *TestRunner { @@ -478,7 +481,7 @@ func (r *TestRunner) interpreterContractValueHandler( return contract case stdlib.TestContractLocation: - testFramework := NewEmulatorBackend( + r.backend = NewEmulatorBackend( r.fileResolver, stdlibHandler, r.coverageReport, @@ -486,7 +489,7 @@ func (r *TestRunner) interpreterContractValueHandler( contract, err := stdlib.GetTestContractType(). NewTestContract( inter, - testFramework, + r.backend, constructorGenerator(common.Address{}), invocationRange, ) @@ -529,6 +532,55 @@ func (r *TestRunner) interpreterImportHandler(ctx runtime.Context) interpreter.I } default: + addressLocation, ok := location.(common.AddressLocation) + if ok { + account, _ := r.backend.blockchain.GetAccount( + flow.Address(addressLocation.Address), + ) + programCode := account.Contracts[addressLocation.Name] + env := runtime.NewBaseInterpreterEnvironment(runtime.Config{}) + newCtx := runtime.Context{ + Interface: newScriptEnvironment(zerolog.Nop()), + Location: addressLocation, + Environment: env, + } + env.CheckerConfig.ImportHandler = func( + checker *sema.Checker, + importedLocation common.Location, + importRange ast.Range, + ) (sema.Import, error) { + addressLoc, ok := importedLocation.(common.AddressLocation) + if !ok { + return nil, fmt.Errorf("no address location given") + } + account, _ := r.backend.blockchain.GetAccount( + flow.Address(addressLoc.Address), + ) + programCode := account.Contracts[addressLoc.Name] + program, err := env.ParseAndCheckProgram( + programCode, addressLoc, true, + ) + if err != nil { + panic(err) + } + + return sema.ElaborationImport{ + Elaboration: program.Elaboration, + }, nil + } + program, err := r.testRuntime.ParseAndCheckProgram(programCode, newCtx) + if err != nil { + panic(err) + } + + subInterpreter, err := inter.NewSubInterpreter(program, addressLocation) + if err != nil { + panic(err) + } + return interpreter.InterpreterImport{ + Interpreter: subInterpreter, + } + } importedProgram, importedElaboration, err := r.parseAndCheckImport(location, ctx) if err != nil { panic(err)