Skip to content

Commit 73c7f24

Browse files
committed
Initial setup for golang tests
1 parent e2c3eac commit 73c7f24

File tree

7 files changed

+258
-84
lines changed

7 files changed

+258
-84
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Golang Linting
1+
name: Golang Validation
22

33
on:
44
push:
@@ -29,3 +29,6 @@ jobs:
2929
- name: Check Format
3030
run: |
3131
gofmt -s -l database logging sse *.go
32+
- name: Run Tests
33+
run: |
34+
go test ./database

database/database.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"os"
66
"strings"
7+
"testing"
78
"time"
89

910
"github.com/computersciencehouse/vote/logging"
@@ -20,10 +21,16 @@ const (
2021
Updated UpsertResult = 1
2122
)
2223

23-
var Client = Connect()
24+
var Client *mongo.Client = Connect()
2425
var db = ""
2526

2627
func Connect() *mongo.Client {
28+
// This always gets invoked on initialisation. bad! it'd be nice if we only did this setup in main rather than components under test. for now we just skip if testing
29+
if testing.Testing() {
30+
logging.Logger.WithFields(logrus.Fields{"module": "database", "method": "Connect"}).Info("testing, not doing db connection, someone should mock this someday")
31+
return nil
32+
}
33+
2734
logging.Logger.WithFields(logrus.Fields{"module": "database", "method": "Connect"}).Info("beginning database connection")
2835

2936
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)

database/poll.go

Lines changed: 86 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"context"
55
"time"
66

7+
"github.com/sirupsen/logrus"
78
"go.mongodb.org/mongo-driver/bson"
89
"go.mongodb.org/mongo-driver/bson/primitive"
910
"go.mongodb.org/mongo-driver/mongo"
11+
12+
"github.com/computersciencehouse/vote/logging"
1013
)
1114

