Add testing and fix tons of bugs
continuous-integration/drone/push Build was killed Details

This commit is contained in:
Hamcha 2019-06-28 16:15:25 +02:00
parent 5158ab539a
commit 4a521f595d
8 changed files with 272 additions and 35 deletions

View File

@ -7,14 +7,14 @@ steps:
commands:
- cd ./draftbot
- GOPROXY=https://modules.fromouter.space go mod download
- CGO_ENABLED=0 go test .
- CGO_ENABLED=0 go test ./...
- name: build_draftbot
image: golang
commands:
- cd ./draftbot
- GOPROXY=https://modules.fromouter.space go mod download
- CGO_ENABLED=0 go install .
- CGO_ENABLED=0 go install ./...
volumes:
- name: gopath
path: /go

66
draftbot/bot/bot_test.go Normal file
View File

@ -0,0 +1,66 @@
package bot_test
import (
"testing"
"git.fromouter.space/mcg/mlp-server-tools/draftbot/bot"
"git.fromouter.space/mcg/draft"
)
const PACKSIZE = 5
// Test set that can be used by tests that don't need special features (like alternates)
var testSet = &draft.GenericSet{
Cards: []draft.Card{{ID: "a"}, {ID: "b"}, {ID: "c"}},
PackSize: PACKSIZE,
}
func TestPick(t *testing.T) {
const PacksPerPlayer = 3
const PlayersPerPod = 5
// Get provider for test set
testProvider := draft.PacksFromSet(PacksPerPlayer, testSet)
// Create pod
pod := draft.MakePod(PlayersPerPod, testProvider)
// Create a bot for each player in the pod
var bots []*bot.Bot
for _, player := range pod.Players {
bots = append(bots, bot.MakeBot(player))
}
// Simulate a round of drafting
// Repeat until all packs are gone
// Channels are tested when they should trigger
for packnum := 0; packnum < PacksPerPlayer; packnum++ {
// Open new packs!
err := pod.OpenPacks()
if err != nil {
t.Fatalf("Got an error while opening packs #%d: %s", packnum, err.Error())
}
for picknum := 0; picknum < PACKSIZE; picknum++ {
for _, bot := range bots {
bot.PickNext()
}
// Make sure either ReadyNextPick or ReadyNextPack triggers
select {
case <-pod.ReadyNextPick:
// Pass packs around
err := pod.NextPacks()
if err != nil {
t.Fatalf("Got an error while passing packs: %s", err.Error())
}
case <-pod.ReadyNextPack:
break
default:
t.Fatal("Either ReadyNextPick/ReadyNextPack should trigger but neither has")
}
}
}
}

View File

