Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve Blockchain events #113

Merged
merged 1 commit into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions test/emulator_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions test/test_framework_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
56 changes: 54 additions & 2 deletions test/test_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Copy link
Member

@SupunS SupunS Jun 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m-Peter The backend here might be a problem similar to #114 (comment). e.g.: Creating two blockchains, and returning events from both.
Would be good to double-check. Maybe add a test case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know 😅 I was caught off-guard, I didn't think it would be merged today 😇
However, I did check the previous comment. Why do you think that it would be a problem? The backend is basically a singleton, it gets instantiated every time the import Test declaration is encountered, without actually calling Test.newEmulatorBlockchain. And the way the wiring is done in flow-cli, https://github.com/onflow/flow-cli/blob/master/internal/test/test.go#L120-L125, a new TestRunner is created per test file. Can you share a test case where a erroneous side-effect can manifest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I just saw that you wrote Maybe add a test case? 😅 I will certainly add tests.

Copy link
Member

@SupunS SupunS Jun 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm also not certain whether that is actually a problem, but rather a hunch that it may potentially cause problems.

A good example/test-case would be, in the same test-script:

  • Create blockchain, blockchain1.
    • Deploy a contract Foo to the blockchain1, that emits an event Foo.Event.
    • Run a script/transaction that triggers the emitting of event Foo.Event.
    • Then get all events from blockchain1.
  • Create another blockchain, blockchain2.
    • Deploy a contract Bar to the blockchain2, that emits an event Bar.Event.
    • Run a script/transaction that triggers the emitting of event Bar.Event.
    • Then get all events from blockchain2.

If this works fine, I guess we should be good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, I will add a case like that!

}

func NewTestRunner() *TestRunner {
Expand Down Expand Up @@ -478,15 +481,15 @@ func (r *TestRunner) interpreterContractValueHandler(
return contract

case stdlib.TestContractLocation:
testFramework := NewEmulatorBackend(
r.backend = NewEmulatorBackend(
r.fileResolver,
stdlibHandler,
r.coverageReport,
)
contract, err := stdlib.GetTestContractType().
NewTestContract(
inter,
testFramework,
r.backend,
constructorGenerator(common.Address{}),
invocationRange,
)
Expand Down Expand Up @@ -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)
Expand Down