-
Notifications
You must be signed in to change notification settings - Fork 54
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
offchain - unit testing improvements #117
Conversation
@@ -142,6 +141,9 @@ func (rf *ExecutionReportingPluginFactory) NewReportingPlugin(config types.Repor | |||
offchainConfig: offchainConfig, | |||
cachedDestTokens: cachedDestTokens, | |||
cachedSourceFeeTokens: cachedSourceFeeTokens, | |||
customTokenPoolFactory: func(ctx context.Context, contractBackend bind.ContractBackend, poolAddress common.Address) (custom_token_pool.CustomTokenPoolInterface, 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.
ctx
is not used, probably can be removed from func
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, I would consider flipping args order and making it consistent with the NewCustomTokenPool
func, for instance
func(ctx context.Context, poolAddress common.Address, contractBackend bind.ContractBackend) (custom_token_pool.CustomTokenPoolInterface, error) {
return custom_token_pool.NewCustomTokenPool(poolAddress, contractBackend)
}
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.
flipped the order, ctx might be useful in the future since contract client
is also passed
} | ||
|
||
func setupCommitTestHarness(t *testing.T) commitTestHarness { | ||
th := plugintesthelpers.SetupCCIPTestHarness(t) |
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.
It seems that func SetupCCIPTestHarness(t *testing.T) CCIPPluginTestHarness
is no longer used and can be removed then
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.
... and probably other functions from plugin_harness.go
might become unused
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.
nice catch
tokenPriceUpdates []ccipevents.Event[price_registry.PriceRegistryUsdPerTokenUpdated] | ||
sendRequests []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] | ||
|
||
expCommitReport commit_store.CommitStoreCommitReport |
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, I am not sure what is the best way to do it in Go, but expCommitReport
always contains a meaningful report when shouldGerReport
is true
. Meaning that maybe it would be easier to have it as a pointer and do the following:
testCases := []struct {
expCommitReport *commit_store.CommitStoreCommitReport
}
if tc.expCommitReport != nil {
encodedExpectedReport, err := abihelpers.EncodeCommitReport(tc.expCommitReport)
assert.NoError(t, err)
assert.Equal(t, types.Report(encodedExpectedReport), gotReport)
}
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.
yeah better
lggr := logger.TestLogger(t) | ||
priceGetter := newMockPriceGetter() | ||
|
||
backendClient := client.NewSimulatedBackendClient(t, th.Dest.Chain, new(big.Int).SetUint64(th.Dest.ChainID)) |
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.
Don't we want to keep some e2e tests using simulated backends anyway? I understand that mocking simplifies some things and makes tests more concise. However, we sacrifice e2e nature of those tests by mocking most of the deps (which may be fine). Anyway, it might be worth keeping some smokes for the simulated backend anyway to verify running on a mocked env, or maybe it's already covered somewhere else? WDYT?
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 Everything should already be covered by the unit tests. A simulated backend, which is still 'fake/mock', adds complexity to the test suite and doesn't really cover/catch more cases. I believe that having real e2e integration tests and a proper unit tests suite is enough.
Return(gas.EvmFee{Legacy: assets.NewWei(tc.fee)}, uint32(0), nil) | ||
} | ||
|
||
p := &CommitReportingPlugin{} |
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 not init when declared?
p := &CommitReportingPlugin{
lggr: logger.TestLogger(t),
inflightReports: newInflightCommitReportsContainer(time.Minute),
...
}
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.
just a matter of initialisation preference, imo not a big deal
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 it's easier to read when a struct is initalized in one instruction instead of being splitted into multiple p.field = "x"
. When I see init split into multiple lines, I would assume that we need some extra computation steps between adding values to the fields. Up to you.
@@ -22,6 +23,8 @@ type BlockMeta struct { | |||
} | |||
|
|||
// Client can be used to fetch CCIP related parsed on-chain events. | |||
// | |||
//go:generate mockery --quiet --name Client --output . --filename mock.go --inpackage --case=underscore | |||
type Client interface { |
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.
wdyt about a rename of the pkg to ccipchain
and this interface to Reader
? Then plugins would make use of a ccipchain.Reader
. More general than events since we make use of a LatestBlock and I can see that evolving into abstracting the evmclient/gas.EvmEstimator as well which will help us support non-evm
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.
hmmm
reader.GetSendRequestsGteSeqNum
reader.GetSendRequestsBetweenSeqNums
reader.GetTokenPriceUpdatesCreatedAfter
Reader
might cause confusion with the io.Reader
and I feel ccipchain
not really related to what this package contains
Thinking out loud: This package contains functionality to easily access/fetch on-chain data/events related to ccip.
What about package name being: onchaindata
and interface Fetcher
?
onchaindata.Fetcher
Plugin {
sourceFetcher onchaindata.Fetcher
destFetcher onchaindata.Fetcher
}
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'm not worried about confusion with io.Reader with our own package prefix present. Fetcher/Accessor/Getter etc are all just less clear/work arounds to avoid Reader. I'm cool with onchaindata
or even just onchain
:
Plugin {
source onchain.Reader
dest onchain.Reader
}
source.GetSendRequestsGteSeqNum
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.
renamed
assert.Equal(t, tt.expected, decodedObservation) | ||
assert.NoError(t, err) | ||
|
||
expObsBytes, _ := tc.expObs.Marshal() |
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.
assert no 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.
lazy :D fixed but it would work anyway, test would fail.
commitStore := mock_contracts.NewCommitStoreInterface(t) | ||
commitStore.On("IsUnpausedAndARMHealthy", mock.Anything).Return(!tc.commitStoreIsPaused, nil) | ||
if tc.commitStoreSeqNum > 0 { | ||
commitStore.On("GetExpectedNextSequenceNumber", mock.Anything).Return(tc.commitStoreSeqNum, 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.
All this conditional mocking makes it super tightly coupled to the impl and hard to read so refactoring will be very painful. I feel like a fake instead of a mock might significantly help us here. Some ideas:
- Trim the interfaces to the bare minimum that we inject into the plugins. E.g. don't use EVM2EVMOffRampInterface/CommitStoreInterface etc. but define the smallest possible interface of only functions we use and inject that. That will make it much easier if we want a custom fake. Also related to my comment above regarding ccipchain.Reader, it'll help with the EVM abstraction as well (not relying on gethwrappers)
- Introduce fakes such that we can construct the whole plugin in the state that we want for a given test case before we call some plugin function i.e. get as close as possible to this:
testCases := []struct {
name string
plugin CommitReportingPlugin
expErr bool
expObs CommitObservation
}{
{
name: "base report",
plugin: CommitReportingPlugin{
config: CommitPluginConfig{
commitStore: NewFakeCommitPlugin(paused=false, nextSeqNr=1, epochAndRound=10),
sourceEvents: NewFakeEvents(sendRequests=[a,b,c])
// TODO
},
// TODO
},
expObs: CommitObservation{
TokenPricesUSD: map[common.Address]*big.Int{
someTokenAddr: big.NewInt(20000000000),
},
SourceGasPriceUSD: big.NewInt(0),
Interval: commit_store.CommitStoreInterval{
Min: 54,
Max: 55,
},
},
},
We can introduce state modifications or errorOnCall params to the fakes if we need to as well, but should be able to get quite far with just static ones I'd imagine. Can probably still make use of mockery if you want too to minimize fake code i.e.:
func NewFakeCommitStore(bool disabled,...) CommitStore {
m := mocks.CommitStore{} // Mockery generated for minimal CommitStore interface
m.On("IsUnpausedAndARMHealthy", mock.Anything).Return(!disabled, nil).Maybe()
// ...
return m
}
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.
Another idea that comes to mind is breaking down the plugin steps into 2 distinct phases: state read and compute. Then unit test them separately e.g.:
(p Plugin) Observe(...) (Observation, error)
obsState, err := p.ObservationState() // read all the relevant info from chain/caches/db etc.
if err != nil {
return nil, err
}
return computeObservation(obsState)
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.
Yeah I agree, I will work a bit more to improve this
…ccip into ref/improve-test-suite
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.
lgtm
Return(gas.EvmFee{Legacy: assets.NewWei(tc.fee)}, uint32(0), nil) | ||
} | ||
|
||
p := &CommitReportingPlugin{} |
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 it's easier to read when a struct is initalized in one instruction instead of being splitted into multiple p.field = "x"
. When I see init split into multiple lines, I would assume that we need some extra computation steps between adding values to the fields. Up to you.
logpoller
with two newccipevents
methods andcustomTokenPoolFactory
plugin field.