@ -29,6 +29,7 @@ func NewDraftBot(botAPI BotInterface, name string) *DraftBot {
return &DraftBot{
API: botAPI,
Name: name,
Rooms: make(map[string]roomInfo),
Sessions: make(map[string]*session),
}
}
@ -37,7 +38,7 @@ func NewDraftBot(botAPI BotInterface, name string) *DraftBot {
func (d *DraftBot) OnMessage(msg room.ServerMessage) {
switch msg.Type {
case room.MsgMessage:
if *logAll {
if logAll {
logger.Log("event", "message",
"roomid", msg.RoomID,
"from", msg.Message.From,
@ -49,7 +50,7 @@ func (d *DraftBot) OnMessage(msg room.ServerMessage) {
d.handleMessage(msg.RoomID, *msg.Message)
}
case room.MsgEvent:
if *logAll {
if logAll {
logger.Log("event", "event",
"roomid", msg.RoomID,
"content", msg.Event.Message)

View File

@ -103,11 +103,7 @@ func (d *DraftBot) cmdPickCard(roomid string, msg room.Message) {
func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) {
// Get session options from data
type sessionOptions struct {
players int
options draftOptions
}
var opt sessionOptions
var opt SessionOptions
err := mapstructure.Decode(msg.Data, &opt)
if err != nil {
d.sendMessage(roomid, room.Message{
@ -118,7 +114,7 @@ func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) {
return
}
sess, err := newSession(opt.players, opt.options)
sess, err := newSession(opt.Players, opt.Options)
if err != nil {
d.sendMessage(roomid, room.Message{
To: msg.From,
@ -140,7 +136,7 @@ func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) {
Channel: "draft",
Type: "session-open",
Data: opt,
Message: fmt.Sprintf("Created a new draft session for %d players, type: %s", opt.players, opt.options.Type),
Message: fmt.Sprintf("Created a new draft session for %d players, type: %s", opt.Players, opt.Options.Type),
})
}

View File

@ -2,12 +2,19 @@ package main_test
import (
"testing"
"time"
"git.fromouter.space/mcg/draft/mlp"
"git.fromouter.space/mcg/cardgage/client/bot"
lobby "git.fromouter.space/mcg/cardgage/lobby/proto"
room "git.fromouter.space/mcg/cardgage/room/api"
draft "git.fromouter.space/mcg/mlp-server-tools/draftbot"
)
const TestBotName = "test-bot"
const TestRoomName = "test-room"
type MockServer struct {
in chan room.ServerMessage
out chan room.BotMessage
@ -35,8 +42,163 @@ func (m *MockServer) Bind(fn bot.MessageHandler) {
func TestDraftSession(t *testing.T) {
mock := makeMockServer()
draftbot := draft.NewDraftBot(mock, "bot")
draftbot := draft.NewDraftBot(mock, TestBotName)
go mock.Bind(draftbot.OnMessage)
//TODO sample session
// Create a new room
mock.in <- room.ServerMessage{
RoomID: TestRoomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: TestRoomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
// Create new session with a fake message from owner
mock.message("test-owner", "create", draft.SessionOptions{
Players: 8, // Two players, six bots
Options: draft.DraftOptions{
Type: draft.DraftSet,
Positioning: draft.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect(t, "session-open", 5)
// Join session as owner
mock.message("test-owner", "join", nil)
mock.expect(t, "player-joined-session", 5)
// .. and as second player
mock.message("test-guest", "join", nil)
mock.expect(t, "player-joined-session", 5)
// Try to start the session
mock.message("test-owner", "start", nil)
// These two can happen in any order, so they get their own special check
mock.multiexpect(t, 5, "session-start", "draft-order")
//TODO make players pick cards etc
// Close the room
mock.in <- room.ServerMessage{
RoomID: TestRoomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtRoomClosed,
},
}
}
func TestDraftSessionButEverythingGoesWrong(t *testing.T) {
mock := makeMockServer()
draftbot := draft.NewDraftBot(mock, TestBotName)
go mock.Bind(draftbot.OnMessage)
// Create a new room
mock.in <- room.ServerMessage{
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: TestRoomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
// Try creating a new session as NOT the owner
mock.message("test-guest", "create", draft.SessionOptions{
Players: 8, // Two players, six bots
Options: draft.DraftOptions{
Type: draft.DraftSet,
Positioning: draft.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect(t, "must-be-owner", 5)
//TODO:
// Try to start session when session doesn't exist
// Try to create session twice
// Try to start session with no players
// Try to start session as not the owner
// Try to make too many players join a session
}
func (m *MockServer) expect(t *testing.T, typ string, timeout int) {
for {
select {
case msg := <-m.out:
// Skip all actions
if msg.Type != room.MsgMessage {
continue
}
// Check expected type
if msg.Message.Type != typ {
// Oh noes
t.Fatalf("Expected message \"%s\" but got \"%s\"", typ, msg.Message.Type)
}
return
case <-time.After(time.Duration(timeout) * time.Second):
t.Fatalf("Expected message \"%s\" but found nothing (timeout after %d seconds)!", typ, timeout)
}
}
}
func (m *MockServer) multiexpect(t *testing.T, timeout int, types ...string) {
for {
if len(types) < 1 {
return
}
select {
case msg := <-m.out:
// Skip all actions
if msg.Type != room.MsgMessage {
continue
}
// Check expected type
found := false
for i, typ := range types {
if typ == msg.Message.Type {
found = true
types[i] = types[len(types)-1]
types = types[:len(types)-1]
break
}
}
if !found {
// Oh noes
t.Fatalf("Expected one of %s but got \"%s\"", types, msg.Message.Type)
}
return
case <-time.After(time.Duration(timeout) * time.Second):
t.Fatalf("Expected one of %s but found nothing (timeout after %d seconds)!", types, timeout)
}
}
}
func (m *MockServer) message(from string, typ string, data interface{}) {
m.in <- room.ServerMessage{
RoomID: TestRoomName,
Type: room.MsgMessage,
Message: &room.Message{
From: from,
To: TestBotName,
Type: typ,
Data: data,
},
}
}

View File

@ -32,6 +32,7 @@ github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -105,6 +106,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.13 h1:x7DQtkU0cedzeS8TD36tT/w1Hm4rDtfCaYYAHE7TTBI=
github.com/miekg/dns v1.1.13/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -144,6 +146,7 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=

View File

@ -17,14 +17,15 @@ import (
)
var logger log.Logger
var logAll *bool
var logAll bool
func main() {
consulAddr := flag.String("consul.addr", "consul:8500", "Consul address")
botName := flag.String("bot.name", "draftbot", "Bot name")
gameFilter := flag.String("filter.game", "mlpccg-mcg", "What game to filter for (separated by comma)")
tagFilter := flag.String("filter.tag", "draft", "What tags to filter for (separated by comma)")
logAll = flag.Bool("debug.log", false, "Log a lot of stuff")
flag.BoolVar(&logAll, "debug.log", false, "Log a lot of stuff")
flag.Parse()
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)

View File

@ -20,7 +20,7 @@ var (
)
type session struct {
Options draftOptions
Options DraftOptions
Players map[string]*draft.Player
Bots []*bot.Bot
Pod *draft.Pod
@ -35,27 +35,34 @@ type session struct {
// Types of drafts
const (
draftBlock = "block"
draftSet = "set"
draftCube = "cube"
draftI8PCube = "i8pcube"
DraftBlock = "block"
DraftSet = "set"
DraftCube = "cube"
DraftI8PCube = "i8pcube"
)
// Ways in which players can be positioned along the draft pod
const (
posRandom = "random" // Place players randomly
posEven = "even" // Place players spaced as evenly as possible
PosRandom = "random" // Place players randomly
PosEven = "even" // Place players spaced as evenly as possible
)
type draftOptions struct {
// SessionOptions is the data contained in a create session request
type SessionOptions struct {
Players int `json:"players"`
Options DraftOptions `json:"options"`
}
// DraftOptions are the options needed for a draft session
type DraftOptions struct {
Type string `json:"type"`
Positioning string `json:"positioning"`
// Block draft properties
Block string `json:"block,omitempty"`
Block mlp.BlockID `json:"block,omitempty"`
// Set draft properties
Set string `json:"set,omitempty"`
Set mlp.SetID `json:"set,omitempty"`
// Cube draft properties
CubeURL string `json:"cube_url,omitempty"`
@ -69,17 +76,17 @@ type draftOptions struct {
PackCount int `json:"pack_count,omitempty"` // Set and Cube
}
func (do draftOptions) getProvider() (draft.PackProvider, error) {
func (do DraftOptions) getProvider() (draft.PackProvider, error) {
switch do.Type {
case draftBlock:
return mlp.BlockPacks(mlp.BlockID(do.Block))
case draftSet:
set, err := mlp.LoadSetHTTP(mlp.SetID(do.Set))
case DraftBlock:
return mlp.BlockPacks(do.Block)
case DraftSet:
set, err := mlp.LoadSetHTTP(do.Set)
if err != nil {
return nil, err
}
return draft.PacksFromSet(do.PackCount, set), nil
case draftCube:
case DraftCube:
cards, err := loadCube(do.CubeURL)
if err != nil {
return nil, err
@ -89,7 +96,7 @@ func (do draftOptions) getProvider() (draft.PackProvider, error) {
PackSize: do.PackSize,
}
return draft.PacksFromSet(do.PackCount, cube), nil
case draftI8PCube:
case DraftI8PCube:
cube, err := loadI8PCube(do.CubeURL)
if err != nil {
return nil, err
@ -99,7 +106,7 @@ func (do draftOptions) getProvider() (draft.PackProvider, error) {
return nil, errors.New("unknown draft type")
}
func newSession(playerCount int, opt draftOptions) (*session, error) {
func newSession(playerCount int, opt DraftOptions) (*session, error) {
// Get pack provider for given options
provider, err := opt.getProvider()
if err != nil {
@ -109,6 +116,7 @@ func newSession(playerCount int, opt draftOptions) (*session, error) {
return &session{
Options: opt,
Pod: draft.MakePod(playerCount, provider),
Players: make(map[string]*draft.Player),
messages: make(chan room.Message),
exit: make(chan bool),
}, nil
@ -130,7 +138,7 @@ func (s *session) Start() error {
playerSpot := make(map[int]string)
switch s.Options.Positioning {
case posRandom:
case PosRandom:
// Assign a random number to each player
for pname := range s.Players {
var pos int
@ -143,7 +151,7 @@ func (s *session) Start() error {
}
playerSpot[pos] = pname
}
case posEven:
case PosEven:
// Space players evenly
playerRatio := float64(spots) / float64(players)
i := 0