Search Apps Documentation Source Content File Folder Download Copy Actions Download

cond_gnolovedao_test.gno

8.64 Kb ยท 333 lines
  1package daocond
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nt/ufmt"
  7	"gno.land/p/nt/urequire"
  8)
  9
 10/*
 11Example 1:
 12T1 100 members --> 300 VP, 3 votes per member
 13T2 100 members --> 200 VP, 2 votes per member
 14T3 100 members --> 100 VP, 1 votes per member
 15Example 2:
 16
 17T1 100 members --> 300 VP, 3 votes per member
 18T2 50 members --> 100 VP, 2 votes per member *
 19T3 10 members --> 10 VP, 1 votes per member *
 20Example 3:
 21
 22T1 100 members --> 300 VP, 3 votes per member
 23T2 200 members --> 200 VP, 1 votes per member *
 24T3 100 members --> 100 VP, 1 votes per member
 25Example 4:
 26
 27T1 100 members --> 300 VP, 3 votes per member
 28T2 200 members --> 200 VP, 1 votes per member *
 29T3 1000 members --> 100 VP, 0.1 votes per member
 30*/
 31func TestComputeVotingPowers(t *testing.T) {
 32	type gnoloveDaoComposition struct {
 33		t1s                int
 34		t2s                int
 35		t3s                int
 36		abstainT3          bool
 37		expectedTotalPower float64
 38		expectedPowers     map[string]float64
 39	}
 40	tests := map[string]gnoloveDaoComposition{
 41		"example 1": {
 42			t1s:                100,
 43			t2s:                100,
 44			t3s:                100,
 45			abstainT3:          false,
 46			expectedTotalPower: 600,
 47			expectedPowers: map[string]float64{
 48				"T1": 3.0,
 49				"T2": 2.0,
 50				"T3": 1.0,
 51			},
 52		},
 53		"example 2": {
 54			t1s:                100,
 55			t2s:                50,
 56			t3s:                10,
 57			abstainT3:          false,
 58			expectedTotalPower: 410,
 59			expectedPowers: map[string]float64{
 60				"T1": 3.0,
 61				"T2": 2.0,
 62				"T3": 1.0,
 63			},
 64		},
 65		"example 3": {
 66			t1s:                100,
 67			t2s:                200,
 68			t3s:                100,
 69			abstainT3:          false,
 70			expectedTotalPower: 600,
 71			expectedPowers: map[string]float64{
 72				"T1": 3.0,
 73				"T2": 1.0,
 74				"T3": 1.0,
 75			},
 76		},
 77		"example 4": {
 78			t1s:                100,
 79			t2s:                200,
 80			t3s:                1000,
 81			abstainT3:          false,
 82			expectedTotalPower: 600,
 83			expectedPowers: map[string]float64{
 84				"T1": 3.0,
 85				"T2": 1.0,
 86				"T3": 0.1,
 87			},
 88		},
 89		"0 -T1s": {
 90			t1s:                0,
 91			t2s:                100,
 92			t3s:                100,
 93			abstainT3:          false,
 94			expectedTotalPower: 0,
 95			expectedPowers: map[string]float64{
 96				"T1": 3.0,
 97				"T2": 0.0,
 98				"T3": 0.0,
 99			},
100		},
101		"100 T1, 1 T2, 1 T3": {
102			t1s:                100,
103			t2s:                1,
104			t3s:                1,
105			abstainT3:          false,
106			expectedTotalPower: 303,
107			expectedPowers: map[string]float64{
108				"T1": 3.0,
109				"T2": 2.0,
110				"T3": 1.0,
111			},
112		},
113		"T3 Abstaining": {
114			t1s:                100,
115			t2s:                100,
116			t3s:                100,
117			expectedTotalPower: 500,
118			abstainT3:          true,
119			expectedPowers: map[string]float64{
120				"T1": 3.0,
121				"T2": 2.0,
122			},
123		},
124	}
125
126	for name, composition := range tests {
127		t.Run(name, func(t *testing.T) {
128			dao := newMockDAO()
129			for i := 0; i < composition.t1s; i++ {
130				dao.addUser(ufmt.Sprintf("%d_T1", i), []string{"T1"})
131			}
132			for i := 0; i < composition.t2s; i++ {
133				dao.addUser(ufmt.Sprintf("%d_T2", i), []string{"T2"})
134			}
135			for i := 0; i < composition.t3s; i++ {
136				dao.addUser(ufmt.Sprintf("%d_T3", i), []string{"T3"})
137			}
138
139			roles := []string{"T1", "T2"}
140			if !composition.abstainT3 {
141				roles = append(roles, "T3")
142			}
143
144			cond := &gnolovDaoCondThreshold{
145				threshold:            0.6,
146				hasRoleFn:            dao.hasRole,
147				roles:                roles,
148				usersWithRoleCountFn: dao.usersWithRoleCount,
149			}
150			votingPowers, totalPower := cond.computeVotingPowers()
151			for tier, expectedPower := range composition.expectedPowers {
152				if votingPowers[tier] != expectedPower {
153					t.Fail()
154				}
155			}
156
157			if totalPower != composition.expectedTotalPower {
158				t.Fail()
159			}
160		})
161	}
162}
163
164func TestEval(t *testing.T) {
165	type gnoloveDaoVotes struct {
166		votesT1      []Vote
167		votesT2      []Vote
168		votesT3      []Vote
169		threshold    float64
170		expectedEval bool
171		expectedYes  float64
172		abstainT3    bool
173		panic        bool
174	}
175	tests := map[string]gnoloveDaoVotes{
176		"2/6% Yes": { //0.3333
177			votesT1:      []Vote{VoteNo},                             // 3 voting power
178			votesT2:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined
179			votesT3:      []Vote{VoteNo},
180			expectedEval: false,
181			abstainT3:    false,
182			threshold:    0.45,
183			expectedYes:  2.0 / 6.0,
184			panic:        false,
185		},
186		"50% Yes": {
187			votesT1:      []Vote{VoteNo},                             // 3 voting power
188			votesT2:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined
189			votesT3:      []Vote{VoteYes},
190			expectedEval: true,
191			abstainT3:    false,
192			threshold:    0.45,
193			expectedYes:  3.0 / 6.0,
194			panic:        false,
195		},
196		"several T2 & T3": {
197			votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power
198			// 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each
199			votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},
200			// 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each
201			votesT3:      []Vote{VoteYes, VoteNo, VoteYes, VoteNo},
202			expectedEval: false,
203			abstainT3:    false,
204			threshold:    0.42, //total power = 6+4+2 T3yes = 1, T2yes = 4 T1yes = 0 totalYes = 0.41666666666 (5/12)
205			expectedYes:  5.0 / 12.0,
206			panic:        false,
207		},
208		"several T2 & T3 eval true": {
209			votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power
210			// 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each
211			votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},
212			// 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each
213			votesT3:      []Vote{VoteYes, VoteYes, VoteYes, VoteNo},
214			expectedEval: true,
215			abstainT3:    false,
216			threshold:    0.42, //total power = 6+4+2 T3yes = 1.5, T2yes = 4 T1yes = 0 totalYes = 0.45833333333 (5.5/12)
217			expectedYes:  5.5 / 12.0,
218			panic:        false,
219		},
220		"only T1s": {
221			votesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power
222			expectedEval: false,
223			abstainT3:    false,
224			threshold:    0.6,
225			expectedYes:  3.0 / 6.0,
226			panic:        false,
227		},
228		"only T3s": { // as T2 & T3 power is capped as power of T1, in this case the power will be 0 everywhere
229			votesT3:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // voting power = 0, 0 each
230			expectedEval: false,
231			abstainT3:    false,
232			threshold:    0.6,
233			expectedYes:  0.0,
234		},
235		"T3 abstaining": {
236			votesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power
237			votesT2:      []Vote{VoteYes, VoteYes},
238			votesT3:      []Vote{},
239			expectedEval: true,
240			abstainT3:    true,
241			threshold:    0.6,
242			expectedYes:  7.0 / 10.0,
243			panic:        false,
244		},
245		"T3 abstaining w/ votes": {
246			votesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power
247			votesT2:      []Vote{VoteYes, VoteYes},
248			votesT3:      []Vote{VoteYes, VoteNo},
249			expectedEval: true,
250			abstainT3:    true,
251			threshold:    0.6,
252			expectedYes:  7.0 / 10.0,
253			panic:        true, // a user with T3 when T3 is abstaining should panic
254		},
255	}
256
257	for name, tdata := range tests {
258		t.Run(name, func(t *testing.T) {
259			if tdata.panic {
260				defer func() {
261					if r := recover(); r == nil {
262						t.Errorf("The code did not panic")
263					}
264				}()
265			}
266			votes := map[string]Vote{}
267			dao := newMockDAO()
268			for i, vote := range tdata.votesT1 {
269				userID := ufmt.Sprintf("%d_T1", i)
270				dao.addUser(userID, []string{"T1"})
271				votes[userID] = vote
272			}
273
274			for i, vote := range tdata.votesT2 {
275				userID := ufmt.Sprintf("%d_T2", i)
276				dao.addUser(userID, []string{"T2"})
277				votes[userID] = vote
278			}
279
280			for i, vote := range tdata.votesT3 {
281				userID := ufmt.Sprintf("%d_T3", i)
282				dao.addUser(userID, []string{"T3"})
283				votes[userID] = vote
284			}
285			ballot := NewBallot()
286			for userId, vote := range votes {
287				ballot.Vote(userId, vote)
288			}
289			roles := []string{"T1", "T2"}
290			if !tdata.abstainT3 {
291				roles = append(roles, "T3")
292			}
293			cond := GnoloveDAOCondThreshold(tdata.threshold, roles, dao.hasRole, dao.usersWithRoleCount)
294			// Get percent of total yes
295			urequire.Equal(t, tdata.expectedEval, cond.Eval(ballot))
296		})
297	}
298}
299
300type mockDAO struct {
301	members map[string][]string
302	roles   map[string][]string
303}
304
305func newMockDAO() *mockDAO {
306	return &mockDAO{
307		members: map[string][]string{},
308		roles:   map[string][]string{}, // roles to users
309	}
310}
311
312func (m *mockDAO) addUser(memberId string, roles []string) {
313	m.members[memberId] = roles
314	for _, memberRole := range roles {
315		m.roles[memberRole] = append(m.roles[memberRole], memberId)
316	}
317}
318func (m *mockDAO) usersWithRoleCount(role string) uint32 {
319	return uint32(len(m.roles[role]))
320}
321
322func (m *mockDAO) hasRole(memberId string, role string) bool {
323	roles, ok := m.members[memberId]
324	if !ok {
325		return false
326	}
327	for _, memberRole := range roles {
328		if memberRole == role {
329			return true
330		}
331	}
332	return false
333}