Skip to content
Snippets Groups Projects
fpc_test.go 5.42 KiB
package fpc_test

import (
	"context"
	"errors"
	"testing"

	"github.com/iotaledger/goshimmer/packages/vote"
	"github.com/iotaledger/goshimmer/packages/vote/fpc"
	"github.com/iotaledger/hive.go/events"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestVoteContext_IsFinalized(t *testing.T) {
	type testInput struct {
		voteCtx               vote.Context
		coolOffPeriod         int
		finalizationThreshold int
		want                  bool
	}
	var tests = []testInput{
		{vote.Context{
			Opinions: []vote.Opinion{vote.Like, vote.Like, vote.Like, vote.Like, vote.Like},
		}, 2, 2, true},
		{vote.Context{
			Opinions: []vote.Opinion{vote.Like, vote.Like, vote.Like, vote.Like, vote.Dislike},
		}, 2, 2, false},
	}

	for _, test := range tests {
		assert.Equal(t, test.want, test.voteCtx.IsFinalized(test.coolOffPeriod, test.finalizationThreshold))
	}
}

func TestVoteContext_LastOpinion(t *testing.T) {
	type testInput struct {
		voteCtx  vote.Context
		expected vote.Opinion
	}
	var tests = []testInput{
		{vote.Context{
			Opinions: []vote.Opinion{vote.Like, vote.Like, vote.Like, vote.Like},
		}, vote.Like},
		{vote.Context{
			Opinions: []vote.Opinion{vote.Like, vote.Like, vote.Like, vote.Dislike},
		}, vote.Dislike},
	}

	for _, test := range tests {
		assert.Equal(t, test.expected, test.voteCtx.LastOpinion())
	}
}

func TestFPCPreventSameIDMultipleTimes(t *testing.T) {
	voter := fpc.New(nil)
	assert.NoError(t, voter.Vote("a", vote.Like))
	// can't add the same item twice
	assert.True(t, errors.Is(voter.Vote("a", vote.Like), fpc.ErrVoteAlreadyOngoing))
}

type opiniongivermock struct {
	roundsReplies []vote.Opinions
	roundIndex    int
}

func (ogm *opiniongivermock) ID() string {
	return ""
}
func (ogm *opiniongivermock) Query(_ context.Context, _ []string) (vote.Opinions, error) {
	if ogm.roundIndex >= len(ogm.roundsReplies) {
		return ogm.roundsReplies[len(ogm.roundsReplies)-1], nil
	}
	opinions := ogm.roundsReplies[ogm.roundIndex]
	ogm.roundIndex++
	return opinions, nil
}

func TestFPCFinalizedEvent(t *testing.T) {
	opinionGiverMock := &opiniongivermock{
		roundsReplies: []vote.Opinions{
			// 2 cool-off period, 2 finalization threshold
			{vote.Like}, {vote.Like}, {vote.Like}, {vote.Like},
		},
	}
	opinionGiverFunc := func() (givers []vote.OpinionGiver, err error) {
		return []vote.OpinionGiver{opinionGiverMock}, nil
	}

	id := "a"

	paras := fpc.DefaultParameters()
	paras.FinalizationThreshold = 2
	paras.CoolingOffPeriod = 2
	paras.QuerySampleSize = 1
	voter := fpc.New(opinionGiverFunc, paras)
	var finalizedOpinion *vote.Opinion
	voter.Events().Finalized.Attach(events.NewClosure(func(id string, opinion vote.Opinion) {
		finalizedOpinion = &opinion
	}))
	assert.NoError(t, voter.Vote(id, vote.Like))

	// do 5 rounds of FPC -> 5 because the last one finalizes the vote
	for i := 0; i < 5; i++ {
		assert.NoError(t, voter.Round(0.5))
	}

	require.NotNil(t, finalizedOpinion, "finalized event should have been fired")
	assert.Equal(t, vote.Like, *finalizedOpinion, "the final opinion should have been 'Like'")
}

func TestFPCFailedEvent(t *testing.T) {
	opinionGiverFunc := func() (givers []vote.OpinionGiver, err error) {
		return []vote.OpinionGiver{&opiniongivermock{
			// doesn't matter what we set here
			roundsReplies: []vote.Opinions{{vote.Dislike}},
		}}, nil
	}

	id := "a"

	paras := fpc.DefaultParameters()
	paras.QuerySampleSize = 1
	paras.MaxRoundsPerVoteContext = 3
	paras.CoolingOffPeriod = 0
	// since the finalization threshold is over max rounds it will
	// always fail finalizing an opinion
	paras.FinalizationThreshold = 4
	voter := fpc.New(opinionGiverFunc, paras)
	var failedOpinion *vote.Opinion
	voter.Events().Failed.Attach(events.NewClosure(func(id string, opinion vote.Opinion) {
		failedOpinion = &opinion
	}))
	assert.NoError(t, voter.Vote(id, vote.Like))

	for i := 0; i < 4; i++ {
		assert.NoError(t, voter.Round(0.5))
	}
	require.NotNil(t, failedOpinion, "failed event should have been fired")
	assert.Equal(t, vote.Dislike, *failedOpinion, "the final opinion should have been 'Dislike'")
}

func TestFPCVotingMultipleOpinionGivers(t *testing.T) {
	type testInput struct {
		id                 string
		initOpinion        vote.Opinion
		expectedRoundsDone int
		expectedOpinion    vote.Opinion
	}
	var tests = []testInput{
		{"1", vote.Like, 5, vote.Like},
		{"2", vote.Dislike, 5, vote.Dislike},
	}

	for _, test := range tests {
		// note that even though we're defining QuerySampleSize times opinion givers,
		// it doesn't mean that FPC will query all of them.
		opinionGiverFunc := func() (givers []vote.OpinionGiver, err error) {
			opinionGivers := make([]vote.OpinionGiver, fpc.DefaultParameters().QuerySampleSize)
			for i := 0; i < len(opinionGivers); i++ {
				opinionGivers[i] = &opiniongivermock{roundsReplies: []vote.Opinions{{test.initOpinion}}}
			}
			return opinionGivers, nil
		}

		paras := fpc.DefaultParameters()
		paras.FinalizationThreshold = 2
		paras.CoolingOffPeriod = 2
		voter := fpc.New(opinionGiverFunc, paras)
		var finalOpinion *vote.Opinion
		voter.Events().Finalized.Attach(events.NewClosure(func(id string, finalizedOpinion vote.Opinion) {
			finalOpinion = &finalizedOpinion
		}))

		assert.NoError(t, voter.Vote(test.id, test.initOpinion))

		var roundsDone int
		for finalOpinion == nil {
			assert.NoError(t, voter.Round(0.7))
			roundsDone++
		}

		assert.Equal(t, test.expectedRoundsDone, roundsDone)
		require.NotNil(t, finalOpinion)
		assert.Equal(t, test.expectedOpinion, *finalOpinion)
	}
}