NeoGoBros / neo-go-coverage

GNU General Public License v3.0
0 stars 0 forks source link

Add coverage collection in neotest #1

Open Furetur opened 4 months ago

Furetur commented 4 months ago

Assuming that the Hooks API is implemented in the VM (https://github.com/NeoGoBros/neo-go/commit/65f116d02ae32aadbdf37f612fe0581dcbdacff2), implement coverage collection in neatest

Furetur commented 4 months ago

Who calls VM? How is VM created?

Callstack

Suppose that we have the following test (alphabet_test.go)

func TestEmit(t *testing.T) {
    _, c := newAlphabetInvoker(t)

    ...

    cCommittee.InvokeFail(t, "no gas to emit", method)

    ...

    cCommittee.Invoke(t, stackitem.Null{}, method)

    ...

    cNotAlphabet.InvokeFail(t, "invalid invoker", method)
}

Invoke and InvokeFail call the specified contract methods. How?

  1. Invoke creates a transaction that calls the specified method and adds it to blockchain with c.AddNewBlock(t, tx)
func (c *ContractInvoker) Invoke(t testing.TB, result any, method string, args ...any) util.Uint256 {
    tx := c.PrepareInvoke(t, method, args...)
    c.AddNewBlock(t, tx)
    c.CheckHalt(t, tx.Hash(), stackitem.Make(result))
    return tx.Hash()
}
  1. AddNewBlock creates a block and calls e.chain.AddBlock(b)
func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
    b := e.NewUnsignedBlock(t, txs...)
    e.SignBlock(b)
    require.NoError(t, e.Chain.AddBlock(b))
    return b
}
  1. AddBlock calls storeBlock
  2. storeBlock calls runPersist
  3. runPersist creates or reuses an already created VM
if v == nil {
        v = systemInterop.SpawnVM()
} else {
        systemInterop.ReuseVM(v)
}

SpawnVM, ReuseVM

// SpawnVM spawns a new VM with the specified gas limit and set context.VM field.
func (ic *Context) SpawnVM() *vm.VM {
    v := vm.NewWithTrigger(ic.Trigger)
    ic.initVM(v)
    return v
}

func (ic *Context) initVM(v *vm.VM) {
    v.LoadToken = ic.LoadToken
    v.GasLimit = -1
    v.SyscallHandler = ic.SyscallHandler
    v.SetPriceGetter(ic.GetPrice)
    ic.VM = v
}

// ReuseVM resets given VM and allows to reuse it in the current context.
func (ic *Context) ReuseVM(v *vm.VM) {
    v.Reset(ic.Trigger)
    ic.initVM(v)
}
Furetur commented 4 months ago

Interop context

Interop context is defined in pkg/core/interop/context.go. ReuseVM and SpawnVM are defined there

It is needed for interop calls to work. For example, the storage.Put call uses DAO to store data

type Context struct {
    ...
    DAO              *dao.Simple
    ...
}