-
Notifications
You must be signed in to change notification settings - Fork 120
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
Add UTs to Processor Execute()
#1890
base: main
Are you sure you want to change the base?
Conversation
internal/logging/noop.go
Outdated
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package logging |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use logging.NoLog{}
from AvalancheGo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
workers: &workers.MockWorkers{ | ||
OnNewJob: func(_ int) (workers.Job, error) { | ||
return &workers.MockJob{ | ||
OnWaitError: errMockWorkerJob, | ||
}, nil | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use the actual implementation of workers and a signature that should fail verification instead of mocking it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
tt.validityWindow = &validitywindowtest.MockTimeValidityWindow[*chain.Transaction]{} | ||
} | ||
|
||
testChain, err := chain.NewChain( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we construct only the processor rather than the chain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
block: &chain.ExecutionBlock{ | ||
StatelessBlock: &chain.StatelessBlock{ | ||
Hght: 1, | ||
Tmstmp: testRules.GetMinEmptyBlockGap(), | ||
StateRoot: invalidStateRoot, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we switch all instances to using an existing constructor where possible?
This ensures that we're using the actual code to produce a fully populated block where possible and reduces the number of paths where we may create a block instance with some fields left unpopulated (this will leave the bytes, id, and auth counts fields unpopulated).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
for k, v := range tt.state { | ||
r.NoError(db.Put([]byte(k), v)) | ||
} | ||
r.NoError(db.CommitToDB(ctx)) | ||
|
||
if tt.includeRoot { | ||
root, err := db.GetMerkleRoot(ctx) | ||
r.NoError(err) | ||
tt.block.StateRoot = root | ||
} | ||
|
||
_, err = testChain.Execute(ctx, db, tt.block, tt.isNormalOp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change to use a function to return the block based off of a merkledb.View
constructed from the state?
This way each test case clearly specifies whether it is using the parent's merkle root or a random value as opposed to relying on overriding it via includeRoot
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/metrics.go
Outdated
@@ -53,7 +53,7 @@ func (em *executorMetrics) RecordExecutable() { | |||
em.executable.Inc() | |||
} | |||
|
|||
func newMetrics(reg *prometheus.Registry) (*chainMetrics, error) { | |||
func NewMetrics(reg *prometheus.Registry) (*chainMetrics, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Exported functions shouldn't return unexported types because then the calling code is unable to use the type it's being given
- This diff is unrelated to the PR's objective of adding unit tests, unstage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per offline convo, going to export NewMetrics()
and ChainMetrics
for now. A future PR which resolves the chain
test cyclic dependencies should un-export these.
chain/processor.go
Outdated
@@ -383,7 +383,7 @@ func (p *Processor) createBlockContext( | |||
} | |||
parentHeight, err := database.ParseUInt64(parentHeightRaw) | |||
if err != nil { | |||
return blockContext{}, fmt.Errorf("failed to parse parent height from state: %w", err) | |||
return blockContext{}, fmt.Errorf("%w %w", ErrFailedToParseParentHeight, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend adding some delimiter like in the previous error message to make this easier to read
return blockContext{}, fmt.Errorf("%w %w", ErrFailedToParseParentHeight, err) | |
return blockContext{}, fmt.Errorf("%w: %w", ErrFailedToParseParentHeight, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package chain_test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't unit tests for chain
live inside of chain
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a cyclic dependency between the chain
package and the genesis
package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cyclic dependency is more of a symptom of the underlying problem which is that the tests depend on auth
since we use their signing implementations. This dependency issue is easily fixable if we just create a test auth implementation - there really isn't a reason why this package needs to exist. I'm fine with us doing this in a different PR though, since this package already exists in the current state of the repo
chain/processor_test.go
Outdated
testRules := genesis.NewDefaultRules() | ||
testRuleFactory := genesis.ImmutableRuleFactory{Rules: testRules} | ||
createValidBlock := func(root ids.ID) (*chain.StatelessBlock, error) { | ||
return chain.NewStatelessBlock( | ||
ids.Empty, | ||
time.Now().UnixMilli(), | ||
1, | ||
nil, | ||
root, | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just eat the cost of duplicate code here and declare these per test - even if these are types that are immutable sharing state across tests is harder to maintain a mental model of + can lead to issues if it becomes mutable in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
expectedErr: chain.ErrFailedToFetchParentFee, | ||
}, | ||
{ | ||
name: "failed VerifyExpiryReplayProtection", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: name this lowercase to be consistent, e.g fails replay protection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
if tt.validityWindow == nil { | ||
tt.validityWindow = &validitywindowtest.MockTimeValidityWindow[*chain.Transaction]{} | ||
} | ||
|
||
if tt.workers == nil { | ||
tt.workers = workers.NewSerial() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we override this from the test? IMO it's better to have a single source of truth and define this in the test table instead of overriding it w/ a default in the test body
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated tests to declare the workers/validity window that they use
chain/processor_test.go
Outdated
}, | ||
{ | ||
name: "block timestamp too late", | ||
createBlock: func(root ids.ID) (*chain.StatelessBlock, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO returning this error is awkward, can we use require.NoError(t, <type>)
to guarantee no error to the caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
validityWindow chain.ValidityWindow | ||
isNormalOp bool | ||
createBlock createBlock | ||
state map[string][]byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the function Execute
that we're testing operates on a merkledb.View
, should we use that instead of a map
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
isNormalOp bool | ||
createBlock createBlock | ||
state map[string][]byte | ||
workers workers.Workers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move this to be adjacent to validityWindow
so that dependencies and parameters are grouped to be adjacent with each other
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
state: map[string][]byte{ | ||
heightKey: binary.BigEndian.AppendUint64(nil, 0), | ||
timestampKey: binary.BigEndian.AppendUint64(nil, 0), | ||
feeKey: {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: Why is this value the empty value? Is this for convenience because the zero value {}
is equivalent to a 0 varint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By setting the raw fee value to empty, this is equivalent to a fresh fee manager being loaded in (and which should be fine since we're not testing the FM in this PR)
hypersdk/internal/fees/manager.go
Lines 33 to 35 in a9801b8
if len(raw) == 0 { | |
raw = make([]byte, consts.Int64Len+fees.FeeDimensions*dimensionStateLen) | |
} |
chain/processor_test.go
Outdated
createValidBlock := func(root ids.ID) *chain.StatelessBlock { | ||
block, err := chain.NewStatelessBlock( | ||
ids.Empty, | ||
time.Now().UnixMilli(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid non-deterministic behavior in unit tests where possible - this test should pass if we just use time.Time{}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done; only exception is the "block timestamp too late"
test which uses time.Now()
to error against the following:
Lines 130 to 133 in a9801b8
// Perform basic correctness checks before doing any expensive work | |
if b.Tmstmp > time.Now().Add(FutureBound).UnixMilli() { | |
return nil, ErrTimestampTooLate | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is still inherently flaky, it could be refactored to accept caller-provided timestamps but it's outside of the scope of the PR
chain/processor_test.go
Outdated
validityWindow chain.ValidityWindow | ||
workers workers.Workers | ||
isNormalOp bool | ||
db merkledb.MerkleDB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use merkledb.View
instead of merkledb.MerkleDB
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
tests := []struct { | ||
name string | ||
validityWindow chain.ValidityWindow | ||
workers workers.Workers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need workers.Workers
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed workers
from tests
chain/processor_test.go
Outdated
name: "verify signatures fails", | ||
validityWindow: &validitywindowtest.MockTimeValidityWindow[*chain.Transaction]{}, | ||
workers: func() workers.Workers { | ||
w := workers.NewParallel(0, 0) | ||
w.Stop() | ||
return w | ||
}(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we test this by using an invalid auth rather than mocking workers? I don't think it's necessary to test with an invalid instance of workers.Workers
so we should remove this as a parameter from the test cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per offline convo, I believe we can get rid of this test. Stop()
is called when the VM is shutting down and since that case is outside the scope of Execute()
, we shouldn't need to test for it.
chain/processor_test.go
Outdated
db: func() merkledb.MerkleDB { | ||
db, err := merkledb.New( | ||
context.Background(), | ||
memdb.New(), | ||
merkledb.Config{ | ||
BranchFactor: merkledb.BranchFactor16, | ||
Tracer: trace.Noop, | ||
}, | ||
) | ||
require.NoError(t, err) | ||
return db | ||
}(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we replace constructing the merkledb each time with a function that takes in a set of state keys and then returns a merkledb.View
with all of those added so that we don't need to repeat the same code in every test case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain/processor_test.go
Outdated
newBlockF: func(root ids.ID) *chain.StatelessBlock { | ||
block, err := chain.NewStatelessBlock( | ||
ids.Empty, | ||
genesis.NewDefaultRules().GetMinEmptyBlockGap(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are using genesis.NewDefaultRules()
in the body of the for loop, then it's more cognitive load imo to use genesis.NewDefaultRules()
, which depends on the fact we're using the default below.
Could we define the rules at the top of the test rather than doing this in each test case where it's needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
chain.NewExecutionBlock(tt.newBlockF(root)), | ||
tt.isNormalOp, | ||
) | ||
r.ErrorIs(err, tt.expectedErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(separate PR) - could we confirm the post state is what we expect as well ie. we should make sure that the metadata is updated correctly and that tx/action execution (include successful and failed w/ rollbacks) has the expected effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack
This PR adds the following:
Execute()
method of the processor