mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 10:27:26 +08:00
* wip: conversion of hotstuff from flow into Q-oriented model * bulk of tests * remaining non-integration tests * add integration test, adjust log interface, small tweaks * further adjustments, restore full pacemaker shape * add component lifecycle management+supervisor * further refinements * resolve timeout hanging * mostly finalized state for consensus * bulk of engine swap out * lifecycle-ify most types * wiring nearly complete, missing needed hooks for proposals * plugged in, vetting message validation paths * global consensus, plugged in and verified * app shard now wired in too * do not decode empty keys.yml (#456) * remove obsolete engine.maxFrames config parameter (#454) * default to Info log level unless debug is enabled (#453) * respect config's "logging" section params, remove obsolete single-file logging (#452) * Trivial code cleanup aiming to reduce Go compiler warnings (#451) * simplify range traversal * simplify channel read for single select case * delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24 * simplify range traversal * simplify channel read for single select case * remove redundant type from array * simplify range traversal * simplify channel read for single select case * RC slate * finalize 2.1.0.5 * Update comments in StrictMonotonicCounter Fix comment formatting and clarify description. --------- Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
159 lines
5.8 KiB
Go
159 lines
5.8 KiB
Go
package voteaggregator
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/gammazero/workerpool"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
"go.uber.org/atomic"
|
|
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/helper"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/mocks"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
|
|
)
|
|
|
|
var factoryError = errors.New("factory error")
|
|
|
|
func TestVoteCollectors(t *testing.T) {
|
|
suite.Run(t, new(VoteCollectorsTestSuite))
|
|
}
|
|
|
|
// VoteCollectorsTestSuite is a test suite for isolated testing of VoteCollectors.
|
|
// Contains helper methods and mocked state which is used to verify correct behavior of VoteCollectors.
|
|
type VoteCollectorsTestSuite struct {
|
|
suite.Suite
|
|
|
|
mockedCollectors map[uint64]*mocks.VoteCollector[*helper.TestState, *helper.TestVote]
|
|
factoryMethod NewCollectorFactoryMethod[*helper.TestState, *helper.TestVote]
|
|
collectors *VoteCollectors[*helper.TestState, *helper.TestVote]
|
|
lowestLevel uint64
|
|
workerPool *workerpool.WorkerPool
|
|
}
|
|
|
|
func (s *VoteCollectorsTestSuite) SetupTest() {
|
|
s.lowestLevel = 1000
|
|
s.mockedCollectors = make(map[uint64]*mocks.VoteCollector[*helper.TestState, *helper.TestVote])
|
|
s.workerPool = workerpool.New(2)
|
|
s.factoryMethod = func(rank uint64, _ consensus.Workers) (consensus.VoteCollector[*helper.TestState, *helper.TestVote], error) {
|
|
if collector, found := s.mockedCollectors[rank]; found {
|
|
return collector, nil
|
|
}
|
|
return nil, fmt.Errorf("mocked collector %v not found: %w", rank, factoryError)
|
|
}
|
|
s.collectors = NewVoteCollectors(helper.Logger(), s.lowestLevel, s.workerPool, s.factoryMethod)
|
|
}
|
|
|
|
func (s *VoteCollectorsTestSuite) TearDownTest() {
|
|
s.workerPool.StopWait()
|
|
}
|
|
|
|
// prepareMockedCollector prepares a mocked collector and stores it in map, later it will be used
|
|
// to mock behavior of vote collectors.
|
|
func (s *VoteCollectorsTestSuite) prepareMockedCollector(rank uint64) *mocks.VoteCollector[*helper.TestState, *helper.TestVote] {
|
|
collector := &mocks.VoteCollector[*helper.TestState, *helper.TestVote]{}
|
|
collector.On("Rank").Return(rank).Maybe()
|
|
s.mockedCollectors[rank] = collector
|
|
return collector
|
|
}
|
|
|
|
// TestGetOrCreatorCollector_RankLowerThanLowest tests a scenario where caller tries to create a collector with rank
|
|
// lower than already pruned one. This should result in sentinel error `BelowPrunedThresholdError`
|
|
func (s *VoteCollectorsTestSuite) TestGetOrCreatorCollector_RankLowerThanLowest() {
|
|
collector, created, err := s.collectors.GetOrCreateCollector(s.lowestLevel - 10)
|
|
require.Nil(s.T(), collector)
|
|
require.False(s.T(), created)
|
|
require.Error(s.T(), err)
|
|
require.True(s.T(), models.IsBelowPrunedThresholdError(err))
|
|
}
|
|
|
|
// TestGetOrCreateCollector_ValidCollector tests a happy path scenario where we try first to create and then retrieve cached collector.
|
|
func (s *VoteCollectorsTestSuite) TestGetOrCreateCollector_ValidCollector() {
|
|
rank := s.lowestLevel + 10
|
|
s.prepareMockedCollector(rank)
|
|
collector, created, err := s.collectors.GetOrCreateCollector(rank)
|
|
require.NoError(s.T(), err)
|
|
require.True(s.T(), created)
|
|
require.Equal(s.T(), rank, collector.Rank())
|
|
|
|
cached, cachedCreated, err := s.collectors.GetOrCreateCollector(rank)
|
|
require.NoError(s.T(), err)
|
|
require.False(s.T(), cachedCreated)
|
|
require.Equal(s.T(), collector, cached)
|
|
}
|
|
|
|
// TestGetOrCreateCollector_FactoryError tests that error from factory method is propagated to caller.
|
|
func (s *VoteCollectorsTestSuite) TestGetOrCreateCollector_FactoryError() {
|
|
// creating collector without calling prepareMockedCollector will yield factoryError.
|
|
collector, created, err := s.collectors.GetOrCreateCollector(s.lowestLevel + 10)
|
|
require.Nil(s.T(), collector)
|
|
require.False(s.T(), created)
|
|
require.ErrorIs(s.T(), err, factoryError)
|
|
}
|
|
|
|
// TestGetOrCreateCollectors_ConcurrentAccess tests that concurrently accessing of GetOrCreateCollector creates
|
|
// only one collector and all other instances are retrieved from cache.
|
|
func (s *VoteCollectorsTestSuite) TestGetOrCreateCollectors_ConcurrentAccess() {
|
|
createdTimes := atomic.NewUint64(0)
|
|
rank := s.lowestLevel + 10
|
|
s.prepareMockedCollector(rank)
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
_, created, err := s.collectors.GetOrCreateCollector(rank)
|
|
require.NoError(s.T(), err)
|
|
if created {
|
|
createdTimes.Add(1)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
require.Equal(s.T(), uint64(1), createdTimes.Load())
|
|
}
|
|
|
|
// TestPruneUpToRank tests pruning removes item below pruning height and leaves unmodified other items.
|
|
func (s *VoteCollectorsTestSuite) TestPruneUpToRank() {
|
|
numberOfCollectors := uint64(10)
|
|
prunedRanks := make([]uint64, 0)
|
|
for i := uint64(0); i < numberOfCollectors; i++ {
|
|
rank := s.lowestLevel + i
|
|
s.prepareMockedCollector(rank)
|
|
_, _, err := s.collectors.GetOrCreateCollector(rank)
|
|
require.NoError(s.T(), err)
|
|
prunedRanks = append(prunedRanks, rank)
|
|
}
|
|
|
|
pruningHeight := s.lowestLevel + numberOfCollectors
|
|
|
|
expectedCollectors := make([]consensus.VoteCollector[*helper.TestState, *helper.TestVote], 0)
|
|
for i := uint64(0); i < numberOfCollectors; i++ {
|
|
rank := pruningHeight + i
|
|
s.prepareMockedCollector(rank)
|
|
collector, _, err := s.collectors.GetOrCreateCollector(rank)
|
|
require.NoError(s.T(), err)
|
|
expectedCollectors = append(expectedCollectors, collector)
|
|
}
|
|
|
|
// after this operation collectors below pruning height should be pruned and everything higher
|
|
// should be left unmodified
|
|
s.collectors.PruneUpToRank(pruningHeight)
|
|
|
|
for _, prunedRank := range prunedRanks {
|
|
_, _, err := s.collectors.GetOrCreateCollector(prunedRank)
|
|
require.Error(s.T(), err)
|
|
require.True(s.T(), models.IsBelowPrunedThresholdError(err))
|
|
}
|
|
|
|
for _, collector := range expectedCollectors {
|
|
cached, _, _ := s.collectors.GetOrCreateCollector(collector.Rank())
|
|
require.Equal(s.T(), collector, cached)
|
|
}
|
|
}
|