Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
28 changes: 18 additions & 10 deletions docs/tutorials/morpheusvm/4_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,30 +362,34 @@ registry of all the tests that we want to run against our VM.
Afterwards, we have the following snippet:

```go
registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork) {
registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork, authFactories ...chain.AuthFactory) {

})
}, 1000)
```

Here, we are adding a test to `TestRegistry`. However, we're
Here, we are adding a test to `TestRegistry`, requesting an authFactory to be funded with 1000 tokens. However, we're
missing the test itself. In short, here's what we want to do in
our testing logic:

- Setup necessary values
- Setup necessary values & Check the funds of the requested authFactory
- Create our test TX
- Send our TX
- Require that our TX is sent and that the outputs are as expected
- Check the receiver has received the funds

Focusing on the first step, we can write the following inside the anonymous
function:

```go
require := require.New(t)
other, err := ed25519.GeneratePrivateKey()
require.NoError(err)
toAddress := auth.NewED25519Address(other.PublicKey())

ctx := context.Background()
targetFactory := authFactories[0]
authFactory := tn.Configuration().AuthFactories()[0]

client := jsonrpc.NewJSONRPCClient(tn.URIs()[0])
balance, err := client.GetBalance(ctx, targetFactory.Address())
require.NoError(err)
require.Equal(uint64(1000), balance)
```

Next, we'll create our test transaction. In short, we'll want to send a value of
Expand All @@ -401,12 +405,13 @@ Next, we'll create our test transaction. In short, we'll want to send a value of
require.NoError(err)
```

Finally, we'll want to send our TX and do the checks mentioned in the last step.
Finally, we'll want to send our TX, check that the Tx has been executed and receiver has received the amount of token.
This step will consist of the following:

- Creating a context with a deadline of 2 seconds
- If the test takes longer than 2 seconds, it will fail
- Calling `ConfirmTxs` with our TX being passed in
- Requesting the balance

The function `ConfirmTXs` is useful as it checks that our TX was
sent and that, if finalized, our transaction has the expected outputs. We have
Expand All @@ -415,8 +420,11 @@ the following:
```go
timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
defer timeoutCtxFnc()

require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx}))

balance, err = client.GetBalance(context.Background(), targetFactory.Address())
require.NoError(err)
require.Equal(uint64(1001), balance)
```

## Registering our Tests
Expand Down
51 changes: 29 additions & 22 deletions examples/morpheusvm/tests/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (

"github.com/stretchr/testify/require"

"github.com/ava-labs/hypersdk/auth"
"github.com/ava-labs/hypersdk/api/jsonrpc"
"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/examples/morpheusvm/actions"
"github.com/ava-labs/hypersdk/tests/registry"

Expand All @@ -24,23 +23,31 @@ import (
// ref https://onsi.github.io/ginkgo/#mental-model-how-ginkgo-traverses-the-spec-hierarchy
var TestsRegistry = &registry.Registry{}

var _ = registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork) {
require := require.New(t)
other, err := ed25519.GeneratePrivateKey()
require.NoError(err)
toAddress := auth.NewED25519Address(other.PublicKey())

authFactory := tn.Configuration().AuthFactories()[0]
tx, err := tn.GenerateTx(context.Background(), []chain.Action{&actions.Transfer{
To: toAddress,
Value: 1,
}},
authFactory,
)
require.NoError(err)

timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
defer timeoutCtxFnc()

require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx}))
})
var _ = registry.Register(TestsRegistry, "Transfer Transaction",
func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork, authFactories ...chain.AuthFactory) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we replace ...chain.AuthFactory with []chain.AuthFactory ? I don't think we need the syntactic sugar here

require := require.New(t)
ctx := context.Background()
targetFactory := authFactories[0]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add a require check that the length of authFactories is 1 first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add a decorator function to check the authFactories length and fail fast, so the user can safely use the array

authFactory := tn.Configuration().AuthFactories()[0]

client := jsonrpc.NewJSONRPCClient(tn.URIs()[0])
balance, err := client.GetBalance(ctx, targetFactory.Address())
require.NoError(err)
require.Equal(uint64(1000), balance)

tx, err := tn.GenerateTx(ctx, []chain.Action{&actions.Transfer{
To: targetFactory.Address(),
Value: 1,
}},
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the flow should be authFactories are assumed to be pre-funded with a specific amount (1000) in this case, and the test flow should be:

  1. confirm the provided balance is as expected
  2. generate and confirm transaction from the provided key (rather than the auth factory from the configuration)
  3. confirm balance updates after confirming the tx

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed the test to have a pre-funded authFactory sending to another pre-funded authFactory 👍
I thought it was interesting to use both pre-funded authFactory and config authFactory. So the users see the two possibilities.

authFactory,
)
require.NoError(err)

timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
defer timeoutCtxFnc()
require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx}))

balance, err = client.GetBalance(ctx, targetFactory.Address())
require.NoError(err)
require.Equal(uint64(1001), balance)
}, 1000)
16 changes: 14 additions & 2 deletions examples/morpheusvm/tests/workload/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/examples/morpheusvm/consts"
"github.com/ava-labs/hypersdk/examples/morpheusvm/tests"
"github.com/ava-labs/hypersdk/examples/morpheusvm/vm"
"github.com/ava-labs/hypersdk/fees"
"github.com/ava-labs/hypersdk/genesis"
Expand All @@ -32,7 +33,7 @@ var ed25519HexKeys = []string{
"8a7be2e0c9a2d09ac2861c34326d6fe5a461d920ba9c2b345ae28e603d517df148735063f8d5d8ba79ea4668358943e5c80bc09e9b2b9a15b5b15db6c1862e88", //nolint:lll
}

func newGenesis(authFactories []chain.AuthFactory, minBlockGap time.Duration) *genesis.DefaultGenesis {
func newGenesis(authFactories []chain.AuthFactory, testsLocalAllocations []*genesis.CustomAllocation, minBlockGap time.Duration) *genesis.DefaultGenesis {
// allocate the initial balance to the addresses
customAllocs := make([]*genesis.CustomAllocation, 0, len(authFactories))
for _, authFactory := range authFactories {
Expand All @@ -41,6 +42,7 @@ func newGenesis(authFactories []chain.AuthFactory, minBlockGap time.Duration) *g
Balance: InitialBalance,
})
}
customAllocs = append(customAllocs, testsLocalAllocations...)

genesis := genesis.NewDefaultGenesis(customAllocs)

Expand Down Expand Up @@ -72,7 +74,17 @@ func newDefaultAuthFactories() []chain.AuthFactory {

func NewTestNetworkConfig(minBlockGap time.Duration) (workload.DefaultTestNetworkConfiguration, error) {
keys := newDefaultAuthFactories()
genesis := newGenesis(keys, minBlockGap)
testsLocalAllocations, err := tests.TestsRegistry.GenerateCustomAllocations(func() (chain.AuthFactory, error) {
privateKey, err := ed25519.GeneratePrivateKey()
if err != nil {
return nil, err
}
return auth.NewED25519Factory(privateKey), nil
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The lambda function could potentially be moved in auth package.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense

Copy link
Collaborator

Choose a reason for hiding this comment

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

This control flow is a bit confusing. This code is used to generate the genesis, but it's not clear from here where it's going to be used even though it's coupled to the tests registry.

We are using the test registry in the integration and e2e environments where we define two entry points. This is also where we invoke NewTestNetworkConfig.

Could we change this function to take in a slice of genesis allocations as an argument (in addition to minBlockGap)?

Then we can import the test registry and use the registered custom allocations in the entry point in both examples/morpheusvm/tests/integration/integration_test.go and examples/morpheusvm/tests/e2e/e2e_test.go`

if err != nil {
return workload.DefaultTestNetworkConfiguration{}, err
}
genesis := newGenesis(keys, testsLocalAllocations, minBlockGap)
genesisBytes, err := json.Marshal(genesis)
if err != nil {
return workload.DefaultTestNetworkConfiguration{}, err
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ var _ = ginkgo.Describe("[Custom VM Tests]", ginkgo.Serial, func() {
for _, test := range testRegistry.List() {
ginkgo.It(test.Name, func() {
testNetwork := NewNetwork(tc)
test.Fnc(ginkgo.GinkgoT(), testNetwork)
test.Fnc(ginkgo.GinkgoT(), testNetwork, test.AuthFactories...)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() {
for _, test := range testRegistry.List() {
ginkgo.It(fmt.Sprintf("Custom VM Test '%s'", test.Name), func() {
require.NoError(testNetwork.SynchronizeNetwork(context.Background()))
test.Fnc(ginkgo.GinkgoT(), testNetwork)
test.Fnc(ginkgo.GinkgoT(), testNetwork, test.AuthFactories...)
})
}
}
Expand Down
46 changes: 36 additions & 10 deletions tests/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,63 @@ package registry
import (
"github.com/onsi/ginkgo/v2"

"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/genesis"
"github.com/ava-labs/hypersdk/tests/workload"
)

type TestFunc func(t ginkgo.FullGinkgoTInterface, tn workload.TestNetwork)
type TestFunc func(t ginkgo.FullGinkgoTInterface, tn workload.TestNetwork, authFactories ...chain.AuthFactory)

type namedTest struct {
Fnc TestFunc
Name string
Fnc TestFunc
Name string
AuthFactories []chain.AuthFactory
requestedBalances []uint64
}
type Registry struct {
tests []namedTest
tests []*namedTest
}

func (r *Registry) Add(name string, f TestFunc) {
r.tests = append(r.tests, namedTest{Fnc: f, Name: name})
func (r *Registry) Add(name string, f TestFunc, requestedBalances ...uint64) {
r.tests = append(r.tests, &namedTest{Fnc: f, Name: name, requestedBalances: requestedBalances})
}

func (r *Registry) List() []namedTest {
func (r *Registry) List() []*namedTest {
if r == nil {
return []namedTest{}
return []*namedTest{}
}
return r.tests
}

func (r *Registry) GenerateCustomAllocations(generateAuthFactory func() (chain.AuthFactory, error)) ([]*genesis.CustomAllocation, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The authFactories creation is delayed because when registering the tests, we don't know which private key type to use.

requestedAllocations := make([]*genesis.CustomAllocation, 0)
for index, test := range r.tests {
for _, requestedBalance := range test.requestedBalances {
authFactory, err := generateAuthFactory()
if err != nil {
return nil, err
}
test.AuthFactories = append(test.AuthFactories, authFactory)
requestedAllocations = append(
requestedAllocations,
&genesis.CustomAllocation{
Address: authFactory.Address(),
Balance: requestedBalance,
},
)
}
r.tests[index] = test
}
return requestedAllocations, nil
}

// we need to pre-register all the test registries that are created externally in order to comply with the ginko execution order.
// i.e. the global `var _ = ginkgo.Describe` used in the integration/e2e tests need to have this field populated before the iteration
// over the top level nodes.
var testRegistries = map[*Registry]bool{}

func Register(registry *Registry, name string, f TestFunc) bool {
registry.Add(name, f)
func Register(registry *Registry, name string, f TestFunc, requestedBalances ...uint64) bool {
registry.Add(name, f, requestedBalances...)
testRegistries[registry] = true
return true
}
Expand Down