1215
type Poll struct {
@@ -178,6 +181,88 @@ func GetClosedVotedPolls(ctx context.Context, userId string) ([]*Poll, error) {
178181
return polls, nil
179182
}
180183

184+
func calculateRankedResult(votesRaw []RankedVote) ([]map[string]int, error) {
185+
// We want to store those that were eliminated
186+
eliminated := make([]string, 0)
187+
votes := make([][]string, 0)
188+
finalResult := make([]map[string]int, 0)
189+
190+
//change ranked votes from a map (which is unordered) to a slice of votes (which is ordered)
191+
//order is from first preference to last preference
192+
for _, vote := range votesRaw {
193+
temp, cf := context.WithTimeout(context.Background(), 1*time.Second)
194+
optionList := orderOptions(vote.Options, temp)
195+
cf()
196+
votes = append(votes, optionList)
197+
}
198+
199+
round := 0
200+
// Iterate until we have a winner
201+
for {
202+
round = round + 1
203+
// Contains candidates to number of votes in this round
204+
tallied := make(map[string]int)
205+
voteCount := 0
206+
for _, picks := range votes {
207+
// Go over picks until we find a non-eliminated candidate
208+
for _, candidate := range picks {
209+
if !containsValue(eliminated, candidate) {
210+
if _, ok := tallied[candidate]; ok {
211+
tallied[candidate]++
212+
} else {
213+
tallied[candidate] = 1
214+
}
215+
voteCount += 1
216+
break
217+
}
218+
}
219+
}
220+
// Eliminate lowest vote getter
221+
minVote := 1000000 //the smallest number of votes received thus far (to find who is in last)
222+
minPerson := make([]string, 0) //the person(s) with the least votes that need removed
223+
for person, vote := range tallied {
224+
if vote < minVote { // this should always be true round one, to set a true "who is in last"
225+
minVote = vote
226+
minPerson = make([]string, 0)
227+
minPerson = append(minPerson, person)
228+
} else if vote == minVote {
229+
minPerson = append(minPerson, person)
230+
}
231+
}
232+
eliminated = append(eliminated, minPerson...)
233+
finalResult = append(finalResult, tallied)
234+
235+
// TODO this should probably include some poll identifier
236+
logging.Logger.WithFields(logrus.Fields{"round": round, "tallies": tallied, "threshold": voteCount / 2}).Debug("round report")
237+
238+
// If one person has all the votes, they win
239+
if len(tallied) == 1 {
240+
break
241+
}
242+
243+
end := true
244+
for str, val := range tallied {
245+
// if any particular entry is above half remaining votes, they win and it ends
246+
if val > (voteCount / 2) {
247+
finalResult = append(finalResult, map[string]int{str: val})
248+
end = true
249+
break
250+
}
251+
// Check if all values in tallied are the same
252+
// In that case, it's a tie?
253+
if val != minVote {
254+
end = false
255+
break
256+
}
257+
}
258+
if end {
259+
break
260+
}
261+
}
262+
return finalResult, nil
263+
264+
}
265+
181266
func (poll *Poll) GetResult(ctx context.Context) ([]map[string]int, error) {
182267
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
183268
defer cancel()
@@ -223,9 +308,6 @@ func (poll *Poll) GetResult(ctx context.Context) ([]map[string]int, error) {
223308
return finalResult, nil
224309

225310
case POLL_TYPE_RANKED:
226-
// We want to store those that were eliminated
227-
eliminated := make([]string, 0)
228-
229311
// Get all votes
230312
cursor, err := Client.Database(db).Collection("votes").Aggregate(ctx, mongo.Pipeline{
231313
{{
@@ -239,76 +321,7 @@ func (poll *Poll) GetResult(ctx context.Context) ([]map[string]int, error) {
239321
}
240322
var votesRaw []RankedVote
241323
cursor.All(ctx, &votesRaw)
242-
243-
votes := make([][]string, 0)
244-
245-
//change ranked votes from a map (which is unordered) to a slice of votes (which is ordered)
246-
//order is from first preference to last preference
247-
for _, vote := range votesRaw {
248-
temp, cf := context.WithTimeout(context.Background(), 1*time.Second)
249-
optionList := orderOptions(vote.Options, temp)
250-
cf()
251-
votes = append(votes, optionList)
252-
}
253-
254-
// Iterate until we have a winner
255-
for {
256-
// Contains candidates to number of votes in this round
257-
tallied := make(map[string]int)
258-
voteCount := 0
259-
for _, picks := range votes {
260-
// Go over picks until we find a non-eliminated candidate
261-
for _, candidate := range picks {
262-
if !containsValue(eliminated, candidate) {
263-
if _, ok := tallied[candidate]; ok {
264-
tallied[candidate]++
265-
} else {
266-
tallied[candidate] = 1
267-
}
268-
voteCount += 1
269-
break
270-
}
271-
}
272-
}
273-
// Eliminate lowest vote getter
274-
minVote := 1000000 //the smallest number of votes received thus far (to find who is in last)
275-
minPerson := make([]string, 0) //the person(s) with the least votes that need removed
276-
for person, vote := range tallied {
277-
if vote < minVote { // this should always be true round one, to set a true "who is in last"
278-
minVote = vote
279-
minPerson = make([]string, 0)
280-
minPerson = append(minPerson, person)
281-
} else if vote == minVote {
282-
minPerson = append(minPerson, person)
283-
}
284-
}
285-
eliminated = append(eliminated, minPerson...)
286-
finalResult = append(finalResult, tallied)
287-
// If one person has all the votes, they win
288-
if len(tallied) == 1 {
289-
break
290-
}
291-
292-
end := true
293-
for str, val := range tallied {
294-
// if any particular entry is above half remaining votes, they win and it ends
295-
if val > (voteCount / 2) {
296-
finalResult = append(finalResult, map[string]int{str: val})
297-
end = true
298-
break
299-
}
300-
// Check if all values in tallied are the same
301-
// In that case, it's a tie?
302-
if val != minVote {
303-
end = false
304-
break
305-
}
306-
}
307-
if end {
308-
break
309-
}
310-
}
311-
return finalResult, nil
324+
return calculateRankedResult(votesRaw)
312325
}
313326
return nil, nil
314327
}

database/poll_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package database
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func makeVotes() []RankedVote {
10+
// so inpyt for this, we want to have option, then a list of ranks.
11+
// am tempted to have some shorthand for generating test cases more easily
12+
return []RankedVote{}
13+
}
14+
15+
func TestResultCalcs(t *testing.T) {
16+
// for votes, we only need to define options, we don't currently rely on IDs
17+
tests := []struct {
18+
name string
19+
votes []RankedVote
20+
results []map[string]int
21+
err error
22+
}{
23+
{
24+
name: "Empty Votes",
25+
votes: []RankedVote{
26+
{
27+
Options: map[string]int{},
28+
},
29+
},
30+
results: []map[string]int{
31+
{},
32+
},
33+
},
34+
{
35+
name: "1 vote",
36+
votes: []RankedVote{
37+
{
38+
Options: map[string]int{
39+
"first": 1,
40+
"second": 2,
41+
"third": 3,
42+
},
43+
},
44+
},
45+
results: []map[string]int{
46+
{
47+
"first": 1,
48+
},
49+
},
50+
},
51+
{
52+
name: "Tie vote",
53+
votes: []RankedVote{
54+
{
55+
Options: map[string]int{
56+
"first": 1,
57+
"second": 2,
58+
},
59+
},
60+
{
61+
Options: map[string]int{
62+
"first": 2,
63+
"second": 1,
64+
},
65+
},
66+
},
67+
results: []map[string]int{
68+
{
69+
"first": 1,
70+
"second": 1,
71+
},
72+
},
73+
},
74+
{
75+
name: "Several Rounds",
76+
votes: []RankedVote{
77+
{
78+
Options: map[string]int{
79+
"a": 1,
80+
"b": 2,
81+
"c": 3,
82+
},
83+
},
84+
{
85+
Options: map[string]int{
86+
"a": 2,
87+
"b": 1,
88+
"c": 3,
89+
},
90+
},
91+
{
92+
Options: map[string]int{
93+
"a": 1,
94+
"b": 2,
95+
"c": 3,
96+
},
97+
},
98+
{
99+
Options: map[string]int{
100+
"a": 2,
101+
"b": 1,
102+
"c": 3,
103+
},
104+
},
105+
{
106+
Options: map[string]int{
107+
"a": 2,
108+
"b": 3,
109+
"c": 1,
110+
},
111+
},
112+
},
113+
results: []map[string]int{
114+
{
115+
"a": 2,
116+
"b": 2,
117+
"c": 1,
118+
},
119+
{
120+
"a": 3,
121+
"b": 2,
122+
},
123+
{
124+
"a": 3,
125+
},
126+
},
127+
},
128+
}
129+
for _, test := range tests {
130+
t.Run(test.name, func(t *testing.T) {
131+
results, err := calculateRankedResult(test.votes)
132+
assert.Equal(t, test.results, results)
133+
assert.Equal(t, test.err, err)
134+
})
135+
}
136+
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gin-gonic/gin v1.11.0
88
github.com/sirupsen/logrus v1.9.3
99
github.com/slack-go/slack v0.17.3
10+
github.com/stretchr/testify v1.11.1
1011
go.mongodb.org/mongo-driver v1.17.6
1112
mvdan.cc/xurls/v2 v2.6.0
1213
)
@@ -17,6 +18,7 @@ require (
1718
github.com/bytedance/sonic/loader v0.4.0 // indirect
1819
github.com/cloudwego/base64x v0.1.6 // indirect
1920
github.com/coreos/go-oidc v2.4.0+incompatible // indirect
21+
github.com/davecgh/go-spew v1.1.1 // indirect
2022
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
2123
github.com/gin-contrib/sse v1.1.0 // indirect
2224
github.com/go-playground/locales v0.14.1 // indirect
@@ -36,6 +38,7 @@ require (
3638
github.com/modern-go/reflect2 v1.0.2 // indirect
3739
github.com/montanaflynn/stats v0.7.1 // indirect
3840
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
41+
github.com/pmezard/go-difflib v1.0.0 // indirect
3942
github.com/pquerna/cachecontrol v0.2.0 // indirect
4043
github.com/quic-go/qpack v0.5.1 // indirect
4144
github.com/quic-go/quic-go v0.55.0 // indirect
@@ -57,4 +60,5 @@ require (
5760
golang.org/x/tools v0.38.0 // indirect
5861
google.golang.org/protobuf v1.36.10 // indirect
5962
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
63+
gopkg.in/yaml.v3 v3.0.1 // indirect
6064
)

0 commit comments

Comments
 (0)