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

offchain - unit testing improvements #117

Merged
merged 31 commits into from
Sep 18, 2023
Merged

Conversation

dimkouv
Copy link
Contributor

@dimkouv dimkouv commented Sep 6, 2023

  • Making the code more testable by replacing logpoller with two new ccipevents methods and customTokenPoolFactory plugin field.
  • Getting rid of a database connection in unit tests.
  • Re-writing several unit-tests.
  • Renaming and re-ordering.
  • Adding more unit tests.

@dimkouv dimkouv changed the title [DRAFT] unit testing improvements offchain - unit testing improvements Sep 7, 2023
@dimkouv dimkouv marked this pull request as ready for review September 8, 2023 08:49
@dimkouv dimkouv requested a review from a team as a code owner September 8, 2023 08:49
@@ -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) {
Copy link
Contributor

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

Copy link
Contributor

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)
}

Copy link
Contributor Author

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)
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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)
}

Copy link
Contributor Author

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))
Copy link
Contributor

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?

Copy link
Contributor Author

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{}
Copy link
Contributor

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),
				...
			}

Copy link
Contributor Author

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

Copy link
Contributor

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 {
Copy link
Collaborator

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

Copy link
Contributor Author

@dimkouv dimkouv Sep 14, 2023

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
}

Copy link
Collaborator

@connorwstein connorwstein Sep 14, 2023

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

Copy link
Contributor Author

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()
Copy link
Collaborator

Choose a reason for hiding this comment

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

assert no 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.

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)
Copy link
Collaborator

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:

  1. 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)
  2. 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
}

Copy link
Collaborator

@connorwstein connorwstein Sep 12, 2023

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) 

Copy link
Contributor Author

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

Copy link
Contributor

@mateusz-sekara mateusz-sekara left a 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{}
Copy link
Contributor

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.

@dimkouv dimkouv merged commit b69f404 into ccip-develop Sep 18, 2023
84 checks passed
@dimkouv dimkouv deleted the ref/improve-test-suite branch September 18, 2023 11:26
asoliman92 pushed a commit that referenced this pull request Jul 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants