From 538e1ab7bc8cc2386985e43d90c4ca87a12847dc Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 28 Aug 2017 20:14:32 -0700 Subject: [PATCH] Refactor field models and logic into separate package. --- alliance_station_display.go | 73 +- alliance_station_display_test.go | 36 +- announcer_display.go | 62 +- announcer_display_test.go | 32 +- api.go | 24 +- api_test.go | 38 +- arena_test.go | 477 ------------ audience_display.go | 58 +- audience_display_test.go | 34 +- .../access_point.go | 73 +- .../access_point_test.go | 4 +- arena.go => field/arena.go | 720 +++++++++--------- field/arena_test.go | 472 ++++++++++++ .../bandwidth_monitor.go | 45 +- .../driver_station_connection.go | 64 +- .../driver_station_connection_test.go | 101 +-- switch_config.go => field/network_switch.go | 42 +- .../network_switch_test.go | 22 +- notifier.go => field/notifier.go | 2 +- notifier_test.go => field/notifier_test.go | 2 +- field/realtime_score.go | 22 + team_match_log.go => field/team_match_log.go | 9 +- field/test_helpers.go | 28 + fta_display.go | 18 +- fta_display_test.go | 4 +- lights.go | 259 ------- main.go | 42 +- match_play.go | 172 ++--- match_play_test.go | 217 +++--- match_review.go | 50 +- match_review_test.go | 46 +- model/database.go | 13 +- model/database_test.go | 4 +- model/test_helpers.go | 11 +- partner/tba_test.go | 2 +- pit_display.go | 12 +- pit_display_test.go | 10 +- referee_display.go | 79 +- referee_display_test.go | 71 +- reports.go | 50 +- reports_test.go | 56 +- scoring_display.go | 43 +- scoring_display_test.go | 31 +- setup_alliance_selection.go | 100 +-- setup_alliance_selection_test.go | 137 ++-- setup_field.go | 31 +- setup_field_test.go | 19 +- setup_lower_thirds.go | 52 +- setup_lower_thirds_test.go | 28 +- setup_schedule.go | 52 +- setup_schedule_test.go | 44 +- setup_settings.go | 77 +- setup_settings_test.go | 67 +- setup_sponsor_slides.go | 20 +- setup_sponsor_slides_test.go | 24 +- setup_teams.go | 92 +-- setup_teams_test.go | 82 +- test_helpers.go | 15 +- tournament/test_helpers.go | 2 +- web.go | 266 +++---- web_test.go | 16 +- websocket.go | 62 ++ 62 files changed, 2317 insertions(+), 2499 deletions(-) delete mode 100644 arena_test.go rename access_point_config.go => field/access_point.go (74%) rename access_point_config_test.go => field/access_point_test.go (98%) rename arena.go => field/arena.go (54%) create mode 100644 field/arena_test.go rename bandwidth_monitor.go => field/bandwidth_monitor.go (74%) rename driver_station_connection.go => field/driver_station_connection.go (86%) rename driver_station_connection_test.go => field/driver_station_connection_test.go (69%) rename switch_config.go => field/network_switch.go (77%) rename switch_config_test.go => field/network_switch_test.go (79%) rename notifier.go => field/notifier.go (99%) rename notifier_test.go => field/notifier_test.go (99%) create mode 100644 field/realtime_score.go rename team_match_log.go => field/team_match_log.go (82%) create mode 100644 field/test_helpers.go delete mode 100644 lights.go create mode 100644 websocket.go diff --git a/alliance_station_display.go b/alliance_station_display.go index 684b380..f05e1c0 100644 --- a/alliance_station_display.go +++ b/alliance_station_display.go @@ -17,12 +17,12 @@ import ( ) // Renders the team number and status display shown above each alliance station. -func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) allianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err := template.ParseFiles("templates/alliance_station_display.html") if err != nil { handleWebErr(w, err) @@ -38,7 +38,7 @@ func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings DisplayId string - }{eventSettings, displayId} + }{web.arena.EventSettings, displayId} err = template.ExecuteTemplate(w, "alliance_station_display.html", data) if err != nil { handleWebErr(w, err) @@ -47,8 +47,8 @@ func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the alliance station display client to receive status updates. -func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } @@ -60,34 +60,35 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque defer websocket.Close() displayId := r.URL.Query()["displayId"][0] - station, ok := mainArena.allianceStationDisplays[displayId] + station, ok := web.arena.AllianceStationDisplays[displayId] if !ok { station = "" - mainArena.allianceStationDisplays[displayId] = station + web.arena.AllianceStationDisplays[displayId] = station } rankings := make(map[string]*game.Ranking) - for _, allianceStation := range mainArena.AllianceStations { + for _, allianceStation := range web.arena.AllianceStations { if allianceStation.Team != nil { - rankings[strconv.Itoa(allianceStation.Team.Id)], _ = db.GetRankingForTeam(allianceStation.Team.Id) + rankings[strconv.Itoa(allianceStation.Team.Id)], _ = + web.arena.Database.GetRankingForTeam(allianceStation.Team.Id) } } - allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen() + allianceStationDisplayListener := web.arena.AllianceStationDisplayNotifier.Listen() defer close(allianceStationDisplayListener) - matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen() + matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen() defer close(matchLoadTeamsListener) - robotStatusListener := mainArena.robotStatusNotifier.Listen() + robotStatusListener := web.arena.RobotStatusNotifier.Listen() defer close(robotStatusListener) - matchTimeListener := mainArena.matchTimeNotifier.Listen() + matchTimeListener := web.arena.MatchTimeNotifier.Listen() defer close(matchTimeListener) - realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen() + realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen() defer close(realtimeScoreListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Send the various notifications immediately upon connection. var data interface{} - err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen) + err = websocket.Write("setAllianceStationDisplay", web.arena.AllianceStationDisplayScreen) if err != nil { log.Printf("Websocket error: %s", err) return @@ -97,7 +98,7 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque log.Printf("Websocket error: %s", err) return } - err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}) + err = websocket.Write("matchTime", MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)}) if err != nil { log.Printf("Websocket error: %s", err) return @@ -106,10 +107,10 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque AllianceStation string Teams map[string]*model.Team Rankings map[string]*game.Ranking - }{station, map[string]*model.Team{"R1": mainArena.AllianceStations["R1"].Team, - "R2": mainArena.AllianceStations["R2"].Team, "R3": mainArena.AllianceStations["R3"].Team, - "B1": mainArena.AllianceStations["B1"].Team, "B2": mainArena.AllianceStations["B2"].Team, - "B3": mainArena.AllianceStations["B3"].Team}, rankings} + }{station, map[string]*model.Team{"R1": web.arena.AllianceStations["R1"].Team, + "R2": web.arena.AllianceStations["R2"].Team, "R3": web.arena.AllianceStations["R3"].Team, + "B1": web.arena.AllianceStations["B1"].Team, "B2": web.arena.AllianceStations["B2"].Team, + "B3": web.arena.AllianceStations["B3"].Team}, rankings} err = websocket.Write("setMatch", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -118,7 +119,7 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque data = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} err = websocket.Write("realtimeScore", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -135,42 +136,42 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque if !ok { return } - websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}) + websocket.Write("matchTime", MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)}) messageType = "setAllianceStationDisplay" - message = mainArena.allianceStationDisplayScreen + message = web.arena.AllianceStationDisplayScreen case _, ok := <-matchLoadTeamsListener: if !ok { return } messageType = "setMatch" - station = mainArena.allianceStationDisplays[displayId] + station = web.arena.AllianceStationDisplays[displayId] rankings := make(map[string]*game.Ranking) - for _, allianceStation := range mainArena.AllianceStations { + for _, allianceStation := range web.arena.AllianceStations { if allianceStation.Team != nil { rankings[strconv.Itoa(allianceStation.Team.Id)], _ = - db.GetRankingForTeam(allianceStation.Team.Id) + web.arena.Database.GetRankingForTeam(allianceStation.Team.Id) } } message = struct { AllianceStation string Teams map[string]*model.Team Rankings map[string]*game.Ranking - }{station, map[string]*model.Team{"R1": mainArena.AllianceStations["R1"].Team, - "R2": mainArena.AllianceStations["R2"].Team, "R3": mainArena.AllianceStations["R3"].Team, - "B1": mainArena.AllianceStations["B1"].Team, "B2": mainArena.AllianceStations["B2"].Team, - "B3": mainArena.AllianceStations["B3"].Team}, rankings} + }{station, map[string]*model.Team{"R1": web.arena.AllianceStations["R1"].Team, + "R2": web.arena.AllianceStations["R2"].Team, "R3": web.arena.AllianceStations["R3"].Team, + "B1": web.arena.AllianceStations["B1"].Team, "B2": web.arena.AllianceStations["B2"].Team, + "B3": web.arena.AllianceStations["B3"].Team}, rankings} case _, ok := <-robotStatusListener: if !ok { return } messageType = "status" - message = mainArena + message = web.arena.GetStatus() case matchTimeSec, ok := <-matchTimeListener: if !ok { return } messageType = "matchTime" - message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} + message = MatchTimeMessage{web.arena.MatchState, matchTimeSec.(int)} case _, ok := <-realtimeScoreListener: if !ok { return @@ -179,7 +180,7 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque message = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} case _, ok := <-reloadDisplaysListener: if !ok { return @@ -215,7 +216,7 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) continue } - mainArena.allianceStationDisplays[displayId] = station + web.arena.AllianceStationDisplays[displayId] = station default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) } diff --git a/alliance_station_display_test.go b/alliance_station_display_test.go index 4d9619b..e23c316 100644 --- a/alliance_station_display_test.go +++ b/alliance_station_display_test.go @@ -12,17 +12,17 @@ import ( ) func TestAllianceStationDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/alliance_station") + recorder := web.getHttpResponse("/displays/alliance_station") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Alliance Station Display - Untitled Event - Cheesy Arena") } func TestAllianceStationDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/alliance_station/websocket?displayId=1", nil) assert.Nil(t, err) @@ -37,33 +37,33 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) { readWebsocketType(t, ws, "realtimeScore") // Change to a different screen. - mainArena.allianceStationDisplayScreen = "logo" - mainArena.allianceStationDisplayNotifier.Notify(nil) + web.arena.AllianceStationDisplayScreen = "logo" + web.arena.AllianceStationDisplayNotifier.Notify(nil) readWebsocketType(t, ws, "matchTime") readWebsocketType(t, ws, "setAllianceStationDisplay") // Inform the server what display ID this is. - assert.Equal(t, "", mainArena.allianceStationDisplays["1"]) + assert.Equal(t, "", web.arena.AllianceStationDisplays["1"]) ws.Write("setAllianceStation", "R3") time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed. - assert.Equal(t, "R3", mainArena.allianceStationDisplays["1"]) + assert.Equal(t, "R3", web.arena.AllianceStationDisplays["1"]) // Run through a match cycle. - mainArena.matchLoadTeamsNotifier.Notify(nil) + web.arena.MatchLoadTeamsNotifier.Notify(nil) readWebsocketType(t, ws, "setMatch") - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - mainArena.StartMatch() - mainArena.Update() + web.arena.AllianceStations["R1"].Bypass = true + web.arena.AllianceStations["R2"].Bypass = true + web.arena.AllianceStations["R3"].Bypass = true + web.arena.AllianceStations["B1"].Bypass = true + web.arena.AllianceStations["B2"].Bypass = true + web.arena.AllianceStations["B3"].Bypass = true + web.arena.StartMatch() + web.arena.Update() messages := readWebsocketMultiple(t, ws, 2) _, ok := messages["status"] assert.True(t, ok) _, ok = messages["matchTime"] assert.True(t, ok) - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) readWebsocketType(t, ws, "realtimeScore") } diff --git a/announcer_display.go b/announcer_display.go index 40e0f17..94e320f 100644 --- a/announcer_display.go +++ b/announcer_display.go @@ -16,12 +16,12 @@ import ( ) // Renders the announcer display which shows team info and scores for the current match. -func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) announcerDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err := template.ParseFiles("templates/announcer_display.html", "templates/base.html") if err != nil { handleWebErr(w, err) @@ -29,7 +29,7 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) { } data := struct { *model.EventSettings - }{eventSettings} + }{web.arena.EventSettings} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -38,8 +38,8 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the announcer display client to send control commands and receive status updates. -func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } @@ -50,17 +50,17 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen() + matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen() defer close(matchLoadTeamsListener) - matchTimeListener := mainArena.matchTimeNotifier.Listen() + matchTimeListener := web.arena.MatchTimeNotifier.Listen() defer close(matchTimeListener) - realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen() + realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen() defer close(realtimeScoreListener) - scorePostedListener := mainArena.scorePostedNotifier.Listen() + scorePostedListener := web.arena.ScorePostedNotifier.Listen() defer close(scorePostedListener) - audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen() + audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen() defer close(audienceDisplayListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Send the various notifications immediately upon connection. @@ -74,10 +74,10 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { Blue1 *model.Team Blue2 *model.Team Blue3 *model.Team - }{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName, - mainArena.AllianceStations["R1"].Team, mainArena.AllianceStations["R2"].Team, - mainArena.AllianceStations["R3"].Team, mainArena.AllianceStations["B1"].Team, - mainArena.AllianceStations["B2"].Team, mainArena.AllianceStations["B3"].Team} + }{web.arena.CurrentMatch.CapitalizedType(), web.arena.CurrentMatch.DisplayName, + web.arena.AllianceStations["R1"].Team, web.arena.AllianceStations["R2"].Team, + web.arena.AllianceStations["R3"].Team, web.arena.AllianceStations["B1"].Team, + web.arena.AllianceStations["B2"].Team, web.arena.AllianceStations["B3"].Team} err = websocket.Write("setMatch", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -88,7 +88,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}) + err = websocket.Write("matchTime", MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)}) if err != nil { log.Printf("Websocket error: %s", err) return @@ -96,7 +96,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { data = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} err = websocket.Write("realtimeScore", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -123,16 +123,16 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { Blue1 *model.Team Blue2 *model.Team Blue3 *model.Team - }{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName, - mainArena.AllianceStations["R1"].Team, mainArena.AllianceStations["R2"].Team, - mainArena.AllianceStations["R3"].Team, mainArena.AllianceStations["B1"].Team, - mainArena.AllianceStations["B2"].Team, mainArena.AllianceStations["B3"].Team} + }{web.arena.CurrentMatch.CapitalizedType(), web.arena.CurrentMatch.DisplayName, + web.arena.AllianceStations["R1"].Team, web.arena.AllianceStations["R2"].Team, + web.arena.AllianceStations["R3"].Team, web.arena.AllianceStations["B1"].Team, + web.arena.AllianceStations["B2"].Team, web.arena.AllianceStations["B3"].Team} case matchTimeSec, ok := <-matchTimeListener: if !ok { return } messageType = "matchTime" - message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} + message = MatchTimeMessage{web.arena.MatchState, matchTimeSec.(int)} case _, ok := <-realtimeScoreListener: if !ok { return @@ -141,7 +141,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { message = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} case _, ok := <-scorePostedListener: if !ok { return @@ -156,16 +156,16 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { BlueFouls []game.Foul RedCards map[string]string BlueCards map[string]string - }{mainArena.savedMatch.CapitalizedType(), mainArena.savedMatch.DisplayName, - mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary(), - mainArena.savedMatchResult.RedScore.Fouls, mainArena.savedMatchResult.BlueScore.Fouls, - mainArena.savedMatchResult.RedCards, mainArena.savedMatchResult.BlueCards} + }{web.arena.SavedMatch.CapitalizedType(), web.arena.SavedMatch.DisplayName, + web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary(), + web.arena.SavedMatchResult.RedScore.Fouls, web.arena.SavedMatchResult.BlueScore.Fouls, + web.arena.SavedMatchResult.RedCards, web.arena.SavedMatchResult.BlueCards} case _, ok := <-audienceDisplayListener: if !ok { return } messageType = "setAudienceDisplay" - message = mainArena.audienceDisplayScreen + message = web.arena.AudienceDisplayScreen case _, ok := <-reloadDisplaysListener: if !ok { return @@ -201,8 +201,8 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) continue } - mainArena.audienceDisplayScreen = screen - mainArena.audienceDisplayNotifier.Notify(nil) + web.arena.AudienceDisplayScreen = screen + web.arena.AudienceDisplayNotifier.Notify(nil) default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) } diff --git a/announcer_display_test.go b/announcer_display_test.go index 580b62e..6fac24a 100644 --- a/announcer_display_test.go +++ b/announcer_display_test.go @@ -12,17 +12,17 @@ import ( ) func TestAnnouncerDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/announcer") + recorder := web.getHttpResponse("/displays/announcer") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Announcer Display - Untitled Event - Cheesy Arena") } func TestAnnouncerDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil) assert.Nil(t, err) @@ -35,28 +35,28 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) { readWebsocketType(t, ws, "matchTime") readWebsocketType(t, ws, "realtimeScore") - mainArena.matchLoadTeamsNotifier.Notify(nil) + web.arena.MatchLoadTeamsNotifier.Notify(nil) readWebsocketType(t, ws, "setMatch") - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - mainArena.StartMatch() - mainArena.Update() + web.arena.AllianceStations["R1"].Bypass = true + web.arena.AllianceStations["R2"].Bypass = true + web.arena.AllianceStations["R3"].Bypass = true + web.arena.AllianceStations["B1"].Bypass = true + web.arena.AllianceStations["B2"].Bypass = true + web.arena.AllianceStations["B3"].Bypass = true + web.arena.StartMatch() + web.arena.Update() messages := readWebsocketMultiple(t, ws, 2) _, ok := messages["setAudienceDisplay"] assert.True(t, ok) _, ok = messages["matchTime"] assert.True(t, ok) - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) readWebsocketType(t, ws, "realtimeScore") - mainArena.scorePostedNotifier.Notify(nil) + web.arena.ScorePostedNotifier.Notify(nil) readWebsocketType(t, ws, "setFinalScore") // Test triggering the final score screen. ws.Write("setAudienceDisplay", "score") time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed. - assert.Equal(t, "score", mainArena.audienceDisplayScreen) + assert.Equal(t, "score", web.arena.AudienceDisplayScreen) } diff --git a/api.go b/api.go index 90fb2c3..ea377e8 100644 --- a/api.go +++ b/api.go @@ -30,13 +30,13 @@ type RankingWithNickname struct { } // Generates a JSON dump of the matches and results. -func MatchesApiHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) matchesApiHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } vars := mux.Vars(r) - matches, err := db.GetMatchesByType(vars["type"]) + matches, err := web.arena.Database.GetMatchesByType(vars["type"]) if err != nil { handleWebErr(w, err) return @@ -45,7 +45,7 @@ func MatchesApiHandler(w http.ResponseWriter, r *http.Request) { matchesWithResults := make([]MatchWithResult, len(matches)) for i, match := range matches { matchesWithResults[i].Match = match - matchResult, err := db.GetMatchResultForMatch(match.Id) + matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) if err != nil { handleWebErr(w, err) return @@ -74,12 +74,12 @@ func MatchesApiHandler(w http.ResponseWriter, r *http.Request) { } // Generates a JSON dump of the sponsor slides for use by the audience display. -func SponsorSlidesApiHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) sponsorSlidesApiHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - sponsors, err := db.GetAllSponsorSlides() + sponsors, err := web.arena.Database.GetAllSponsorSlides() if err != nil { handleWebErr(w, err) return @@ -100,12 +100,12 @@ func SponsorSlidesApiHandler(w http.ResponseWriter, r *http.Request) { } // Generates a JSON dump of the qualification rankings, primarily for use by the pit display. -func RankingsApiHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) rankingsApiHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - rankings, err := db.GetAllRankings() + rankings, err := web.arena.Database.GetAllRankings() if err != nil { handleWebErr(w, err) return @@ -119,7 +119,7 @@ func RankingsApiHandler(w http.ResponseWriter, r *http.Request) { } // Get team info so that nicknames can be displayed. - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -133,7 +133,7 @@ func RankingsApiHandler(w http.ResponseWriter, r *http.Request) { } // Get the last match scored so we can report that on the display. - matches, err := db.GetMatchesByType("qualification") + matches, err := web.arena.Database.GetMatchesByType("qualification") if err != nil { handleWebErr(w, err) return diff --git a/api_test.go b/api_test.go index 7149c92..05116c5 100644 --- a/api_test.go +++ b/api_test.go @@ -13,7 +13,7 @@ import ( ) func TestMatchesApi(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match1 := model.Match{Type: "qualification", DisplayName: "1", Time: time.Unix(0, 0), Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6, Blue1IsSurrogate: true, Blue2IsSurrogate: true, Blue3IsSurrogate: true} @@ -21,13 +21,13 @@ func TestMatchesApi(t *testing.T) { Blue1: 10, Blue2: 11, Blue3: 12, Red1IsSurrogate: true, Red2IsSurrogate: true, Red3IsSurrogate: true} match3 := model.Match{Type: "practice", DisplayName: "1", Time: time.Now(), Red1: 6, Red2: 5, Red3: 4, Blue1: 3, Blue2: 2, Blue3: 1} - db.CreateMatch(&match1) - db.CreateMatch(&match2) - db.CreateMatch(&match3) + web.arena.Database.CreateMatch(&match1) + web.arena.Database.CreateMatch(&match2) + web.arena.Database.CreateMatch(&match3) matchResult1 := model.BuildTestMatchResult(match1.Id, 1) - db.CreateMatchResult(matchResult1) + web.arena.Database.CreateMatchResult(matchResult1) - recorder := getHttpResponse("/api/matches/qualification") + recorder := web.getHttpResponse("/api/matches/qualification") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) var matchesData []MatchWithResult @@ -42,10 +42,10 @@ func TestMatchesApi(t *testing.T) { } func TestRankingsApi(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Test that empty rankings produces an empty array. - recorder := getHttpResponse("/api/rankings") + recorder := web.getHttpResponse("/api/rankings") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) rankingsData := struct { @@ -60,14 +60,14 @@ func TestRankingsApi(t *testing.T) { ranking1 := RankingWithNickname{*game.TestRanking2(), "Simbots"} ranking2 := RankingWithNickname{*game.TestRanking1(), "ChezyPof"} - db.CreateRanking(&ranking1.Ranking) - db.CreateRanking(&ranking2.Ranking) - db.CreateMatch(&model.Match{Type: "qualification", DisplayName: "29", Status: "complete"}) - db.CreateMatch(&model.Match{Type: "qualification", DisplayName: "30"}) - db.CreateTeam(&model.Team{Id: 254, Nickname: "ChezyPof"}) - db.CreateTeam(&model.Team{Id: 1114, Nickname: "Simbots"}) + web.arena.Database.CreateRanking(&ranking1.Ranking) + web.arena.Database.CreateRanking(&ranking2.Ranking) + web.arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "29", Status: "complete"}) + web.arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "30"}) + web.arena.Database.CreateTeam(&model.Team{Id: 254, Nickname: "ChezyPof"}) + web.arena.Database.CreateTeam(&model.Team{Id: 1114, Nickname: "Simbots"}) - recorder = getHttpResponse("/api/rankings") + recorder = web.getHttpResponse("/api/rankings") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) err = json.Unmarshal([]byte(recorder.Body.String()), &rankingsData) @@ -80,14 +80,14 @@ func TestRankingsApi(t *testing.T) { } func TestSponsorSlides(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) slide1 := model.SponsorSlide{1, "subtitle", "line1", "line2", "image", 2} slide2 := model.SponsorSlide{2, "Chezy Sponsaur", "Teh", "Chezy Pofs", "ejface.jpg", 54} - db.CreateSponsorSlide(&slide1) - db.CreateSponsorSlide(&slide2) + web.arena.Database.CreateSponsorSlide(&slide1) + web.arena.Database.CreateSponsorSlide(&slide2) - recorder := getHttpResponse("/api/sponsor_slides") + recorder := web.getHttpResponse("/api/sponsor_slides") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) var sponsorSlides []model.SponsorSlide diff --git a/arena_test.go b/arena_test.go deleted file mode 100644 index 92105e4..0000000 --- a/arena_test.go +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright 2014 Team 254. All Rights Reserved. -// Author: pat@patfairbank.com (Patrick Fairbank) - -package main - -import ( - "bytes" - "github.com/Team254/cheesy-arena/game" - "github.com/Team254/cheesy-arena/model" - "github.com/stretchr/testify/assert" - "log" - "testing" - "time" -) - -func TestAssignTeam(t *testing.T) { - setupTest(t) - - team := model.Team{Id: 254} - err := db.CreateTeam(&team) - assert.Nil(t, err) - err = db.CreateTeam(&model.Team{Id: 1114}) - assert.Nil(t, err) - mainArena.Setup() - - err = mainArena.AssignTeam(254, "B1") - assert.Nil(t, err) - assert.Equal(t, team, *mainArena.AllianceStations["B1"].Team) - dummyDs := &DriverStationConnection{TeamId: 254} - mainArena.AllianceStations["B1"].DsConn = dummyDs - - // Nothing should happen if the same team is assigned to the same station. - err = mainArena.AssignTeam(254, "B1") - assert.Nil(t, err) - assert.Equal(t, team, *mainArena.AllianceStations["B1"].Team) - assert.NotNil(t, mainArena.AllianceStations["B1"]) - assert.Equal(t, dummyDs, mainArena.AllianceStations["B1"].DsConn) // Pointer equality - - // Test reassignment to another team. - err = mainArena.AssignTeam(1114, "B1") - assert.Nil(t, err) - assert.NotEqual(t, team, *mainArena.AllianceStations["B1"].Team) - assert.Nil(t, mainArena.AllianceStations["B1"].DsConn) - - // Check assigning zero as the team number. - err = mainArena.AssignTeam(0, "R2") - assert.Nil(t, err) - assert.Nil(t, mainArena.AllianceStations["R2"].Team) - assert.Nil(t, mainArena.AllianceStations["R2"].DsConn) - - // Check assigning to a non-existent station. - err = mainArena.AssignTeam(254, "R4") - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Invalid alliance station") - } -} - -func TestArenaMatchFlow(t *testing.T) { - setupTest(t) - - db.CreateTeam(&model.Team{Id: 254}) - err := mainArena.AssignTeam(254, "B3") - assert.Nil(t, err) - dummyDs := &DriverStationConnection{TeamId: 254} - mainArena.AllianceStations["B3"].DsConn = dummyDs - - // Check pre-match state and packet timing. - assert.Equal(t, preMatch, mainArena.MatchState) - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - lastPacketCount := mainArena.AllianceStations["B3"].DsConn.packetCount - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-10 * time.Millisecond) - mainArena.Update() - assert.Equal(t, lastPacketCount, mainArena.AllianceStations["B3"].DsConn.packetCount) - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, lastPacketCount+1, mainArena.AllianceStations["B3"].DsConn.packetCount) - - // Check match start, autonomous and transition to teleop. - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].DsConn.RobotLinked = true - err = mainArena.StartMatch() - assert.Nil(t, err) - mainArena.Update() - assert.Equal(t, autoPeriod, mainArena.MatchState) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.Update() - assert.Equal(t, autoPeriod, mainArena.MatchState) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.matchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec) * time.Second) - mainArena.Update() - assert.Equal(t, pausePeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.Update() - assert.Equal(t, pausePeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.matchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec+ - game.MatchTiming.PauseDurationSec) * time.Second) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - - // Check e-stop and bypass. - mainArena.AllianceStations["B3"].EmergencyStop = true - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.AllianceStations["B3"].Bypass = true - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.AllianceStations["B3"].EmergencyStop = false - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.AllianceStations["B3"].Bypass = false - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, teleopPeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - - // Check endgame and match end. - mainArena.matchStartTime = time.Now(). - Add(-time.Duration(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ - game.MatchTiming.TeleopDurationSec-game.MatchTiming.EndgameTimeLeftSec) * time.Second) - mainArena.Update() - assert.Equal(t, endgamePeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.Update() - assert.Equal(t, endgamePeriod, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.matchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec+ - game.MatchTiming.PauseDurationSec+game.MatchTiming.TeleopDurationSec) * time.Second) - mainArena.Update() - assert.Equal(t, postMatch, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - mainArena.Update() - assert.Equal(t, postMatch, mainArena.MatchState) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - - mainArena.AllianceStations["R1"].Bypass = true - mainArena.ResetMatch() - mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond) - mainArena.Update() - assert.Equal(t, preMatch, mainArena.MatchState) - assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto) - assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled) - assert.Equal(t, false, mainArena.AllianceStations["R1"].Bypass) -} - -func TestArenaStateEnforcement(t *testing.T) { - setupTest(t) - - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - - err := mainArena.LoadMatch(new(model.Match)) - assert.Nil(t, err) - err = mainArena.AbortMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") - } - err = mainArena.StartMatch() - assert.Nil(t, err) - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.ResetMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") - } - mainArena.MatchState = autoPeriod - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.ResetMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") - } - mainArena.MatchState = pausePeriod - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.ResetMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") - } - mainArena.MatchState = teleopPeriod - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.ResetMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") - } - mainArena.MatchState = endgamePeriod - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.ResetMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") - } - err = mainArena.AbortMatch() - assert.Nil(t, err) - mainArena.MatchState = postMatch - err = mainArena.LoadMatch(new(model.Match)) - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") - } - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") - } - err = mainArena.AbortMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") - } - - err = mainArena.ResetMatch() - assert.Nil(t, err) - assert.Equal(t, preMatch, mainArena.MatchState) - err = mainArena.ResetMatch() - assert.Nil(t, err) - err = mainArena.LoadMatch(new(model.Match)) - assert.Nil(t, err) -} - -func TestMatchStartRobotLinkEnforcement(t *testing.T) { - setupTest(t) - - db.CreateTeam(&model.Team{Id: 101}) - db.CreateTeam(&model.Team{Id: 102}) - db.CreateTeam(&model.Team{Id: 103}) - db.CreateTeam(&model.Team{Id: 104}) - db.CreateTeam(&model.Team{Id: 105}) - db.CreateTeam(&model.Team{Id: 106}) - match := model.Match{Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - db.CreateMatch(&match) - mainArena.Setup() - - err := mainArena.LoadMatch(&match) - assert.Nil(t, err) - mainArena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 101} - mainArena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 102} - mainArena.AllianceStations["R3"].DsConn = &DriverStationConnection{TeamId: 103} - mainArena.AllianceStations["B1"].DsConn = &DriverStationConnection{TeamId: 104} - mainArena.AllianceStations["B2"].DsConn = &DriverStationConnection{TeamId: 105} - mainArena.AllianceStations["B3"].DsConn = &DriverStationConnection{TeamId: 106} - for _, station := range mainArena.AllianceStations { - station.DsConn.RobotLinked = true - } - err = mainArena.StartMatch() - assert.Nil(t, err) - mainArena.MatchState = preMatch - - // Check with a single team e-stopped, not linked and bypassed. - mainArena.AllianceStations["R1"].EmergencyStop = true - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "while an emergency stop is active") - } - mainArena.AllianceStations["R1"].EmergencyStop = false - mainArena.AllianceStations["R1"].DsConn.RobotLinked = false - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "until all robots are connected or bypassed") - } - mainArena.AllianceStations["R1"].Bypass = true - err = mainArena.StartMatch() - assert.Nil(t, err) - mainArena.AllianceStations["R1"].Bypass = false - mainArena.MatchState = preMatch - - // Check with a team missing. - err = mainArena.AssignTeam(0, "R1") - assert.Nil(t, err) - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "until all robots are connected or bypassed") - } - mainArena.AllianceStations["R1"].Bypass = true - err = mainArena.StartMatch() - assert.Nil(t, err) - mainArena.MatchState = preMatch - - // Check with no teams present. - mainArena.LoadMatch(new(model.Match)) - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "until all robots are connected or bypassed") - } - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - mainArena.AllianceStations["B3"].EmergencyStop = true - err = mainArena.StartMatch() - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "while an emergency stop is active") - } - mainArena.AllianceStations["B3"].EmergencyStop = false - err = mainArena.StartMatch() - assert.Nil(t, err) -} - -func TestLoadNextMatch(t *testing.T) { - setupTest(t) - - db.CreateTeam(&model.Team{Id: 1114}) - practiceMatch1 := model.Match{Type: "practice", DisplayName: "1"} - practiceMatch2 := model.Match{Type: "practice", DisplayName: "2", Status: "complete"} - practiceMatch3 := model.Match{Type: "practice", DisplayName: "3"} - db.CreateMatch(&practiceMatch1) - db.CreateMatch(&practiceMatch2) - db.CreateMatch(&practiceMatch3) - qualificationMatch1 := model.Match{Type: "qualification", DisplayName: "1", Status: "complete"} - qualificationMatch2 := model.Match{Type: "qualification", DisplayName: "2"} - db.CreateMatch(&qualificationMatch1) - db.CreateMatch(&qualificationMatch2) - - // Test match should be followed by another, empty test match. - assert.Equal(t, 0, mainArena.currentMatch.Id) - err := mainArena.SubstituteTeam(1114, "R1") - assert.Nil(t, err) - mainArena.currentMatch.Status = "complete" - err = mainArena.LoadNextMatch() - assert.Nil(t, err) - assert.Equal(t, 0, mainArena.currentMatch.Id) - assert.Equal(t, 0, mainArena.currentMatch.Red1) - assert.NotEqual(t, "complete", mainArena.currentMatch.Status) - - // Other matches should be loaded by type until they're all complete. - err = mainArena.LoadMatch(&practiceMatch2) - assert.Nil(t, err) - err = mainArena.LoadNextMatch() - assert.Nil(t, err) - assert.Equal(t, practiceMatch1.Id, mainArena.currentMatch.Id) - practiceMatch1.Status = "complete" - db.SaveMatch(&practiceMatch1) - err = mainArena.LoadNextMatch() - assert.Nil(t, err) - assert.Equal(t, practiceMatch3.Id, mainArena.currentMatch.Id) - practiceMatch3.Status = "complete" - db.SaveMatch(&practiceMatch3) - err = mainArena.LoadNextMatch() - assert.Nil(t, err) - assert.Equal(t, practiceMatch3.Id, mainArena.currentMatch.Id) - assert.Equal(t, "complete", practiceMatch3.Status) - - err = mainArena.LoadMatch(&qualificationMatch1) - assert.Nil(t, err) - err = mainArena.LoadNextMatch() - assert.Nil(t, err) - assert.Equal(t, qualificationMatch2.Id, mainArena.currentMatch.Id) -} - -func TestSubstituteTeam(t *testing.T) { - setupTest(t) - - db.CreateTeam(&model.Team{Id: 101}) - db.CreateTeam(&model.Team{Id: 102}) - db.CreateTeam(&model.Team{Id: 103}) - db.CreateTeam(&model.Team{Id: 104}) - db.CreateTeam(&model.Team{Id: 105}) - db.CreateTeam(&model.Team{Id: 106}) - db.CreateTeam(&model.Team{Id: 107}) - - // Substitute teams into test match. - err := mainArena.SubstituteTeam(101, "B1") - assert.Nil(t, err) - assert.Equal(t, 101, mainArena.currentMatch.Blue1) - assert.Equal(t, 101, mainArena.AllianceStations["B1"].Team.Id) - err = mainArena.AssignTeam(104, "R4") - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Invalid alliance station") - } - - // Substitute teams into practice match. Replacement should also be persisted in the DB. - match := model.Match{Type: "practice", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - db.CreateMatch(&match) - mainArena.LoadMatch(&match) - err = mainArena.SubstituteTeam(107, "R1") - assert.Nil(t, err) - assert.Equal(t, 107, mainArena.currentMatch.Red1) - assert.Equal(t, 107, mainArena.AllianceStations["R1"].Team.Id) - matchResult := model.NewMatchResult() - matchResult.MatchId = mainArena.currentMatch.Id - CommitMatchScore(mainArena.currentMatch, matchResult, false) - match2, _ := db.GetMatchById(match.Id) - assert.Equal(t, 107, match2.Red1) - - // Check that substitution is disallowed in qualification matches. - match = model.Match{Type: "qualification", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - db.CreateMatch(&match) - mainArena.LoadMatch(&match) - err = mainArena.SubstituteTeam(107, "R1") - if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Can't substitute teams for qualification matches.") - } - match = model.Match{Type: "elimination", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - db.CreateMatch(&match) - mainArena.LoadMatch(&match) - assert.Nil(t, mainArena.SubstituteTeam(107, "R1")) -} - -func TestSetupNetwork(t *testing.T) { - setupTest(t) - - // Verify the setup ran by checking the log for the expected failure messages. - eventSettings.NetworkSecurityEnabled = true - accessPointSshPort = 10022 - switchTelnetPort = 10023 - mainArena.LoadMatch(&model.Match{Type: "test"}) - var writer bytes.Buffer - log.SetOutput(&writer) - time.Sleep(time.Millisecond * 10) // Allow some time for the asynchronous configuration to happen. - assert.Contains(t, writer.String(), "Failed to configure team Ethernet") - assert.Contains(t, writer.String(), "Failed to configure team WiFi") -} diff --git a/audience_display.go b/audience_display.go index 1c70218..f21e004 100644 --- a/audience_display.go +++ b/audience_display.go @@ -15,12 +15,12 @@ import ( ) // Renders the audience display to be chroma keyed over the video feed. -func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) audienceDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err := template.ParseFiles("templates/audience_display.html") if err != nil { handleWebErr(w, err) @@ -29,7 +29,7 @@ func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings - }{eventSettings} + }{web.arena.EventSettings} err = template.ExecuteTemplate(w, "audience_display.html", data) if err != nil { handleWebErr(w, err) @@ -38,8 +38,8 @@ func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the audience display client to receive status updates. -func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } @@ -50,23 +50,23 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen() + audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen() defer close(audienceDisplayListener) - matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen() + matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen() defer close(matchLoadTeamsListener) - matchTimeListener := mainArena.matchTimeNotifier.Listen() + matchTimeListener := web.arena.MatchTimeNotifier.Listen() defer close(matchTimeListener) - realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen() + realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen() defer close(realtimeScoreListener) - scorePostedListener := mainArena.scorePostedNotifier.Listen() + scorePostedListener := web.arena.ScorePostedNotifier.Listen() defer close(scorePostedListener) - playSoundListener := mainArena.playSoundNotifier.Listen() + playSoundListener := web.arena.PlaySoundNotifier.Listen() defer close(playSoundListener) - allianceSelectionListener := mainArena.allianceSelectionNotifier.Listen() + allianceSelectionListener := web.arena.AllianceSelectionNotifier.Listen() defer close(allianceSelectionListener) - lowerThirdListener := mainArena.lowerThirdNotifier.Listen() + lowerThirdListener := web.arena.LowerThirdNotifier.Listen() defer close(lowerThirdListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Send the various notifications immediately upon connection. @@ -76,12 +76,12 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}) + err = websocket.Write("matchTime", MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)}) if err != nil { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen) + err = websocket.Write("setAudienceDisplay", web.arena.AudienceDisplayScreen) if err != nil { log.Printf("Websocket error: %s", err) return @@ -89,7 +89,7 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { data = struct { Match *model.Match MatchName string - }{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()} + }{web.arena.CurrentMatch, web.arena.CurrentMatch.CapitalizedType()} err = websocket.Write("setMatch", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -100,8 +100,8 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { BlueScore *game.Score RedScoreSummary *game.ScoreSummary BlueScoreSummary *game.ScoreSummary - }{mainArena.redRealtimeScore.CurrentScore, mainArena.blueRealtimeScore.CurrentScore, - mainArena.RedScoreSummary(), mainArena.BlueScoreSummary()} + }{web.arena.RedRealtimeScore.CurrentScore, web.arena.BlueRealtimeScore.CurrentScore, + web.arena.RedScoreSummary(), web.arena.BlueScoreSummary()} err = websocket.Write("realtimeScore", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -112,8 +112,8 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { MatchName string RedScore *game.ScoreSummary BlueScore *game.ScoreSummary - }{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(), - mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()} + }{web.arena.SavedMatch, web.arena.SavedMatch.CapitalizedType(), + web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary()} err = websocket.Write("setFinalScore", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -136,7 +136,7 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { return } messageType = "setAudienceDisplay" - message = mainArena.audienceDisplayScreen + message = web.arena.AudienceDisplayScreen case _, ok := <-matchLoadTeamsListener: if !ok { return @@ -145,13 +145,13 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { message = struct { Match *model.Match MatchName string - }{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()} + }{web.arena.CurrentMatch, web.arena.CurrentMatch.CapitalizedType()} case matchTimeSec, ok := <-matchTimeListener: if !ok { return } messageType = "matchTime" - message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} + message = MatchTimeMessage{web.arena.MatchState, matchTimeSec.(int)} case _, ok := <-realtimeScoreListener: if !ok { return @@ -162,8 +162,8 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { BlueScore *game.Score RedScoreSummary *game.ScoreSummary BlueScoreSummary *game.ScoreSummary - }{mainArena.redRealtimeScore.CurrentScore, mainArena.blueRealtimeScore.CurrentScore, - mainArena.RedScoreSummary(), mainArena.BlueScoreSummary()} + }{web.arena.RedRealtimeScore.CurrentScore, web.arena.BlueRealtimeScore.CurrentScore, + web.arena.RedScoreSummary(), web.arena.BlueScoreSummary()} case _, ok := <-scorePostedListener: if !ok { return @@ -174,8 +174,8 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { MatchName string RedScore *game.ScoreSummary BlueScore *game.ScoreSummary - }{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(), - mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()} + }{web.arena.SavedMatch, web.arena.SavedMatch.CapitalizedType(), + web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary()} case sound, ok := <-playSoundListener: if !ok { return diff --git a/audience_display_test.go b/audience_display_test.go index 9c98001..1626f3b 100644 --- a/audience_display_test.go +++ b/audience_display_test.go @@ -11,17 +11,17 @@ import ( ) func TestAudienceDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/audience") + recorder := web.getHttpResponse("/displays/audience") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Audience Display - Untitled Event - Cheesy Arena") } func TestAudienceDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil) assert.Nil(t, err) @@ -38,16 +38,16 @@ func TestAudienceDisplayWebsocket(t *testing.T) { readWebsocketType(t, ws, "allianceSelection") // Run through a match cycle. - mainArena.matchLoadTeamsNotifier.Notify(nil) + web.arena.MatchLoadTeamsNotifier.Notify(nil) readWebsocketType(t, ws, "setMatch") - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - mainArena.StartMatch() - mainArena.Update() + web.arena.AllianceStations["R1"].Bypass = true + web.arena.AllianceStations["R2"].Bypass = true + web.arena.AllianceStations["R3"].Bypass = true + web.arena.AllianceStations["B1"].Bypass = true + web.arena.AllianceStations["B2"].Bypass = true + web.arena.AllianceStations["B3"].Bypass = true + web.arena.StartMatch() + web.arena.Update() messages := readWebsocketMultiple(t, ws, 3) screen, ok := messages["setAudienceDisplay"] if assert.True(t, ok) { @@ -59,14 +59,14 @@ func TestAudienceDisplayWebsocket(t *testing.T) { } _, ok = messages["matchTime"] assert.True(t, ok) - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) readWebsocketType(t, ws, "realtimeScore") - mainArena.scorePostedNotifier.Notify(nil) + web.arena.ScorePostedNotifier.Notify(nil) readWebsocketType(t, ws, "setFinalScore") // Test other overlays. - mainArena.allianceSelectionNotifier.Notify(nil) + web.arena.AllianceSelectionNotifier.Notify(nil) readWebsocketType(t, ws, "allianceSelection") - mainArena.lowerThirdNotifier.Notify(nil) + web.arena.LowerThirdNotifier.Notify(nil) readWebsocketType(t, ws, "lowerThird") } diff --git a/access_point_config.go b/field/access_point.go similarity index 74% rename from access_point_config.go rename to field/access_point.go index e568fbd..635ebb1 100644 --- a/access_point_config.go +++ b/field/access_point.go @@ -3,7 +3,7 @@ // // Methods for configuring a Linksys WRT1900ACS access point running OpenWRT for team SSIDs and VLANs. -package main +package field import ( "bytes" @@ -11,11 +11,12 @@ import ( "github.com/Team254/cheesy-arena/model" "golang.org/x/crypto/ssh" "os" + "path/filepath" "sync" "text/template" ) -var accessPointSshPort = 22 +const accessPointSshPort = 22 const ( red1Vlan = 10 @@ -26,20 +27,54 @@ const ( blue3Vlan = 60 ) -var accessPointMutex sync.Mutex +var templatesPath = "." + +type AccessPoint struct { + address string + port int + username string + password string + mutex sync.Mutex +} + +func NewAccessPoint(address, username, password string) *AccessPoint { + return &AccessPoint{address: address, port: accessPointSshPort, username: username, password: password} +} // Sets up wireless networks for the given set of teams. -func ConfigureTeamWifi(red1, red2, red3, blue1, blue2, blue3 *model.Team) error { +func (ap *AccessPoint) ConfigureTeamWifi(red1, red2, red3, blue1, blue2, blue3 *model.Team) error { // Make sure multiple configurations aren't being set at the same time. - accessPointMutex.Lock() - defer accessPointMutex.Unlock() + ap.mutex.Lock() + defer ap.mutex.Unlock() config, err := generateAccessPointConfig(red1, red2, red3, blue1, blue2, blue3) if err != nil { return err } command := fmt.Sprintf("cat < /etc/config/wireless && wifi radio0\n%sENDCONFIG\n", config) - return runAccessPointCommand(command) + return ap.runCommand(command) +} + +// Logs into the access point via SSH and runs the given shell command. +func (ap *AccessPoint) runCommand(command string) error { + // Open an SSH connection to the AP. + config := &ssh.ClientConfig{User: ap.username, + Auth: []ssh.AuthMethod{ssh.Password(ap.password)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey()} + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", ap.address, ap.port), config) + if err != nil { + return err + } + session, err := conn.NewSession() + if err != nil { + return err + } + defer session.Close() + defer conn.Close() + session.Stdout = os.Stdout + + // Run the command. An error will be returned if the exit status is non-zero. + return session.Run(command) } func generateAccessPointConfig(red1, red2, red3, blue1, blue2, blue3 *model.Team) (string, error) { @@ -66,7 +101,7 @@ func generateAccessPointConfig(red1, red2, red3, blue1, blue2, blue3 *model.Team } // Generate the config file to be uploaded to the AP. - template, err := template.ParseFiles("templates/access_point.cfg") + template, err := template.ParseFiles(filepath.Join(templatesPath, "templates/access_point.cfg")) if err != nil { return "", err } @@ -90,25 +125,3 @@ func addTeamNetwork(networks map[int]*model.Team, team *model.Team, vlan int) er networks[vlan] = team return nil } - -// Logs into the access point via SSH and runs the given shell command. -func runAccessPointCommand(command string) error { - // Open an SSH connection to the AP. - config := &ssh.ClientConfig{User: eventSettings.ApUsername, - Auth: []ssh.AuthMethod{ssh.Password(eventSettings.ApPassword)}, - HostKeyCallback: ssh.InsecureIgnoreHostKey()} - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", eventSettings.ApAddress, accessPointSshPort), config) - if err != nil { - return err - } - session, err := conn.NewSession() - if err != nil { - return err - } - defer session.Close() - defer conn.Close() - session.Stdout = os.Stdout - - // Run the command. An error will be returned if the exit status is non-zero. - return session.Run(command) -} diff --git a/access_point_config_test.go b/field/access_point_test.go similarity index 98% rename from access_point_config_test.go rename to field/access_point_test.go index 9e4a957..18a6bb7 100644 --- a/access_point_config_test.go +++ b/field/access_point_test.go @@ -1,7 +1,7 @@ // Copyright 2014 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) -package main +package field import ( "github.com/Team254/cheesy-arena/model" @@ -11,6 +11,8 @@ import ( ) func TestConfigureAccessPoint(t *testing.T) { + templatesPath = ".." + radioRe := regexp.MustCompile("option device 'radio0'") ssidRe := regexp.MustCompile("option ssid '([-\\w ]+)'") wpaKeyRe := regexp.MustCompile("option key '([-\\w ]+)'") diff --git a/arena.go b/field/arena.go similarity index 54% rename from arena.go rename to field/arena.go index 543b418..d4ad8ff 100644 --- a/arena.go +++ b/field/arena.go @@ -3,12 +3,13 @@ // // Functions for controlling the arena and match play. -package main +package field import ( "fmt" "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" + "github.com/Team254/cheesy-arena/partner" "log" "time" ) @@ -21,15 +22,59 @@ const ( // Progression of match states. const ( - preMatch = 0 - startMatch = 1 - autoPeriod = 2 - pausePeriod = 3 - teleopPeriod = 4 - endgamePeriod = 5 - postMatch = 6 + PreMatch = 0 + StartMatch = 1 + AutoPeriod = 2 + PausePeriod = 3 + TeleopPeriod = 4 + EndgamePeriod = 5 + PostMatch = 6 ) +type Arena struct { + Database *model.Database + EventSettings *model.EventSettings + accessPoint *AccessPoint + networkSwitch *NetworkSwitch + TbaClient *partner.TbaClient + StemTvClient *partner.StemTvClient + AllianceStations map[string]*AllianceStation + CurrentMatch *model.Match + MatchState int + lastMatchState int + MatchStartTime time.Time + LastMatchTimeSec float64 + RedRealtimeScore *RealtimeScore + BlueRealtimeScore *RealtimeScore + lastDsPacketTime time.Time + FieldReset bool + AudienceDisplayScreen string + SavedMatch *model.Match + SavedMatchResult *model.MatchResult + AllianceStationDisplays map[string]string + AllianceStationDisplayScreen string + MuteMatchSounds bool + matchStateNotifier *Notifier + MatchTimeNotifier *Notifier + RobotStatusNotifier *Notifier + MatchLoadTeamsNotifier *Notifier + ScoringStatusNotifier *Notifier + RealtimeScoreNotifier *Notifier + ScorePostedNotifier *Notifier + AudienceDisplayNotifier *Notifier + PlaySoundNotifier *Notifier + AllianceStationDisplayNotifier *Notifier + AllianceSelectionNotifier *Notifier + LowerThirdNotifier *Notifier + ReloadDisplaysNotifier *Notifier +} + +type ArenaStatus struct { + AllianceStations map[string]*AllianceStation + MatchState int + CanStartMatch bool +} + type AllianceStation struct { DsConn *DriverStationConnection EmergencyStop bool @@ -37,58 +82,20 @@ type AllianceStation struct { Team *model.Team } -type RealtimeScore struct { - CurrentScore *game.Score - Cards map[string]string - TeleopCommitted bool - FoulsCommitted bool -} +// Creates the arena and sets it to its initial state. +func NewArena(dbPath string) (*Arena, error) { + arena := new(Arena) -type Arena struct { - AllianceStations map[string]*AllianceStation - MatchState int - CanStartMatch bool - currentMatch *model.Match - redRealtimeScore *RealtimeScore - blueRealtimeScore *RealtimeScore - matchStartTime time.Time - lastDsPacketTime time.Time - matchStateNotifier *Notifier - matchTimeNotifier *Notifier - robotStatusNotifier *Notifier - matchLoadTeamsNotifier *Notifier - scoringStatusNotifier *Notifier - realtimeScoreNotifier *Notifier - scorePostedNotifier *Notifier - audienceDisplayNotifier *Notifier - playSoundNotifier *Notifier - allianceStationDisplayNotifier *Notifier - allianceSelectionNotifier *Notifier - lowerThirdNotifier *Notifier - reloadDisplaysNotifier *Notifier - audienceDisplayScreen string - allianceStationDisplays map[string]string - allianceStationDisplayScreen string - lastMatchState int - lastMatchTimeSec float64 - savedMatch *model.Match - savedMatchResult *model.MatchResult - lights Lights - muteMatchSounds bool - fieldReset bool -} + var err error + arena.Database, err = model.OpenDatabase(dbPath) + if err != nil { + return nil, err + } + err = arena.LoadSettings() + if err != nil { + return nil, err + } -var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic. - -func NewRealtimeScore() *RealtimeScore { - realtimeScore := new(RealtimeScore) - realtimeScore.CurrentScore = new(game.Score) - realtimeScore.Cards = make(map[string]string) - return realtimeScore -} - -// Sets the arena to its initial state. -func (arena *Arena) Setup() { arena.AllianceStations = make(map[string]*AllianceStation) arena.AllianceStations["R1"] = new(AllianceStation) arena.AllianceStations["R2"] = new(AllianceStation) @@ -98,117 +105,96 @@ func (arena *Arena) Setup() { arena.AllianceStations["B3"] = new(AllianceStation) arena.matchStateNotifier = NewNotifier() - arena.matchTimeNotifier = NewNotifier() - arena.robotStatusNotifier = NewNotifier() - arena.matchLoadTeamsNotifier = NewNotifier() - arena.scoringStatusNotifier = NewNotifier() - arena.realtimeScoreNotifier = NewNotifier() - arena.scorePostedNotifier = NewNotifier() - arena.audienceDisplayNotifier = NewNotifier() - arena.playSoundNotifier = NewNotifier() - arena.allianceStationDisplayNotifier = NewNotifier() - arena.allianceSelectionNotifier = NewNotifier() - arena.lowerThirdNotifier = NewNotifier() - arena.reloadDisplaysNotifier = NewNotifier() - - arena.lights.Setup() + arena.MatchTimeNotifier = NewNotifier() + arena.RobotStatusNotifier = NewNotifier() + arena.MatchLoadTeamsNotifier = NewNotifier() + arena.ScoringStatusNotifier = NewNotifier() + arena.RealtimeScoreNotifier = NewNotifier() + arena.ScorePostedNotifier = NewNotifier() + arena.AudienceDisplayNotifier = NewNotifier() + arena.PlaySoundNotifier = NewNotifier() + arena.AllianceStationDisplayNotifier = NewNotifier() + arena.AllianceSelectionNotifier = NewNotifier() + arena.LowerThirdNotifier = NewNotifier() + arena.ReloadDisplaysNotifier = NewNotifier() // Load empty match as current. - arena.MatchState = preMatch + arena.MatchState = PreMatch arena.LoadTestMatch() arena.lastMatchState = -1 - arena.lastMatchTimeSec = 0 + arena.LastMatchTimeSec = 0 // Initialize display parameters. - arena.audienceDisplayScreen = "blank" - arena.savedMatch = &model.Match{} - arena.savedMatchResult = model.NewMatchResult() - arena.allianceStationDisplays = make(map[string]string) - arena.allianceStationDisplayScreen = "match" + arena.AudienceDisplayScreen = "blank" + arena.SavedMatch = &model.Match{} + arena.SavedMatchResult = model.NewMatchResult() + arena.AllianceStationDisplays = make(map[string]string) + arena.AllianceStationDisplayScreen = "match" + + return arena, nil } -// Loads a team into an alliance station, cleaning up the previous team there if there is one. -func (arena *Arena) AssignTeam(teamId int, station string) error { - // Reject invalid station values. - if _, ok := arena.AllianceStations[station]; !ok { - return fmt.Errorf("Invalid alliance station '%s'.", station) - } - - // Do nothing if the station is already assigned to the requested team. - dsConn := arena.AllianceStations[station].DsConn - if dsConn != nil && dsConn.TeamId == teamId { - return nil - } - if dsConn != nil { - dsConn.Close() - arena.AllianceStations[station].Team = nil - arena.AllianceStations[station].DsConn = nil - } - - // Leave the station empty if the team number is zero. - if teamId == 0 { - arena.AllianceStations[station].Team = nil - return nil - } - - // Load the team model. If it doesn't exist, enable anonymous operation. - team, err := db.GetTeamById(teamId) +// Loads or reloads the event settings upon initial setup or change. +func (arena *Arena) LoadSettings() error { + settings, err := arena.Database.GetEventSettings() if err != nil { return err } - if team == nil { - team = &model.Team{Id: teamId} - } + arena.EventSettings = settings + + // Initialize the components that depend on settings. + arena.accessPoint = NewAccessPoint(settings.ApAddress, settings.ApUsername, settings.ApPassword) + arena.networkSwitch = NewNetworkSwitch(settings.SwitchAddress, settings.SwitchPassword) + arena.TbaClient = partner.NewTbaClient(settings.TbaEventCode, settings.TbaSecretId, settings.TbaSecret) + arena.StemTvClient = partner.NewStemTvClient(settings.StemTvEventCode) - arena.AllianceStations[station].Team = team return nil } // Sets up the arena for the given match. func (arena *Arena) LoadMatch(match *model.Match) error { - if arena.MatchState != preMatch { + if arena.MatchState != PreMatch { return fmt.Errorf("Cannot load match while there is a match still in progress or with results pending.") } - arena.currentMatch = match - err := arena.AssignTeam(match.Red1, "R1") + arena.CurrentMatch = match + err := arena.assignTeam(match.Red1, "R1") if err != nil { return err } - err = arena.AssignTeam(match.Red2, "R2") + err = arena.assignTeam(match.Red2, "R2") if err != nil { return err } - err = arena.AssignTeam(match.Red3, "R3") + err = arena.assignTeam(match.Red3, "R3") if err != nil { return err } - err = arena.AssignTeam(match.Blue1, "B1") + err = arena.assignTeam(match.Blue1, "B1") if err != nil { return err } - err = arena.AssignTeam(match.Blue2, "B2") + err = arena.assignTeam(match.Blue2, "B2") if err != nil { return err } - err = arena.AssignTeam(match.Blue3, "B3") + err = arena.assignTeam(match.Blue3, "B3") if err != nil { return err } - arena.SetupNetwork() + arena.setupNetwork() // Reset the realtime scores. - arena.redRealtimeScore = NewRealtimeScore() - arena.blueRealtimeScore = NewRealtimeScore() - arena.fieldReset = false - arena.lights.ClearAll() + arena.RedRealtimeScore = NewRealtimeScore() + arena.BlueRealtimeScore = NewRealtimeScore() + arena.FieldReset = false // Notify any listeners about the new match. - arena.matchLoadTeamsNotifier.Notify(nil) - arena.realtimeScoreNotifier.Notify(nil) - arena.allianceStationDisplayScreen = "match" - arena.allianceStationDisplayNotifier.Notify(nil) + arena.MatchLoadTeamsNotifier.Notify(nil) + arena.RealtimeScoreNotifier.Notify(nil) + arena.AllianceStationDisplayScreen = "match" + arena.AllianceStationDisplayNotifier.Notify(nil) return nil } @@ -220,11 +206,11 @@ func (arena *Arena) LoadTestMatch() error { // Loads the first unplayed match of the current match type. func (arena *Arena) LoadNextMatch() error { - if arena.currentMatch.Type == "test" { + if arena.CurrentMatch.Type == "test" { return arena.LoadTestMatch() } - matches, err := db.GetMatchesByType(arena.currentMatch.Type) + matches, err := arena.Database.GetMatchesByType(arena.CurrentMatch.Type) if err != nil { return err } @@ -242,46 +228,277 @@ func (arena *Arena) LoadNextMatch() error { // Assigns the given team to the given station, also substituting it into the match record. func (arena *Arena) SubstituteTeam(teamId int, station string) error { - if arena.currentMatch.Type == "qualification" { + if arena.CurrentMatch.Type == "qualification" { return fmt.Errorf("Can't substitute teams for qualification matches.") } - err := arena.AssignTeam(teamId, station) + err := arena.assignTeam(teamId, station) if err != nil { return err } switch station { case "R1": - arena.currentMatch.Red1 = teamId + arena.CurrentMatch.Red1 = teamId case "R2": - arena.currentMatch.Red2 = teamId + arena.CurrentMatch.Red2 = teamId case "R3": - arena.currentMatch.Red3 = teamId + arena.CurrentMatch.Red3 = teamId case "B1": - arena.currentMatch.Blue1 = teamId + arena.CurrentMatch.Blue1 = teamId case "B2": - arena.currentMatch.Blue2 = teamId + arena.CurrentMatch.Blue2 = teamId case "B3": - arena.currentMatch.Blue3 = teamId + arena.CurrentMatch.Blue3 = teamId } - arena.SetupNetwork() - arena.matchLoadTeamsNotifier.Notify(nil) + arena.setupNetwork() + arena.MatchLoadTeamsNotifier.Notify(nil) + return nil +} + +// Starts the match if all conditions are met. +func (arena *Arena) StartMatch() error { + err := arena.checkCanStartMatch() + if err == nil { + // Save the match start time to the database for posterity. + arena.CurrentMatch.StartedAt = time.Now() + if arena.CurrentMatch.Type != "test" { + arena.Database.SaveMatch(arena.CurrentMatch) + } + + // Save the missed packet count to subtract it from the running count. + for _, allianceStation := range arena.AllianceStations { + if allianceStation.DsConn != nil { + err = allianceStation.DsConn.signalMatchStart(arena.CurrentMatch) + if err != nil { + log.Println(err) + } + } + } + + arena.MatchState = StartMatch + } + return err +} + +// Kills the current match if it is underway. +func (arena *Arena) AbortMatch() error { + if arena.MatchState == PreMatch || arena.MatchState == PostMatch { + return fmt.Errorf("Cannot abort match when it is not in progress.") + } + arena.MatchState = PostMatch + arena.AudienceDisplayScreen = "blank" + arena.AudienceDisplayNotifier.Notify(nil) + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-abort") + } + return nil +} + +// Clears out the match and resets the arena state unless there is a match underway. +func (arena *Arena) ResetMatch() error { + if arena.MatchState != PostMatch && arena.MatchState != PreMatch { + return fmt.Errorf("Cannot reset match while it is in progress.") + } + arena.MatchState = PreMatch + arena.AllianceStations["R1"].Bypass = false + arena.AllianceStations["R2"].Bypass = false + arena.AllianceStations["R3"].Bypass = false + arena.AllianceStations["B1"].Bypass = false + arena.AllianceStations["B2"].Bypass = false + arena.AllianceStations["B3"].Bypass = false + arena.MuteMatchSounds = false + return nil +} + +// Returns the fractional number of seconds since the start of the match. +func (arena *Arena) MatchTimeSec() float64 { + if arena.MatchState == PreMatch || arena.MatchState == StartMatch || arena.MatchState == PostMatch { + return 0 + } else { + return time.Since(arena.MatchStartTime).Seconds() + } +} + +// Performs a single iteration of checking inputs and timers and setting outputs accordingly to control the +// flow of a match. +func (arena *Arena) Update() { + // Decide what state the robots need to be in, depending on where we are in the match. + auto := false + enabled := false + sendDsPacket := false + matchTimeSec := arena.MatchTimeSec() + switch arena.MatchState { + case PreMatch: + auto = true + enabled = false + case StartMatch: + arena.MatchState = AutoPeriod + arena.MatchStartTime = time.Now() + arena.LastMatchTimeSec = -1 + auto = true + enabled = true + sendDsPacket = true + arena.AudienceDisplayScreen = "match" + arena.AudienceDisplayNotifier.Notify(nil) + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-start") + } + case AutoPeriod: + auto = true + enabled = true + if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec) { + arena.MatchState = PausePeriod + auto = false + enabled = false + sendDsPacket = true + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-end") + } + } + case PausePeriod: + auto = false + enabled = false + if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec) { + arena.MatchState = TeleopPeriod + auto = false + enabled = true + sendDsPacket = true + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-resume") + } + } + case TeleopPeriod: + auto = false + enabled = true + if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ + game.MatchTiming.TeleopDurationSec-game.MatchTiming.EndgameTimeLeftSec) { + arena.MatchState = EndgamePeriod + sendDsPacket = false + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-endgame") + } + } + case EndgamePeriod: + auto = false + enabled = true + if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ + game.MatchTiming.TeleopDurationSec) { + arena.MatchState = PostMatch + auto = false + enabled = false + sendDsPacket = true + go func() { + // Leave the scores on the screen briefly at the end of the match. + time.Sleep(time.Second * matchEndScoreDwellSec) + arena.AudienceDisplayScreen = "blank" + arena.AudienceDisplayNotifier.Notify(nil) + arena.AllianceStationDisplayScreen = "logo" + arena.AllianceStationDisplayNotifier.Notify(nil) + }() + if !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-end") + } + } + } + + // Send a notification if the match state has changed. + if arena.MatchState != arena.lastMatchState { + arena.matchStateNotifier.Notify(arena.MatchState) + } + arena.lastMatchState = arena.MatchState + + // Send a match tick notification if passing an integer second threshold. + if int(matchTimeSec) != int(arena.LastMatchTimeSec) { + arena.MatchTimeNotifier.Notify(int(matchTimeSec)) + } + arena.LastMatchTimeSec = matchTimeSec + + // Send a packet if at a period transition point or if it's been long enough since the last one. + if sendDsPacket || time.Since(arena.lastDsPacketTime).Seconds()*1000 >= dsPacketPeriodMs { + arena.sendDsPacket(auto, enabled) + arena.RobotStatusNotifier.Notify(nil) + } +} + +// Loops indefinitely to track and update the arena components. +func (arena *Arena) Run() { + // Start other loops in goroutines. + go arena.listenForDriverStations() + go arena.listenForDsUdpPackets() + go arena.monitorBandwidth() + + for { + arena.Update() + time.Sleep(time.Millisecond * arenaLoopPeriodMs) + } +} + +// Calculates the red alliance score summary for the given realtime snapshot. +func (arena *Arena) RedScoreSummary() *game.ScoreSummary { + return arena.RedRealtimeScore.CurrentScore.Summarize(arena.BlueRealtimeScore.CurrentScore.Fouls, + arena.CurrentMatch.Type) +} + +// Calculates the blue alliance score summary for the given realtime snapshot. +func (arena *Arena) BlueScoreSummary() *game.ScoreSummary { + return arena.BlueRealtimeScore.CurrentScore.Summarize(arena.RedRealtimeScore.CurrentScore.Fouls, + arena.CurrentMatch.Type) +} + +func (arena *Arena) GetStatus() *ArenaStatus { + return &ArenaStatus{arena.AllianceStations, arena.MatchState, arena.checkCanStartMatch() == nil} +} + +// Loads a team into an alliance station, cleaning up the previous team there if there is one. +func (arena *Arena) assignTeam(teamId int, station string) error { + // Reject invalid station values. + if _, ok := arena.AllianceStations[station]; !ok { + return fmt.Errorf("Invalid alliance station '%s'.", station) + } + + // Do nothing if the station is already assigned to the requested team. + dsConn := arena.AllianceStations[station].DsConn + if dsConn != nil && dsConn.TeamId == teamId { + return nil + } + if dsConn != nil { + dsConn.close() + arena.AllianceStations[station].Team = nil + arena.AllianceStations[station].DsConn = nil + } + + // Leave the station empty if the team number is zero. + if teamId == 0 { + arena.AllianceStations[station].Team = nil + return nil + } + + // Load the team model. If it doesn't exist, enable anonymous operation. + team, err := arena.Database.GetTeamById(teamId) + if err != nil { + return err + } + if team == nil { + team = &model.Team{Id: teamId} + } + + arena.AllianceStations[station].Team = team return nil } // Asynchronously reconfigures the networking hardware for the new set of teams. -func (arena *Arena) SetupNetwork() { - if eventSettings.NetworkSecurityEnabled { +func (arena *Arena) setupNetwork() { + if arena.EventSettings.NetworkSecurityEnabled { go func() { - err := ConfigureTeamWifi(arena.AllianceStations["R1"].Team, arena.AllianceStations["R2"].Team, - arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, + err := arena.accessPoint.ConfigureTeamWifi(arena.AllianceStations["R1"].Team, + arena.AllianceStations["R2"].Team, arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, arena.AllianceStations["B2"].Team, arena.AllianceStations["B3"].Team) if err != nil { log.Printf("Failed to configure team WiFi: %s", err.Error()) } }() go func() { - err := ConfigureTeamEthernet(arena.AllianceStations["R1"].Team, arena.AllianceStations["R2"].Team, - arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, + err := arena.networkSwitch.ConfigureTeamEthernet(arena.AllianceStations["R1"].Team, + arena.AllianceStations["R2"].Team, arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, arena.AllianceStations["B2"].Team, arena.AllianceStations["B3"].Team) if err != nil { log.Printf("Failed to configure team Ethernet: %s", err.Error()) @@ -291,8 +508,8 @@ func (arena *Arena) SetupNetwork() { } // Returns nil if the match can be started, and an error otherwise. -func (arena *Arena) CheckCanStartMatch() error { - if arena.MatchState != preMatch { +func (arena *Arena) checkCanStartMatch() error { + if arena.MatchState != PreMatch { return fmt.Errorf("Cannot start match while there is a match still in progress or with results pending.") } for _, allianceStation := range arena.AllianceStations { @@ -308,190 +525,13 @@ func (arena *Arena) CheckCanStartMatch() error { return nil } -// Starts the match if all conditions are met. -func (arena *Arena) StartMatch() error { - err := arena.CheckCanStartMatch() - if err == nil { - // Save the match start time to the database for posterity. - arena.currentMatch.StartedAt = time.Now() - if arena.currentMatch.Type != "test" { - db.SaveMatch(arena.currentMatch) - } - - // Save the missed packet count to subtract it from the running count. - for _, allianceStation := range arena.AllianceStations { - if allianceStation.DsConn != nil { - err = allianceStation.DsConn.signalMatchStart(arena.currentMatch) - if err != nil { - log.Println(err) - } - } - } - - arena.MatchState = startMatch - } - return err -} - -// Kills the current match if it is underway. -func (arena *Arena) AbortMatch() error { - if arena.MatchState == preMatch || arena.MatchState == postMatch { - return fmt.Errorf("Cannot abort match when it is not in progress.") - } - arena.MatchState = postMatch - arena.audienceDisplayScreen = "blank" - arena.audienceDisplayNotifier.Notify(nil) - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-abort") - } - return nil -} - -// Clears out the match and resets the arena state unless there is a match underway. -func (arena *Arena) ResetMatch() error { - if arena.MatchState != postMatch && arena.MatchState != preMatch { - return fmt.Errorf("Cannot reset match while it is in progress.") - } - arena.MatchState = preMatch - arena.AllianceStations["R1"].Bypass = false - arena.AllianceStations["R2"].Bypass = false - arena.AllianceStations["R3"].Bypass = false - arena.AllianceStations["B1"].Bypass = false - arena.AllianceStations["B2"].Bypass = false - arena.AllianceStations["B3"].Bypass = false - arena.muteMatchSounds = false - return nil -} - -// Returns the fractional number of seconds since the start of the match. -func (arena *Arena) MatchTimeSec() float64 { - if arena.MatchState == preMatch || arena.MatchState == startMatch || arena.MatchState == postMatch { - return 0 - } else { - return time.Since(arena.matchStartTime).Seconds() - } -} - -// Performs a single iteration of checking inputs and timers and setting outputs accordingly to control the -// flow of a match. -func (arena *Arena) Update() { - arena.CanStartMatch = arena.CheckCanStartMatch() == nil - - // Decide what state the robots need to be in, depending on where we are in the match. - auto := false - enabled := false - sendDsPacket := false - matchTimeSec := arena.MatchTimeSec() - switch arena.MatchState { - case preMatch: - auto = true - enabled = false - case startMatch: - arena.MatchState = autoPeriod - arena.matchStartTime = time.Now() - arena.lastMatchTimeSec = -1 - auto = true - enabled = true - sendDsPacket = true - arena.audienceDisplayScreen = "match" - arena.audienceDisplayNotifier.Notify(nil) - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-start") - } - case autoPeriod: - auto = true - enabled = true - if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec) { - arena.MatchState = pausePeriod - auto = false - enabled = false - sendDsPacket = true - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-end") - } - } - case pausePeriod: - auto = false - enabled = false - if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec) { - arena.MatchState = teleopPeriod - auto = false - enabled = true - sendDsPacket = true - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-resume") - } - } - case teleopPeriod: - auto = false - enabled = true - if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ - game.MatchTiming.TeleopDurationSec-game.MatchTiming.EndgameTimeLeftSec) { - arena.MatchState = endgamePeriod - sendDsPacket = false - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-endgame") - } - } - case endgamePeriod: - auto = false - enabled = true - if matchTimeSec >= float64(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ - game.MatchTiming.TeleopDurationSec) { - arena.MatchState = postMatch - auto = false - enabled = false - sendDsPacket = true - go func() { - // Leave the scores on the screen briefly at the end of the match. - time.Sleep(time.Second * matchEndScoreDwellSec) - arena.audienceDisplayScreen = "blank" - arena.audienceDisplayNotifier.Notify(nil) - arena.allianceStationDisplayScreen = "logo" - arena.allianceStationDisplayNotifier.Notify(nil) - }() - if !arena.muteMatchSounds { - arena.playSoundNotifier.Notify("match-end") - } - } - } - - // Send a notification if the match state has changed. - if arena.MatchState != arena.lastMatchState { - arena.matchStateNotifier.Notify(arena.MatchState) - } - arena.lastMatchState = arena.MatchState - - // Send a match tick notification if passing an integer second threshold. - if int(matchTimeSec) != int(arena.lastMatchTimeSec) { - arena.matchTimeNotifier.Notify(int(matchTimeSec)) - } - arena.lastMatchTimeSec = matchTimeSec - - // Send a packet if at a period transition point or if it's been long enough since the last one. - if sendDsPacket || time.Since(arena.lastDsPacketTime).Seconds()*1000 >= dsPacketPeriodMs { - arena.sendDsPacket(auto, enabled) - arena.robotStatusNotifier.Notify(nil) - } - - arena.handleLighting() -} - -// Loops indefinitely to track and update the arena components. -func (arena *Arena) Run() { - for { - arena.Update() - time.Sleep(time.Millisecond * arenaLoopPeriodMs) - } -} - func (arena *Arena) sendDsPacket(auto bool, enabled bool) { for _, allianceStation := range arena.AllianceStations { dsConn := allianceStation.DsConn if dsConn != nil { dsConn.Auto = auto dsConn.Enabled = enabled && !allianceStation.EmergencyStop && !allianceStation.Bypass - err := dsConn.Update() + err := dsConn.update(arena) if err != nil { log.Printf("Unable to send driver station packet for team %d.", allianceStation.Team.Id) } @@ -500,38 +540,6 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) { arena.lastDsPacketTime = time.Now() } -// Calculates the red alliance score summary for the given realtime snapshot. -func (arena *Arena) RedScoreSummary() *game.ScoreSummary { - return arena.redRealtimeScore.CurrentScore.Summarize(arena.blueRealtimeScore.CurrentScore.Fouls, - arena.currentMatch.Type) -} - -// Calculates the blue alliance score summary for the given realtime snapshot. -func (arena *Arena) BlueScoreSummary() *game.ScoreSummary { - return arena.blueRealtimeScore.CurrentScore.Summarize(arena.redRealtimeScore.CurrentScore.Fouls, - arena.currentMatch.Type) -} - -// Manipulates the arena LED lighting based on the current state of the match. -func (arena *Arena) handleLighting() { - switch arena.MatchState { - case autoPeriod: - fallthrough - case pausePeriod: - fallthrough - case teleopPeriod: - fallthrough - case endgamePeriod: - break - case postMatch: - if mainArena.fieldReset { - arena.lights.SetFieldReset() - } else { - arena.lights.ClearAll() - } - } -} - // Returns the alliance station identifier for the given team, or the empty string if the team is not present // in the current match. func (arena *Arena) getAssignedAllianceStation(teamId int) string { diff --git a/field/arena_test.go b/field/arena_test.go new file mode 100644 index 0000000..d116b91 --- /dev/null +++ b/field/arena_test.go @@ -0,0 +1,472 @@ +// Copyright 2014 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package field + +import ( + "bytes" + "github.com/Team254/cheesy-arena/game" + "github.com/Team254/cheesy-arena/model" + "github.com/stretchr/testify/assert" + "log" + "testing" + "time" +) + +func TestAssignTeam(t *testing.T) { + arena := setupTestArena(t) + + team := model.Team{Id: 254} + err := arena.Database.CreateTeam(&team) + assert.Nil(t, err) + err = arena.Database.CreateTeam(&model.Team{Id: 1114}) + assert.Nil(t, err) + + err = arena.assignTeam(254, "B1") + assert.Nil(t, err) + assert.Equal(t, team, *arena.AllianceStations["B1"].Team) + dummyDs := &DriverStationConnection{TeamId: 254} + arena.AllianceStations["B1"].DsConn = dummyDs + + // Nothing should happen if the same team is assigned to the same station. + err = arena.assignTeam(254, "B1") + assert.Nil(t, err) + assert.Equal(t, team, *arena.AllianceStations["B1"].Team) + assert.NotNil(t, arena.AllianceStations["B1"]) + assert.Equal(t, dummyDs, arena.AllianceStations["B1"].DsConn) // Pointer equality + + // Test reassignment to another team. + err = arena.assignTeam(1114, "B1") + assert.Nil(t, err) + assert.NotEqual(t, team, *arena.AllianceStations["B1"].Team) + assert.Nil(t, arena.AllianceStations["B1"].DsConn) + + // Check assigning zero as the team number. + err = arena.assignTeam(0, "R2") + assert.Nil(t, err) + assert.Nil(t, arena.AllianceStations["R2"].Team) + assert.Nil(t, arena.AllianceStations["R2"].DsConn) + + // Check assigning to a non-existent station. + err = arena.assignTeam(254, "R4") + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Invalid alliance station") + } +} + +func TestArenaMatchFlow(t *testing.T) { + arena := setupTestArena(t) + + arena.Database.CreateTeam(&model.Team{Id: 254}) + err := arena.assignTeam(254, "B3") + assert.Nil(t, err) + dummyDs := &DriverStationConnection{TeamId: 254} + arena.AllianceStations["B3"].DsConn = dummyDs + + // Check pre-match state and packet timing. + assert.Equal(t, PreMatch, arena.MatchState) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + lastPacketCount := arena.AllianceStations["B3"].DsConn.packetCount + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-10 * time.Millisecond) + arena.Update() + assert.Equal(t, lastPacketCount, arena.AllianceStations["B3"].DsConn.packetCount) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, lastPacketCount+1, arena.AllianceStations["B3"].DsConn.packetCount) + + // Check match start, autonomous and transition to teleop. + arena.AllianceStations["R1"].Bypass = true + arena.AllianceStations["R2"].Bypass = true + arena.AllianceStations["R3"].Bypass = true + arena.AllianceStations["B1"].Bypass = true + arena.AllianceStations["B2"].Bypass = true + arena.AllianceStations["B3"].DsConn.RobotLinked = true + err = arena.StartMatch() + assert.Nil(t, err) + arena.Update() + assert.Equal(t, AutoPeriod, arena.MatchState) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + arena.Update() + assert.Equal(t, AutoPeriod, arena.MatchState) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec) * time.Second) + arena.Update() + assert.Equal(t, PausePeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.Update() + assert.Equal(t, PausePeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec+ + game.MatchTiming.PauseDurationSec) * time.Second) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + + // Check e-stop and bypass. + arena.AllianceStations["B3"].EmergencyStop = true + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.AllianceStations["B3"].Bypass = true + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.AllianceStations["B3"].EmergencyStop = false + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.AllianceStations["B3"].Bypass = false + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, TeleopPeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + + // Check endgame and match end. + arena.MatchStartTime = time.Now(). + Add(-time.Duration(game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+ + game.MatchTiming.TeleopDurationSec-game.MatchTiming.EndgameTimeLeftSec) * time.Second) + arena.Update() + assert.Equal(t, EndgamePeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + arena.Update() + assert.Equal(t, EndgamePeriod, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled) + arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec+ + game.MatchTiming.PauseDurationSec+game.MatchTiming.TeleopDurationSec) * time.Second) + arena.Update() + assert.Equal(t, PostMatch, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + arena.Update() + assert.Equal(t, PostMatch, arena.MatchState) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + + arena.AllianceStations["R1"].Bypass = true + arena.ResetMatch() + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.Update() + assert.Equal(t, PreMatch, arena.MatchState) + assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto) + assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) + assert.Equal(t, false, arena.AllianceStations["R1"].Bypass) +} + +func TestArenaStateEnforcement(t *testing.T) { + arena := setupTestArena(t) + + arena.AllianceStations["R1"].Bypass = true + arena.AllianceStations["R2"].Bypass = true + arena.AllianceStations["R3"].Bypass = true + arena.AllianceStations["B1"].Bypass = true + arena.AllianceStations["B2"].Bypass = true + arena.AllianceStations["B3"].Bypass = true + + err := arena.LoadMatch(new(model.Match)) + assert.Nil(t, err) + err = arena.AbortMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot abort match when") + } + err = arena.StartMatch() + assert.Nil(t, err) + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.ResetMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot reset match while") + } + arena.MatchState = AutoPeriod + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.ResetMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot reset match while") + } + arena.MatchState = PausePeriod + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.ResetMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot reset match while") + } + arena.MatchState = TeleopPeriod + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.ResetMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot reset match while") + } + arena.MatchState = EndgamePeriod + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.ResetMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot reset match while") + } + err = arena.AbortMatch() + assert.Nil(t, err) + arena.MatchState = PostMatch + err = arena.LoadMatch(new(model.Match)) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot load match while") + } + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot start match while") + } + err = arena.AbortMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Cannot abort match when") + } + + err = arena.ResetMatch() + assert.Nil(t, err) + assert.Equal(t, PreMatch, arena.MatchState) + err = arena.ResetMatch() + assert.Nil(t, err) + err = arena.LoadMatch(new(model.Match)) + assert.Nil(t, err) +} + +func TestMatchStartRobotLinkEnforcement(t *testing.T) { + arena := setupTestArena(t) + + arena.Database.CreateTeam(&model.Team{Id: 101}) + arena.Database.CreateTeam(&model.Team{Id: 102}) + arena.Database.CreateTeam(&model.Team{Id: 103}) + arena.Database.CreateTeam(&model.Team{Id: 104}) + arena.Database.CreateTeam(&model.Team{Id: 105}) + arena.Database.CreateTeam(&model.Team{Id: 106}) + match := model.Match{Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} + arena.Database.CreateMatch(&match) + + err := arena.LoadMatch(&match) + assert.Nil(t, err) + arena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 101} + arena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 102} + arena.AllianceStations["R3"].DsConn = &DriverStationConnection{TeamId: 103} + arena.AllianceStations["B1"].DsConn = &DriverStationConnection{TeamId: 104} + arena.AllianceStations["B2"].DsConn = &DriverStationConnection{TeamId: 105} + arena.AllianceStations["B3"].DsConn = &DriverStationConnection{TeamId: 106} + for _, station := range arena.AllianceStations { + station.DsConn.RobotLinked = true + } + err = arena.StartMatch() + assert.Nil(t, err) + arena.MatchState = PreMatch + + // Check with a single team e-stopped, not linked and bypassed. + arena.AllianceStations["R1"].EmergencyStop = true + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "while an emergency stop is active") + } + arena.AllianceStations["R1"].EmergencyStop = false + arena.AllianceStations["R1"].DsConn.RobotLinked = false + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "until all robots are connected or bypassed") + } + arena.AllianceStations["R1"].Bypass = true + err = arena.StartMatch() + assert.Nil(t, err) + arena.AllianceStations["R1"].Bypass = false + arena.MatchState = PreMatch + + // Check with a team missing. + err = arena.assignTeam(0, "R1") + assert.Nil(t, err) + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "until all robots are connected or bypassed") + } + arena.AllianceStations["R1"].Bypass = true + err = arena.StartMatch() + assert.Nil(t, err) + arena.MatchState = PreMatch + + // Check with no teams present. + arena.LoadMatch(new(model.Match)) + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "until all robots are connected or bypassed") + } + arena.AllianceStations["R1"].Bypass = true + arena.AllianceStations["R2"].Bypass = true + arena.AllianceStations["R3"].Bypass = true + arena.AllianceStations["B1"].Bypass = true + arena.AllianceStations["B2"].Bypass = true + arena.AllianceStations["B3"].Bypass = true + arena.AllianceStations["B3"].EmergencyStop = true + err = arena.StartMatch() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "while an emergency stop is active") + } + arena.AllianceStations["B3"].EmergencyStop = false + err = arena.StartMatch() + assert.Nil(t, err) +} + +func TestLoadNextMatch(t *testing.T) { + arena := setupTestArena(t) + + arena.Database.CreateTeam(&model.Team{Id: 1114}) + practiceMatch1 := model.Match{Type: "practice", DisplayName: "1"} + practiceMatch2 := model.Match{Type: "practice", DisplayName: "2", Status: "complete"} + practiceMatch3 := model.Match{Type: "practice", DisplayName: "3"} + arena.Database.CreateMatch(&practiceMatch1) + arena.Database.CreateMatch(&practiceMatch2) + arena.Database.CreateMatch(&practiceMatch3) + qualificationMatch1 := model.Match{Type: "qualification", DisplayName: "1", Status: "complete"} + qualificationMatch2 := model.Match{Type: "qualification", DisplayName: "2"} + arena.Database.CreateMatch(&qualificationMatch1) + arena.Database.CreateMatch(&qualificationMatch2) + + // Test match should be followed by another, empty test match. + assert.Equal(t, 0, arena.CurrentMatch.Id) + err := arena.SubstituteTeam(1114, "R1") + assert.Nil(t, err) + arena.CurrentMatch.Status = "complete" + err = arena.LoadNextMatch() + assert.Nil(t, err) + assert.Equal(t, 0, arena.CurrentMatch.Id) + assert.Equal(t, 0, arena.CurrentMatch.Red1) + assert.NotEqual(t, "complete", arena.CurrentMatch.Status) + + // Other matches should be loaded by type until they're all complete. + err = arena.LoadMatch(&practiceMatch2) + assert.Nil(t, err) + err = arena.LoadNextMatch() + assert.Nil(t, err) + assert.Equal(t, practiceMatch1.Id, arena.CurrentMatch.Id) + practiceMatch1.Status = "complete" + arena.Database.SaveMatch(&practiceMatch1) + err = arena.LoadNextMatch() + assert.Nil(t, err) + assert.Equal(t, practiceMatch3.Id, arena.CurrentMatch.Id) + practiceMatch3.Status = "complete" + arena.Database.SaveMatch(&practiceMatch3) + err = arena.LoadNextMatch() + assert.Nil(t, err) + assert.Equal(t, practiceMatch3.Id, arena.CurrentMatch.Id) + assert.Equal(t, "complete", practiceMatch3.Status) + + err = arena.LoadMatch(&qualificationMatch1) + assert.Nil(t, err) + err = arena.LoadNextMatch() + assert.Nil(t, err) + assert.Equal(t, qualificationMatch2.Id, arena.CurrentMatch.Id) +} + +func TestSubstituteTeam(t *testing.T) { + arena := setupTestArena(t) + + arena.Database.CreateTeam(&model.Team{Id: 101}) + arena.Database.CreateTeam(&model.Team{Id: 102}) + arena.Database.CreateTeam(&model.Team{Id: 103}) + arena.Database.CreateTeam(&model.Team{Id: 104}) + arena.Database.CreateTeam(&model.Team{Id: 105}) + arena.Database.CreateTeam(&model.Team{Id: 106}) + arena.Database.CreateTeam(&model.Team{Id: 107}) + + // Substitute teams into test match. + err := arena.SubstituteTeam(101, "B1") + assert.Nil(t, err) + assert.Equal(t, 101, arena.CurrentMatch.Blue1) + assert.Equal(t, 101, arena.AllianceStations["B1"].Team.Id) + err = arena.assignTeam(104, "R4") + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Invalid alliance station") + } + + // Substitute teams into practice match. + match := model.Match{Type: "practice", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} + arena.Database.CreateMatch(&match) + arena.LoadMatch(&match) + err = arena.SubstituteTeam(107, "R1") + assert.Nil(t, err) + assert.Equal(t, 107, arena.CurrentMatch.Red1) + assert.Equal(t, 107, arena.AllianceStations["R1"].Team.Id) + matchResult := model.NewMatchResult() + matchResult.MatchId = arena.CurrentMatch.Id + + // Check that substitution is disallowed in qualification matches. + match = model.Match{Type: "qualification", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} + arena.Database.CreateMatch(&match) + arena.LoadMatch(&match) + err = arena.SubstituteTeam(107, "R1") + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Can't substitute teams for qualification matches.") + } + match = model.Match{Type: "elimination", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} + arena.Database.CreateMatch(&match) + arena.LoadMatch(&match) + assert.Nil(t, arena.SubstituteTeam(107, "R1")) +} + +func TestSetupNetwork(t *testing.T) { + arena := setupTestArena(t) + + // Verify the setup ran by checking the log for the expected failure messages. + arena.EventSettings.NetworkSecurityEnabled = true + arena.accessPoint.port = 10022 + arena.networkSwitch.port = 10023 + arena.LoadMatch(&model.Match{Type: "test"}) + var writer bytes.Buffer + log.SetOutput(&writer) + time.Sleep(time.Millisecond * 10) // Allow some time for the asynchronous configuration to happen. + assert.Contains(t, writer.String(), "Failed to configure team Ethernet") + assert.Contains(t, writer.String(), "Failed to configure team WiFi") +} diff --git a/bandwidth_monitor.go b/field/bandwidth_monitor.go similarity index 74% rename from bandwidth_monitor.go rename to field/bandwidth_monitor.go index 4606e23..a5bda5b 100644 --- a/bandwidth_monitor.go +++ b/field/bandwidth_monitor.go @@ -3,7 +3,7 @@ // // Methods for monitoring team bandwidth usage across a managed switch. -package main +package field import ( "fmt" @@ -25,6 +25,7 @@ const ( ) type BandwidthMonitor struct { + allianceStations *map[string]*AllianceStation snmpClient *wapsnmp.WapSNMP toRobotOid wapsnmp.Oid fromRobotOid wapsnmp.Oid @@ -34,14 +35,29 @@ type BandwidthMonitor struct { } // Loops indefinitely to query the managed switch via SNMP (Simple Network Management Protocol). -func MonitorBandwidth() { - monitor := BandwidthMonitor{toRobotOid: wapsnmp.MustParseOid(toRobotBytesOid), - fromRobotOid: wapsnmp.MustParseOid(fromRobotBytesOid)} +func (arena *Arena) monitorBandwidth() { + monitor := BandwidthMonitor{allianceStations: &arena.AllianceStations, + toRobotOid: wapsnmp.MustParseOid(toRobotBytesOid), fromRobotOid: wapsnmp.MustParseOid(fromRobotBytesOid)} for { - if eventSettings.NetworkSecurityEnabled && eventSettings.BandwidthMonitoringEnabled { + if monitor.snmpClient != nil && monitor.snmpClient.Target != arena.EventSettings.SwitchAddress { + // Switch address has changed; must re-create the SNMP client. + monitor.snmpClient.Close() + monitor.snmpClient = nil + } + + if monitor.snmpClient == nil { + var err error + monitor.snmpClient, err = wapsnmp.NewWapSNMP(arena.EventSettings.SwitchAddress, + arena.EventSettings.SwitchPassword, wapsnmp.SNMPv2c, 2*time.Second, 0) + if err != nil { + log.Printf("Error starting bandwidth monitoring: %v", err) + } + } + + if arena.EventSettings.NetworkSecurityEnabled && arena.EventSettings.BandwidthMonitoringEnabled { err := monitor.updateBandwidth() if err != nil { - log.Printf("Bandwidth monitoring error: %s", err) + log.Printf("Bandwidth monitoring error: %v", err) } } time.Sleep(time.Millisecond * monitoringIntervalMs) @@ -49,21 +65,6 @@ func MonitorBandwidth() { } func (monitor *BandwidthMonitor) updateBandwidth() error { - if monitor.snmpClient != nil && monitor.snmpClient.Target != eventSettings.SwitchAddress { - // Switch address has changed; must re-create the SNMP client. - monitor.snmpClient.Close() - monitor.snmpClient = nil - } - - if monitor.snmpClient == nil { - var err error - monitor.snmpClient, err = wapsnmp.NewWapSNMP(eventSettings.SwitchAddress, eventSettings.SwitchPassword, - wapsnmp.SNMPv2c, 2*time.Second, 0) - if err != nil { - return err - } - } - // Retrieve total number of bytes sent/received per port. toRobotBytes, err := monitor.snmpClient.GetTable(monitor.toRobotOid) if err != nil { @@ -90,7 +91,7 @@ func (monitor *BandwidthMonitor) updateBandwidth() error { func (monitor *BandwidthMonitor) updateStationBandwidth(station string, port int, toRobotBytes map[string]interface{}, fromRobotBytes map[string]interface{}) { - dsConn := mainArena.AllianceStations[station].DsConn + dsConn := (*monitor.allianceStations)[station].DsConn if dsConn == nil { // No team assigned; just skip it. return diff --git a/driver_station_connection.go b/field/driver_station_connection.go similarity index 86% rename from driver_station_connection.go rename to field/driver_station_connection.go index 2733159..332ba83 100644 --- a/driver_station_connection.go +++ b/field/driver_station_connection.go @@ -3,7 +3,7 @@ // // Model and methods for interacting with a team's Driver Station. -package main +package field import ( "fmt" @@ -52,7 +52,7 @@ var allianceStationPositionMap = map[string]byte{"R1": 0, "R2": 1, "R3": 2, "B1" var driverStationTcpListenAddress = "10.0.100.5" // The DS will try to connect to this address only. // Opens a UDP connection for communicating to the driver station. -func NewDriverStationConnection(teamId int, allianceStation string, tcpConn net.Conn) (*DriverStationConnection, error) { +func newDriverStationConnection(teamId int, allianceStation string, tcpConn net.Conn) (*DriverStationConnection, error) { ipAddress, _, err := net.SplitHostPort(tcpConn.RemoteAddr().String()) if err != nil { return nil, err @@ -67,7 +67,7 @@ func NewDriverStationConnection(teamId int, allianceStation string, tcpConn net. } // Loops indefinitely to read packets and update connection status. -func ListenForDsUdpPackets() { +func (arena *Arena) listenForDsUdpPackets() { udpAddress, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", driverStationUdpReceivePort)) listener, err := net.ListenUDP("udp4", udpAddress) if err != nil { @@ -82,7 +82,7 @@ func ListenForDsUdpPackets() { teamId := int(data[4])<<8 + int(data[5]) var dsConn *DriverStationConnection - for _, allianceStation := range mainArena.AllianceStations { + for _, allianceStation := range arena.AllianceStations { if allianceStation.Team != nil && allianceStation.Team.Id == teamId { dsConn = allianceStation.DsConn break @@ -105,8 +105,8 @@ func ListenForDsUdpPackets() { } // Sends a control packet to the Driver Station and checks for timeout conditions. -func (dsConn *DriverStationConnection) Update() error { - err := dsConn.sendControlPacket() +func (dsConn *DriverStationConnection) update(arena *Arena) error { + err := dsConn.sendControlPacket(arena) if err != nil { return err } @@ -123,7 +123,7 @@ func (dsConn *DriverStationConnection) Update() error { return nil } -func (dsConn *DriverStationConnection) Close() { +func (dsConn *DriverStationConnection) close() { if dsConn.log != nil { dsConn.log.Close() } @@ -145,7 +145,7 @@ func (dsConn *DriverStationConnection) signalMatchStart(match *model.Match) erro } // Serializes the control information into a packet. -func (dsConn *DriverStationConnection) encodeControlPacket() [22]byte { +func (dsConn *DriverStationConnection) encodeControlPacket(arena *Arena) [22]byte { var packet [22]byte // Packet number, stored big-endian in two bytes. @@ -174,7 +174,7 @@ func (dsConn *DriverStationConnection) encodeControlPacket() [22]byte { packet[5] = allianceStationPositionMap[dsConn.AllianceStation] // Match information. - match := mainArena.currentMatch + match := arena.CurrentMatch if match.Type == "practice" { packet[6] = 1 } else if match.Type == "qualification" { @@ -205,20 +205,20 @@ func (dsConn *DriverStationConnection) encodeControlPacket() [22]byte { // Remaining number of seconds in match. var matchSecondsRemaining int - switch mainArena.MatchState { - case preMatch: + switch arena.MatchState { + case PreMatch: fallthrough - case startMatch: + case StartMatch: fallthrough - case autoPeriod: - matchSecondsRemaining = game.MatchTiming.AutoDurationSec - int(mainArena.MatchTimeSec()) - case pausePeriod: + case AutoPeriod: + matchSecondsRemaining = game.MatchTiming.AutoDurationSec - int(arena.MatchTimeSec()) + case PausePeriod: matchSecondsRemaining = game.MatchTiming.TeleopDurationSec - case teleopPeriod: + case TeleopPeriod: fallthrough - case endgamePeriod: + case EndgamePeriod: matchSecondsRemaining = game.MatchTiming.AutoDurationSec + game.MatchTiming.TeleopDurationSec + - game.MatchTiming.PauseDurationSec - int(mainArena.MatchTimeSec()) + game.MatchTiming.PauseDurationSec - int(arena.MatchTimeSec()) default: matchSecondsRemaining = 0 } @@ -232,8 +232,8 @@ func (dsConn *DriverStationConnection) encodeControlPacket() [22]byte { } // Builds and sends the next control packet to the Driver Station. -func (dsConn *DriverStationConnection) sendControlPacket() error { - packet := dsConn.encodeControlPacket() +func (dsConn *DriverStationConnection) sendControlPacket(arena *Arena) error { + packet := dsConn.encodeControlPacket(arena) if dsConn.udpConn != nil { _, err := dsConn.udpConn.Write(packet[:]) if err != nil { @@ -254,7 +254,7 @@ func (dsConn *DriverStationConnection) decodeStatusPacket(data [36]byte) { } // Listens for TCP connection requests to Cheesy Arena from driver stations. -func ListenForDriverStations() { +func (arena *Arena) listenForDriverStations() { l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", driverStationTcpListenAddress, driverStationTcpListenPort)) if err != nil { log.Printf("Error opening driver station TCP socket: %v", err.Error()) @@ -286,7 +286,7 @@ func ListenForDriverStations() { teamId := int(packet[3])<<8 + int(packet[4]) // Check to see if the team is supposed to be on the field, and notify the DS accordingly. - assignedStation := mainArena.getAssignedAllianceStation(teamId) + assignedStation := arena.getAssignedAllianceStation(teamId) if assignedStation == "" { log.Printf("Rejecting connection from Team %d, who is not in the current match, soon.", teamId) go func() { @@ -305,33 +305,33 @@ func ListenForDriverStations() { assignmentPacket[4] = 0 _, err = tcpConn.Write(assignmentPacket[:]) if err != nil { - log.Println("Error sending driver station assignment packet: %v", err) + log.Printf("Error sending driver station assignment packet: %v", err) tcpConn.Close() continue } - dsConn, err := NewDriverStationConnection(teamId, assignedStation, tcpConn) + dsConn, err := newDriverStationConnection(teamId, assignedStation, tcpConn) if err != nil { - log.Println("Error registering driver station connection: %v", err) + log.Printf("Error registering driver station connection: %v", err) tcpConn.Close() continue } - mainArena.AllianceStations[assignedStation].DsConn = dsConn + arena.AllianceStations[assignedStation].DsConn = dsConn // Spin up a goroutine to handle further TCP communication with this driver station. - go dsConn.handleTcpConnection() + go dsConn.handleTcpConnection(arena) } } -func (dsConn *DriverStationConnection) handleTcpConnection() { +func (dsConn *DriverStationConnection) handleTcpConnection(arena *Arena) { buffer := make([]byte, maxTcpPacketBytes) for { dsConn.tcpConn.SetReadDeadline(time.Now().Add(time.Second * driverStationTcpLinkTimeoutSec)) _, err := dsConn.tcpConn.Read(buffer) if err != nil { - log.Printf("Error reading from connection for Team %d: %v\n", dsConn.TeamId, err.Error()) - dsConn.Close() - mainArena.AllianceStations[dsConn.AllianceStation].DsConn = nil + log.Printf("Error reading from connection for Team %d: %v", dsConn.TeamId, err) + dsConn.close() + arena.AllianceStations[dsConn.AllianceStation].DsConn = nil break } @@ -347,7 +347,7 @@ func (dsConn *DriverStationConnection) handleTcpConnection() { } // Log the packet if the match is in progress. - matchTimeSec := mainArena.MatchTimeSec() + matchTimeSec := arena.MatchTimeSec() if matchTimeSec > 0 && dsConn.log != nil { dsConn.log.LogDsPacket(matchTimeSec, packetType, dsConn) } diff --git a/driver_station_connection_test.go b/field/driver_station_connection_test.go similarity index 69% rename from driver_station_connection_test.go rename to field/driver_station_connection_test.go index 70a40f2..5975a26 100644 --- a/driver_station_connection_test.go +++ b/field/driver_station_connection_test.go @@ -1,7 +1,7 @@ // Copyright 2014 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) -package main +package field import ( "github.com/stretchr/testify/assert" @@ -11,15 +11,15 @@ import ( ) func TestEncodeControlPacket(t *testing.T) { - setupTest(t) + arena := setupTestArena(t) tcpConn := setupFakeTcpConnection(t) defer tcpConn.Close() - dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) + dsConn, err := newDriverStationConnection(254, "R1", tcpConn) assert.Nil(t, err) - defer dsConn.Close() + defer dsConn.close() - data := dsConn.encodeControlPacket() + data := dsConn.encodeControlPacket(arena) assert.Equal(t, byte(0), data[5]) assert.Equal(t, byte(0), data[6]) assert.Equal(t, byte(0), data[20]) @@ -27,109 +27,111 @@ func TestEncodeControlPacket(t *testing.T) { // Check the different alliance station values. dsConn.AllianceStation = "R2" - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(1), data[5]) dsConn.AllianceStation = "R3" - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(2), data[5]) dsConn.AllianceStation = "B1" - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(3), data[5]) dsConn.AllianceStation = "B2" - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(4), data[5]) dsConn.AllianceStation = "B3" - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(5), data[5]) // Check packet count rollover. dsConn.packetCount = 255 - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(0), data[0]) assert.Equal(t, byte(255), data[1]) - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(1), data[0]) assert.Equal(t, byte(0), data[1]) - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(1), data[0]) assert.Equal(t, byte(1), data[1]) dsConn.packetCount = 65535 - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(255), data[0]) assert.Equal(t, byte(255), data[1]) - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(0), data[0]) assert.Equal(t, byte(0), data[1]) // Check different robot statuses. dsConn.Auto = true - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(2), data[3]) dsConn.Enabled = true - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(6), data[3]) dsConn.Auto = false - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(4), data[3]) dsConn.EmergencyStop = true - data = dsConn.encodeControlPacket() + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(132), data[3]) // Check different match types. - mainArena.currentMatch.Type = "practice" - data = dsConn.encodeControlPacket() + arena.CurrentMatch.Type = "practice" + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(1), data[6]) - mainArena.currentMatch.Type = "qualification" - data = dsConn.encodeControlPacket() + arena.CurrentMatch.Type = "qualification" + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(2), data[6]) - mainArena.currentMatch.Type = "elimination" - data = dsConn.encodeControlPacket() + arena.CurrentMatch.Type = "elimination" + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(3), data[6]) // Check the countdown at different points during the match. - mainArena.MatchState = autoPeriod - mainArena.matchStartTime = time.Now().Add(-time.Duration(4 * time.Second)) - data = dsConn.encodeControlPacket() + arena.MatchState = AutoPeriod + arena.MatchStartTime = time.Now().Add(-time.Duration(4 * time.Second)) + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(11), data[21]) - mainArena.MatchState = pausePeriod - mainArena.matchStartTime = time.Now().Add(-time.Duration(16 * time.Second)) - data = dsConn.encodeControlPacket() + arena.MatchState = PausePeriod + arena.MatchStartTime = time.Now().Add(-time.Duration(16 * time.Second)) + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(135), data[21]) - mainArena.MatchState = teleopPeriod - mainArena.matchStartTime = time.Now().Add(-time.Duration(33 * time.Second)) - data = dsConn.encodeControlPacket() + arena.MatchState = TeleopPeriod + arena.MatchStartTime = time.Now().Add(-time.Duration(33 * time.Second)) + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(119), data[21]) - mainArena.MatchState = endgamePeriod - mainArena.matchStartTime = time.Now().Add(-time.Duration(150 * time.Second)) - data = dsConn.encodeControlPacket() + arena.MatchState = EndgamePeriod + arena.MatchStartTime = time.Now().Add(-time.Duration(150 * time.Second)) + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(2), data[21]) - mainArena.MatchState = postMatch - mainArena.matchStartTime = time.Now().Add(-time.Duration(180 * time.Second)) - data = dsConn.encodeControlPacket() + arena.MatchState = PostMatch + arena.MatchStartTime = time.Now().Add(-time.Duration(180 * time.Second)) + data = dsConn.encodeControlPacket(arena) assert.Equal(t, byte(0), data[21]) } func TestSendControlPacket(t *testing.T) { + arena := setupTestArena(t) + tcpConn := setupFakeTcpConnection(t) defer tcpConn.Close() - dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) + dsConn, err := newDriverStationConnection(254, "R1", tcpConn) assert.Nil(t, err) - defer dsConn.Close() + defer dsConn.close() // No real way of checking this since the destination IP is remote, so settle for there being no errors. - err = dsConn.sendControlPacket() + err = dsConn.sendControlPacket(arena) assert.Nil(t, err) } func TestDecodeStatusPacket(t *testing.T) { tcpConn := setupFakeTcpConnection(t) defer tcpConn.Close() - dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) + dsConn, err := newDriverStationConnection(254, "R1", tcpConn) assert.Nil(t, err) - defer dsConn.Close() + defer dsConn.close() data := [36]byte{22, 28, 103, 19, 192, 0, 246, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} @@ -139,12 +141,11 @@ func TestDecodeStatusPacket(t *testing.T) { } func TestListenForDriverStations(t *testing.T) { - setupTest(t) + arena := setupTestArena(t) oldAddress := driverStationTcpListenAddress driverStationTcpListenAddress = "127.0.0.1" - go ListenForDriverStations() - mainArena.Setup() + go arena.listenForDriverStations() time.Sleep(time.Millisecond * 10) driverStationTcpListenAddress = oldAddress // Put it back to avoid affecting other tests. @@ -171,7 +172,7 @@ func TestListenForDriverStations(t *testing.T) { } // Connect as a team in the current match. - mainArena.AssignTeam(1503, "B2") + arena.assignTeam(1503, "B2") tcpConn, err = net.Dial("tcp", "127.0.0.1:1750") if assert.Nil(t, err) { defer tcpConn.Close() @@ -183,7 +184,7 @@ func TestListenForDriverStations(t *testing.T) { assert.Equal(t, [5]byte{0, 3, 25, 4, 0}, dataReceived) time.Sleep(time.Millisecond * 10) - dsConn := mainArena.AllianceStations["B2"].DsConn + dsConn := arena.AllianceStations["B2"].DsConn if assert.NotNil(t, dsConn) { assert.Equal(t, 1503, dsConn.TeamId) assert.Equal(t, "B2", dsConn.AllianceStation) diff --git a/switch_config.go b/field/network_switch.go similarity index 77% rename from switch_config.go rename to field/network_switch.go index 3507192..f9c9160 100644 --- a/switch_config.go +++ b/field/network_switch.go @@ -3,7 +3,7 @@ // // Methods for configuring a Cisco Switch 3500-series switch for team VLANs. -package main +package field import ( "bufio" @@ -16,18 +16,27 @@ import ( "sync" ) -var switchTelnetPort = 23 +const switchTelnetPort = 23 -var switchMutex sync.Mutex +type NetworkSwitch struct { + address string + port int + password string + mutex sync.Mutex +} + +func NewNetworkSwitch(address, password string) *NetworkSwitch { + return &NetworkSwitch{address: address, port: switchTelnetPort, password: password} +} // Sets up wired networks for the given set of teams. -func ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *model.Team) error { +func (ns *NetworkSwitch) ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *model.Team) error { // Make sure multiple configurations aren't being set at the same time. - switchMutex.Lock() - defer switchMutex.Unlock() + ns.mutex.Lock() + defer ns.mutex.Unlock() // Determine what new team VLANs are needed and build the commands to set them up. - oldTeamVlans, err := getTeamVlans() + oldTeamVlans, err := ns.getTeamVlans() if err != nil { return err } @@ -71,7 +80,7 @@ func ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *model.Team) er // Build and run the overall command to do everything in a single telnet session. command := removeTeamVlansCommand + addTeamVlansCommand if len(command) > 0 { - _, err = runSwitchConfigCommand(removeTeamVlansCommand + addTeamVlansCommand) + _, err = ns.runConfigCommand(removeTeamVlansCommand + addTeamVlansCommand) if err != nil { return err } @@ -81,9 +90,9 @@ func ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *model.Team) er } // Returns a map of currently-configured teams to VLANs. -func getTeamVlans() (map[int]int, error) { +func (ns *NetworkSwitch) getTeamVlans() (map[int]int, error) { // Get the entire config dump. - config, err := runSwitchCommand("show running-config\n") + config, err := ns.runCommand("show running-config\n") if err != nil { return nil, err } @@ -110,9 +119,9 @@ func getTeamVlans() (map[int]int, error) { // Logs into the switch via Telnet and runs the given command in user exec mode. Reads the output and // returns it as a string. -func runSwitchCommand(command string) (string, error) { +func (ns *NetworkSwitch) runCommand(command string) (string, error) { // Open a Telnet connection to the switch. - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", eventSettings.SwitchAddress, switchTelnetPort)) + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ns.address, ns.port)) if err != nil { return "", err } @@ -120,8 +129,8 @@ func runSwitchCommand(command string) (string, error) { // Login to the AP, send the command, and log out all at once. writer := bufio.NewWriter(conn) - _, err = writer.WriteString(fmt.Sprintf("%s\nenable\n%s\nterminal length 0\n%sexit\n", - eventSettings.SwitchPassword, eventSettings.SwitchPassword, command)) + _, err = writer.WriteString(fmt.Sprintf("%s\nenable\n%s\nterminal length 0\n%sexit\n", ns.password, ns.password, + command)) if err != nil { return "", err } @@ -141,7 +150,6 @@ func runSwitchCommand(command string) (string, error) { // Logs into the switch via Telnet and runs the given command in global configuration mode. Reads the output // and returns it as a string. -func runSwitchConfigCommand(command string) (string, error) { - return runSwitchCommand(fmt.Sprintf("config terminal\n%send\ncopy running-config startup-config\n\n", - command)) +func (ns *NetworkSwitch) runConfigCommand(command string) (string, error) { + return ns.runCommand(fmt.Sprintf("config terminal\n%send\ncopy running-config startup-config\n\n", command)) } diff --git a/switch_config_test.go b/field/network_switch_test.go similarity index 79% rename from switch_config_test.go rename to field/network_switch_test.go index cb4a3e3..90b654e 100644 --- a/switch_config_test.go +++ b/field/network_switch_test.go @@ -1,7 +1,7 @@ // Copyright 2014 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) -package main +package field import ( "bytes" @@ -14,27 +14,27 @@ import ( ) func TestConfigureSwitch(t *testing.T) { - switchTelnetPort = 9050 - eventSettings = &model.EventSettings{SwitchAddress: "127.0.0.1", SwitchPassword: "password"} + ns := NewNetworkSwitch("127.0.0.1", "password") + ns.port = 9050 var command string // Should do nothing if current configuration is blank. - mockTelnet(t, switchTelnetPort, "", &command) - assert.Nil(t, ConfigureTeamEthernet(nil, nil, nil, nil, nil, nil)) + mockTelnet(t, ns.port, "", &command) + assert.Nil(t, ns.ConfigureTeamEthernet(nil, nil, nil, nil, nil, nil)) assert.Equal(t, "", command) // Should remove any existing teams but not other SSIDs. - switchTelnetPort += 1 - mockTelnet(t, switchTelnetPort, + ns.port += 1 + mockTelnet(t, ns.port, "interface Vlan100\nip address 10.0.100.2\ninterface Vlan50\nip address 10.2.54.61\n", &command) - assert.Nil(t, ConfigureTeamEthernet(nil, nil, nil, nil, nil, nil)) + assert.Nil(t, ns.ConfigureTeamEthernet(nil, nil, nil, nil, nil, nil)) assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\ninterface Vlan50\nno ip"+ " address\nno access-list 150\nend\ncopy running-config startup-config\n\nexit\n", command) // Should configure new teams and leave existing ones alone if still needed. - switchTelnetPort += 1 - mockTelnet(t, switchTelnetPort, "interface Vlan50\nip address 10.2.54.61\n", &command) - assert.Nil(t, ConfigureTeamEthernet(nil, &model.Team{Id: 1114}, nil, nil, &model.Team{Id: 254}, nil)) + ns.port += 1 + mockTelnet(t, ns.port, "interface Vlan50\nip address 10.2.54.61\n", &command) + assert.Nil(t, ns.ConfigureTeamEthernet(nil, &model.Team{Id: 1114}, nil, nil, &model.Team{Id: 254}, nil)) assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ "ip dhcp excluded-address 10.11.14.1 10.11.14.100\nno ip dhcp pool dhcp20\nip dhcp pool dhcp20\n"+ "network 10.11.14.0 255.255.255.0\ndefault-router 10.11.14.61\nlease 7\nno access-list 120\n"+ diff --git a/notifier.go b/field/notifier.go similarity index 99% rename from notifier.go rename to field/notifier.go index 49733b7..920ab0b 100644 --- a/notifier.go +++ b/field/notifier.go @@ -3,7 +3,7 @@ // // Publish-subscribe model for nonblocking notification of server events to websocket clients. -package main +package field import ( "log" diff --git a/notifier_test.go b/field/notifier_test.go similarity index 99% rename from notifier_test.go rename to field/notifier_test.go index 02ded1c..14c340a 100644 --- a/notifier_test.go +++ b/field/notifier_test.go @@ -1,7 +1,7 @@ // Copyright 2014 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) -package main +package field import ( "github.com/stretchr/testify/assert" diff --git a/field/realtime_score.go b/field/realtime_score.go new file mode 100644 index 0000000..5f03759 --- /dev/null +++ b/field/realtime_score.go @@ -0,0 +1,22 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Model representing the current state of the score during a match. + +package field + +import "github.com/Team254/cheesy-arena/game" + +type RealtimeScore struct { + CurrentScore *game.Score + Cards map[string]string + TeleopCommitted bool + FoulsCommitted bool +} + +func NewRealtimeScore() *RealtimeScore { + realtimeScore := new(RealtimeScore) + realtimeScore.CurrentScore = new(game.Score) + realtimeScore.Cards = make(map[string]string) + return realtimeScore +} diff --git a/team_match_log.go b/field/team_match_log.go similarity index 82% rename from team_match_log.go rename to field/team_match_log.go index 0fe07c9..ca3813d 100644 --- a/team_match_log.go +++ b/field/team_match_log.go @@ -3,13 +3,14 @@ // // Utilities for logging packets received from team driver stations during a match. -package main +package field import ( "fmt" "github.com/Team254/cheesy-arena/model" "log" "os" + "path/filepath" "time" ) @@ -22,13 +23,13 @@ type TeamMatchLog struct { // Creates a file to log to for the given match and team. func NewTeamMatchLog(teamId int, match *model.Match) (*TeamMatchLog, error) { - err := os.MkdirAll(logsDir, 0755) + err := os.MkdirAll(filepath.Join(model.BaseDir, logsDir), 0755) if err != nil { return nil, err } - filename := fmt.Sprintf("%s/%s_%s_Match_%s_%d.csv", logsDir, time.Now().Format("20060102150405"), - match.CapitalizedType(), match.DisplayName, teamId) + filename := fmt.Sprintf("%s/%s_%s_Match_%s_%d.csv", filepath.Join(model.BaseDir, logsDir), + time.Now().Format("20060102150405"), match.CapitalizedType(), match.DisplayName, teamId) logFile, err := os.Create(filename) if err != nil { return nil, err diff --git a/field/test_helpers.go b/field/test_helpers.go new file mode 100644 index 0000000..ee1f6a1 --- /dev/null +++ b/field/test_helpers.go @@ -0,0 +1,28 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Helper methods for use in tests in this package and others. + +package field + +import ( + "fmt" + "github.com/Team254/cheesy-arena/model" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func SetupTestArena(t *testing.T, uniqueName string) *Arena { + dbPath := fmt.Sprintf("%s_test.db", uniqueName) + os.Remove(filepath.Join(model.BaseDir, dbPath)) + arena, err := NewArena(dbPath) + assert.Nil(t, err) + return arena +} + +func setupTestArena(t *testing.T) *Arena { + model.BaseDir = ".." + return SetupTestArena(t, "field") +} diff --git a/fta_display.go b/fta_display.go index 48a26f0..380bd48 100644 --- a/fta_display.go +++ b/fta_display.go @@ -14,12 +14,12 @@ import ( ) // Renders the FTA diagnostic display. -func FtaDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) ftaDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err := template.ParseFiles("templates/fta_display.html", "templates/base.html") if err != nil { handleWebErr(w, err) @@ -27,7 +27,7 @@ func FtaDisplayHandler(w http.ResponseWriter, r *http.Request) { } data := struct { *model.EventSettings - }{eventSettings} + }{web.arena.EventSettings} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -36,7 +36,7 @@ func FtaDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the FTA display client to receive status updates. -func FtaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { +func (web *Web) ftaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket. websocket, err := NewWebsocket(w, r) @@ -46,13 +46,13 @@ func FtaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - robotStatusListener := mainArena.robotStatusNotifier.Listen() + robotStatusListener := web.arena.RobotStatusNotifier.Listen() defer close(robotStatusListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Send the various notifications immediately upon connection. - err = websocket.Write("status", mainArena) + err = websocket.Write("status", web.arena.GetStatus()) if err != nil { log.Printf("Websocket error: %s", err) return @@ -69,7 +69,7 @@ func FtaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { return } messageType = "status" - message = mainArena + message = web.arena.GetStatus() case _, ok := <-reloadDisplaysListener: if !ok { return diff --git a/fta_display_test.go b/fta_display_test.go index e0d9a7b..cb667f3 100644 --- a/fta_display_test.go +++ b/fta_display_test.go @@ -9,9 +9,9 @@ import ( ) func TestFtaDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/fta") + recorder := web.getHttpResponse("/displays/fta") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Field Monitor - Untitled Event - Cheesy Arena") } diff --git a/lights.go b/lights.go deleted file mode 100644 index f0cddf7..0000000 --- a/lights.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2014 Team 254. All Rights Reserved. -// Author: pat@patfairbank.com (Patrick Fairbank) -// -// Methods for controlling the field LED lighting. - -package main - -import ( - "log" - "net" - "time" -) - -const ( - RED_DEFENSE = "redDefense" - BLUE_DEFENSE = "blueDefense" -) - -type LightPacket [32]byte - -type Lights struct { - connections map[string]*net.Conn - packets map[string]*LightPacket - oldPackets map[string]*LightPacket - newConnections bool - currentMode string - animationCount int -} - -// Sets the color by name and transition time for the given LED channel. -func (lightPacket *LightPacket) setColorFade(channel int, color string, fade byte) { - switch color { - case "off": - lightPacket.setRgbFade(channel, 0, 0, 0, fade) - case "white": - lightPacket.setRgbFade(channel, 15, 15, 15, fade) - case "red": - lightPacket.setRgbFade(channel, 15, 0, 0, fade) - case "blue": - lightPacket.setRgbFade(channel, 0, 0, 15, fade) - case "green": - lightPacket.setRgbFade(channel, 0, 15, 0, fade) - case "yellow": - lightPacket.setRgbFade(channel, 13, 15, 7, fade) - case "darkred": - lightPacket.setRgbFade(channel, 1, 0, 0, fade) - case "darkblue": - lightPacket.setRgbFade(channel, 0, 0, 1, fade) - } -} - -// Sets the color by name with instant transition for the given LED channel. -func (lightPacket *LightPacket) setColor(channel int, color string) { - lightPacket.setColorFade(channel, color, 0) -} - -// Sets the color by RGB values and transition time for the given LED channel. -func (lightPacket *LightPacket) setRgbFade(channel int, red byte, green byte, blue byte, fade byte) { - lightPacket[channel*4] = red - lightPacket[channel*4+1] = green - lightPacket[channel*4+2] = blue - lightPacket[channel*4+3] = fade -} - -// Sets the color by name with instant transition for all LED channels. -func (lightPacket *LightPacket) setAllColor(color string) { - lightPacket.setAllColorFade(color, 0) -} - -// Sets the color by name and transition time for all LED channels. -func (lightPacket *LightPacket) setAllColorFade(color string, fade byte) { - for i := 0; i < 8; i++ { - lightPacket.setColorFade(i, color, fade) - } -} - -func (lights *Lights) Setup() error { - lights.currentMode = "off" - - err := lights.SetupConnections() - if err != nil { - return err - } - - lights.packets = make(map[string]*LightPacket) - lights.packets[RED_DEFENSE] = &LightPacket{} - lights.packets[BLUE_DEFENSE] = &LightPacket{} - lights.oldPackets = make(map[string]*LightPacket) - lights.oldPackets[RED_DEFENSE] = &LightPacket{} - lights.oldPackets[BLUE_DEFENSE] = &LightPacket{} - - lights.sendLights() - - // Set up a goroutine to animate the lights when necessary. - ticker := time.NewTicker(time.Millisecond * 50) - go func() { - for _ = range ticker.C { - lights.animate() - } - }() - return nil -} - -func (lights *Lights) SetupConnections() error { - lights.connections = make(map[string]*net.Conn) - // TODO(patrick): Update for 2017. - if err := lights.connect(RED_DEFENSE, ""); err != nil { - return err - } - if err := lights.connect(BLUE_DEFENSE, ""); err != nil { - return err - } - lights.newConnections = true - return nil -} - -func (lights *Lights) connect(controller, address string) error { - // Don't enable lights for a side if the controller address is not configured. - if len(address) != 0 { - conn, err := net.Dial("udp4", address) - lights.connections[controller] = &conn - if err != nil { - return err - } - } else { - lights.connections[controller] = nil - } - return nil -} - -func (lights *Lights) ClearAll() { - lights.packets[RED_DEFENSE].setAllColorFade("off", 10) - lights.packets[BLUE_DEFENSE].setAllColorFade("off", 10) - lights.sendLights() -} - -// Turns all lights green to signal that the field is safe to enter. -func (lights *Lights) SetFieldReset() { - lights.packets[RED_DEFENSE].setAllColor("green") - lights.packets[BLUE_DEFENSE].setAllColor("green") - lights.sendLights() -} - -// Sets the lights to the given non-match mode for show or testing. -func (lights *Lights) SetMode(mode string) { - lights.currentMode = mode - lights.animationCount = 0 - - switch mode { - case "off": - lights.packets[RED_DEFENSE].setAllColor("off") - lights.packets[BLUE_DEFENSE].setAllColor("off") - case "all_white": - lights.packets[RED_DEFENSE].setAllColor("white") - lights.packets[BLUE_DEFENSE].setAllColor("white") - case "all_red": - lights.packets[RED_DEFENSE].setAllColor("red") - lights.packets[BLUE_DEFENSE].setAllColor("red") - case "all_green": - lights.packets[RED_DEFENSE].setAllColor("green") - lights.packets[BLUE_DEFENSE].setAllColor("green") - case "all_blue": - lights.packets[RED_DEFENSE].setAllColor("blue") - lights.packets[BLUE_DEFENSE].setAllColor("blue") - } - lights.sendLights() -} - -// Sends a control packet to the LED controllers only if their state needs to be updated. -func (lights *Lights) sendLights() { - for controller, connection := range lights.connections { - if lights.newConnections || *lights.packets[controller] != *lights.oldPackets[controller] { - if connection != nil { - _, err := (*connection).Write(lights.packets[controller][:]) - if err != nil { - log.Printf("Failed to send %s light packet.", controller) - } - } - } - *lights.oldPackets[controller] = *lights.packets[controller] - } - lights.newConnections = false -} - -// State machine for controlling light sequences in the non-match modes. -func (lights *Lights) animate() { - lights.animationCount += 1 - - switch lights.currentMode { - case "strobe": - switch lights.animationCount { - case 1: - lights.packets[RED_DEFENSE].setAllColor("white") - lights.packets[BLUE_DEFENSE].setAllColor("off") - case 2: - lights.packets[RED_DEFENSE].setAllColor("off") - lights.packets[BLUE_DEFENSE].setAllColor("white") - fallthrough - default: - lights.animationCount = 0 - } - lights.sendLights() - case "fade_red": - if lights.animationCount == 1 { - lights.packets[RED_DEFENSE].setAllColorFade("red", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("red", 18) - } else if lights.animationCount == 61 { - lights.packets[RED_DEFENSE].setAllColorFade("darkred", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("darkred", 18) - } else if lights.animationCount > 120 { - lights.animationCount = 0 - } - lights.sendLights() - case "fade_blue": - if lights.animationCount == 1 { - lights.packets[RED_DEFENSE].setAllColorFade("blue", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("blue", 18) - } else if lights.animationCount == 61 { - lights.packets[RED_DEFENSE].setAllColorFade("darkblue", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("darkblue", 18) - } else if lights.animationCount > 120 { - lights.animationCount = 0 - } - lights.sendLights() - case "fade_red_blue": - if lights.animationCount == 1 { - lights.packets[RED_DEFENSE].setAllColorFade("blue", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("darkred", 18) - } else if lights.animationCount == 61 { - lights.packets[RED_DEFENSE].setAllColorFade("darkblue", 18) - lights.packets[BLUE_DEFENSE].setAllColorFade("red", 18) - } else if lights.animationCount > 120 { - lights.animationCount = 0 - } - lights.sendLights() - } -} - -// Turns on the lights below the defenses, with one channel per defense. -func (lights *Lights) SetDefenses(redDefensesStrength, blueDefensesStrength [5]int) { - for i := 0; i < 5; i++ { - if redDefensesStrength[i] == 0 { - lights.packets[RED_DEFENSE].setColorFade(i, "off", 10) - } else if redDefensesStrength[i] == 1 { - lights.packets[RED_DEFENSE].setColorFade(i, "yellow", 10) - } else { - lights.packets[RED_DEFENSE].setColorFade(i, "red", 10) - } - - if blueDefensesStrength[i] == 0 { - lights.packets[BLUE_DEFENSE].setColorFade(i, "off", 10) - } else if blueDefensesStrength[i] == 1 { - lights.packets[BLUE_DEFENSE].setColorFade(i, "yellow", 10) - } else { - lights.packets[BLUE_DEFENSE].setColorFade(i, "blue", 10) - } - } - lights.sendLights() -} diff --git a/main.go b/main.go index 0874a4c..3336a0e 100644 --- a/main.go +++ b/main.go @@ -4,48 +4,28 @@ package main import ( - "github.com/Team254/cheesy-arena/model" - "github.com/Team254/cheesy-arena/partner" + "github.com/Team254/cheesy-arena/field" "log" "math/rand" "time" ) const eventDbPath = "./event.db" - -var db *model.Database -var eventSettings *model.EventSettings -var tbaClient *partner.TbaClient -var stemTvClient *partner.StemTvClient +const httpPort = 8080 // Main entry point for the application. func main() { rand.Seed(time.Now().UnixNano()) - initDb() - tbaClient = partner.NewTbaClient(eventSettings.TbaEventCode, eventSettings.TbaSecretId, eventSettings.TbaSecret) - stemTvClient = partner.NewStemTvClient(eventSettings.StemTvEventCode) - // Run the webserver and DS packet listener in goroutines and use the main one for the arena state machine. - go ServeWebInterface() - go ListenForDriverStations() - go ListenForDsUdpPackets() - go MonitorBandwidth() - mainArena.Setup() - mainArena.Run() -} - -// Opens the database and stores a handle to it in a global variable. -func initDb() { - var err error - db, err = model.OpenDatabase(".", eventDbPath) - checkErr(err) - eventSettings, err = db.GetEventSettings() - checkErr(err) -} - -// Logs and exits the application if the given error is not nil. -func checkErr(err error) { + arena, err := field.NewArena(eventDbPath) if err != nil { - log.Fatalln("Error: ", err) + log.Fatalln("Error during startup: ", err) } + + // Start the web server in a separate goroutine. + web := NewWeb(arena) + go web.ServeWebInterface(httpPort) + + // Run the arena state machine in the main thread. + arena.Run() } diff --git a/match_play.go b/match_play.go index e7e99fb..be5ff03 100644 --- a/match_play.go +++ b/match_play.go @@ -40,28 +40,28 @@ type MatchTimeMessage struct { var currentMatchType string // Shows the match play control interface. -func MatchPlayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - practiceMatches, err := buildMatchPlayList("practice") + practiceMatches, err := web.buildMatchPlayList("practice") if err != nil { handleWebErr(w, err) return } - qualificationMatches, err := buildMatchPlayList("qualification") + qualificationMatches, err := web.buildMatchPlayList("qualification") if err != nil { handleWebErr(w, err) return } - eliminationMatches, err := buildMatchPlayList("elimination") + eliminationMatches, err := web.buildMatchPlayList("elimination") if err != nil { handleWebErr(w, err) return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err = template.ParseFiles("templates/match_play.html", "templates/base.html") if err != nil { handleWebErr(w, err) @@ -72,8 +72,8 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) { if currentMatchType == "" { currentMatchType = "practice" } - allowSubstitution := mainArena.currentMatch.Type != "qualification" - matchResult, err := db.GetMatchResultForMatch(mainArena.currentMatch.Id) + allowSubstitution := web.arena.CurrentMatch.Type != "qualification" + matchResult, err := web.arena.Database.GetMatchResultForMatch(web.arena.CurrentMatch.Id) if err != nil { handleWebErr(w, err) return @@ -86,7 +86,7 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) { Match *model.Match AllowSubstitution bool IsReplay bool - }{eventSettings, matchesByType, currentMatchType, mainArena.currentMatch, allowSubstitution, isReplay} + }{web.arena.EventSettings, matchesByType, currentMatchType, web.arena.CurrentMatch, allowSubstitution, isReplay} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -95,8 +95,8 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) { } // Loads the given match onto the arena in preparation for playing it. -func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchPlayLoadHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -105,9 +105,9 @@ func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) { var match *model.Match var err error if matchId == 0 { - err = mainArena.LoadTestMatch() + err = web.arena.LoadTestMatch() } else { - match, err = db.GetMatchById(matchId) + match, err = web.arena.Database.GetMatchById(matchId) if err != nil { handleWebErr(w, err) return @@ -116,26 +116,26 @@ func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId)) return } - err = mainArena.LoadMatch(match) + err = web.arena.LoadMatch(match) } if err != nil { handleWebErr(w, err) return } - currentMatchType = mainArena.currentMatch.Type + currentMatchType = web.arena.CurrentMatch.Type http.Redirect(w, r, "/match_play", 302) } // Loads the results for the given match into the display buffer. -func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } vars := mux.Vars(r) matchId, _ := strconv.Atoi(vars["matchId"]) - match, err := db.GetMatchById(matchId) + match, err := web.arena.Database.GetMatchById(matchId) if err != nil { handleWebErr(w, err) return @@ -144,7 +144,7 @@ func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId)) return } - matchResult, err := db.GetMatchResultForMatch(match.Id) + matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) if err != nil { handleWebErr(w, err) return @@ -153,16 +153,16 @@ func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, fmt.Errorf("No result found for match ID %d.", matchId)) return } - mainArena.savedMatch = match - mainArena.savedMatchResult = matchResult - mainArena.scorePostedNotifier.Notify(nil) + web.arena.SavedMatch = match + web.arena.SavedMatchResult = matchResult + web.arena.ScorePostedNotifier.Notify(nil) http.Redirect(w, r, "/match_play", 302) } // The websocket endpoint for the match play client to send control commands and receive status updates. -func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -173,22 +173,22 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - matchTimeListener := mainArena.matchTimeNotifier.Listen() + matchTimeListener := web.arena.MatchTimeNotifier.Listen() defer close(matchTimeListener) - realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen() + realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen() defer close(realtimeScoreListener) - robotStatusListener := mainArena.robotStatusNotifier.Listen() + robotStatusListener := web.arena.RobotStatusNotifier.Listen() defer close(robotStatusListener) - audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen() + audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen() defer close(audienceDisplayListener) - scoringStatusListener := mainArena.scoringStatusNotifier.Listen() + scoringStatusListener := web.arena.ScoringStatusNotifier.Listen() defer close(scoringStatusListener) - allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen() + allianceStationDisplayListener := web.arena.AllianceStationDisplayNotifier.Listen() defer close(allianceStationDisplayListener) // Send the various notifications immediately upon connection. var data interface{} - err = websocket.Write("status", mainArena) + err = websocket.Write("status", web.arena.GetStatus()) if err != nil { log.Printf("Websocket error: %s", err) return @@ -198,7 +198,7 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Websocket error: %s", err) return } - data = MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)} + data = MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)} err = websocket.Write("matchTime", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -207,13 +207,13 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { data = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} err = websocket.Write("realtimeScore", data) if err != nil { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen) + err = websocket.Write("setAudienceDisplay", web.arena.AudienceDisplayScreen) if err != nil { log.Printf("Websocket error: %s", err) return @@ -222,14 +222,14 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { RefereeScoreReady bool RedScoreReady bool BlueScoreReady bool - }{mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted, - mainArena.redRealtimeScore.TeleopCommitted, mainArena.blueRealtimeScore.TeleopCommitted} + }{web.arena.RedRealtimeScore.FoulsCommitted && web.arena.BlueRealtimeScore.FoulsCommitted, + web.arena.RedRealtimeScore.TeleopCommitted, web.arena.BlueRealtimeScore.TeleopCommitted} err = websocket.Write("scoringStatus", data) if err != nil { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen) + err = websocket.Write("setAllianceStationDisplay", web.arena.AllianceStationDisplayScreen) if err != nil { log.Printf("Websocket error: %s", err) return @@ -246,7 +246,7 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { return } messageType = "matchTime" - message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} + message = MatchTimeMessage{web.arena.MatchState, matchTimeSec.(int)} case _, ok := <-realtimeScoreListener: if !ok { return @@ -255,19 +255,19 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { message = struct { RedScore int BlueScore int - }{mainArena.RedScoreSummary().Score, mainArena.BlueScoreSummary().Score} + }{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score} case _, ok := <-robotStatusListener: if !ok { return } messageType = "status" - message = mainArena + message = web.arena.GetStatus() case _, ok := <-audienceDisplayListener: if !ok { return } messageType = "setAudienceDisplay" - message = mainArena.audienceDisplayScreen + message = web.arena.AudienceDisplayScreen case _, ok := <-scoringStatusListener: if !ok { return @@ -277,14 +277,14 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { RefereeScoreReady bool RedScoreReady bool BlueScoreReady bool - }{mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted, - mainArena.redRealtimeScore.TeleopCommitted, mainArena.blueRealtimeScore.TeleopCommitted} + }{web.arena.RedRealtimeScore.FoulsCommitted && web.arena.BlueRealtimeScore.FoulsCommitted, + web.arena.RedRealtimeScore.TeleopCommitted, web.arena.BlueRealtimeScore.TeleopCommitted} case _, ok := <-allianceStationDisplayListener: if !ok { return } messageType = "setAllianceStationDisplay" - message = mainArena.allianceStationDisplayScreen + message = web.arena.AllianceStationDisplayScreen } err = websocket.Write(messageType, message) if err != nil { @@ -317,7 +317,7 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - err = mainArena.SubstituteTeam(args.Team, args.Position) + err = web.arena.SubstituteTeam(args.Team, args.Position) if err != nil { websocket.WriteError(err.Error()) continue @@ -328,11 +328,11 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) continue } - if _, ok := mainArena.AllianceStations[station]; !ok { + if _, ok := web.arena.AllianceStations[station]; !ok { websocket.WriteError(fmt.Sprintf("Invalid alliance station '%s'.", station)) continue } - mainArena.AllianceStations[station].Bypass = !mainArena.AllianceStations[station].Bypass + web.arena.AllianceStations[station].Bypass = !web.arena.AllianceStations[station].Bypass case "startMatch": args := struct { MuteMatchSounds bool @@ -342,30 +342,30 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - mainArena.muteMatchSounds = args.MuteMatchSounds - err = mainArena.StartMatch() + web.arena.MuteMatchSounds = args.MuteMatchSounds + err = web.arena.StartMatch() if err != nil { websocket.WriteError(err.Error()) continue } case "abortMatch": - err = mainArena.AbortMatch() + err = web.arena.AbortMatch() if err != nil { websocket.WriteError(err.Error()) continue } case "commitResults": - err = CommitCurrentMatchScore() + err = web.commitCurrentMatchScore() if err != nil { websocket.WriteError(err.Error()) continue } - err = mainArena.ResetMatch() + err = web.arena.ResetMatch() if err != nil { websocket.WriteError(err.Error()) continue } - err = mainArena.LoadNextMatch() + err = web.arena.LoadNextMatch() if err != nil { websocket.WriteError(err.Error()) continue @@ -377,12 +377,12 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } continue // Skip sending the status update, as the client is about to terminate and reload. case "discardResults": - err = mainArena.ResetMatch() + err = web.arena.ResetMatch() if err != nil { websocket.WriteError(err.Error()) continue } - err = mainArena.LoadNextMatch() + err = web.arena.LoadNextMatch() if err != nil { websocket.WriteError(err.Error()) continue @@ -399,8 +399,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) continue } - mainArena.audienceDisplayScreen = screen - mainArena.audienceDisplayNotifier.Notify(nil) + web.arena.AudienceDisplayScreen = screen + web.arena.AudienceDisplayNotifier.Notify(nil) continue case "setAllianceStationDisplay": screen, ok := data.(string) @@ -408,8 +408,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) continue } - mainArena.allianceStationDisplayScreen = screen - mainArena.allianceStationDisplayNotifier.Notify(nil) + web.arena.AllianceStationDisplayScreen = screen + web.arena.AllianceStationDisplayNotifier.Notify(nil) continue default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) @@ -417,7 +417,7 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } // Send out the status again after handling the command, as it most likely changed as a result. - err = websocket.Write("status", mainArena) + err = websocket.Write("status", web.arena) if err != nil { log.Printf("Websocket error: %s", err) return @@ -426,7 +426,7 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } // Saves the given match and result to the database, supplanting any previous result for the match. -func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadToShowBuffer bool) error { +func (web *Web) commitMatchScore(match *model.Match, matchResult *model.MatchResult, loadToShowBuffer bool) error { if match.Type == "elimination" { // Adjust the score if necessary for an elimination DQ. matchResult.CorrectEliminationScore() @@ -434,9 +434,9 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo if loadToShowBuffer { // Store the result in the buffer to be shown in the audience display. - mainArena.savedMatch = match - mainArena.savedMatchResult = matchResult - mainArena.scorePostedNotifier.Notify(nil) + web.arena.SavedMatch = match + web.arena.SavedMatchResult = matchResult + web.arena.ScorePostedNotifier.Notify(nil) } if match.Type == "test" { @@ -446,7 +446,7 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo if matchResult.PlayNumber == 0 { // Determine the play number for this new match result. - prevMatchResult, err := db.GetMatchResultForMatch(match.Id) + prevMatchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) if err != nil { return err } @@ -457,13 +457,13 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo } // Save the match result record to the database. - err = db.CreateMatchResult(matchResult) + err = web.arena.Database.CreateMatchResult(matchResult) if err != nil { return err } } else { // We are updating a match result record that already exists. - err := db.SaveMatchResult(matchResult) + err := web.arena.Database.SaveMatchResult(matchResult) if err != nil { return err } @@ -480,19 +480,19 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo } else { match.Winner = "T" } - err := db.SaveMatch(match) + err := web.arena.Database.SaveMatch(match) if err != nil { return err } if match.Type != "practice" { // Regenerate the residual yellow cards that teams may carry. - tournament.CalculateTeamCards(db, match.Type) + tournament.CalculateTeamCards(web.arena.Database, match.Type) } if match.Type == "qualification" { // Recalculate all the rankings. - err = tournament.CalculateRankings(db) + err = tournament.CalculateRankings(web.arena.Database) if err != nil { return err } @@ -500,21 +500,21 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo if match.Type == "elimination" { // Generate any subsequent elimination matches. - _, err = tournament.UpdateEliminationSchedule(db, time.Now().Add(time.Second*tournament.ElimMatchSpacingSec)) + _, err = tournament.UpdateEliminationSchedule(web.arena.Database, time.Now().Add(time.Second*tournament.ElimMatchSpacingSec)) if err != nil { return err } } - if eventSettings.TbaPublishingEnabled && match.Type != "practice" { + if web.arena.EventSettings.TbaPublishingEnabled && match.Type != "practice" { // Publish asynchronously to The Blue Alliance. go func() { - err = tbaClient.PublishMatches(db) + err = web.arena.TbaClient.PublishMatches(web.arena.Database) if err != nil { log.Printf("Failed to publish matches: %s", err.Error()) } if match.Type == "qualification" { - err = tbaClient.PublishRankings(db) + err = web.arena.TbaClient.PublishRankings(web.arena.Database) if err != nil { log.Printf("Failed to publish rankings: %s", err.Error()) } @@ -522,10 +522,10 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo }() } - if eventSettings.StemTvPublishingEnabled && match.Type != "practice" { + if web.arena.EventSettings.StemTvPublishingEnabled && match.Type != "practice" { // Publish asynchronously to STEMtv. go func() { - err = stemTvClient.PublishMatchVideoSplit(match, time.Now()) + err = web.arena.StemTvClient.PublishMatchVideoSplit(match, time.Now()) if err != nil { log.Printf("Failed to publish match video split to STEMtv: %s", err.Error()) } @@ -533,7 +533,7 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo } // Back up the database, but don't error out if it fails. - err = db.Backup(eventSettings.Name, fmt.Sprintf("post_%s_match_%s", match.Type, match.DisplayName)) + err = web.arena.Database.Backup(web.arena.EventSettings.Name, fmt.Sprintf("post_%s_match_%s", match.Type, match.DisplayName)) if err != nil { log.Println(err) } @@ -541,15 +541,15 @@ func CommitMatchScore(match *model.Match, matchResult *model.MatchResult, loadTo return nil } -func GetCurrentMatchResult() *model.MatchResult { - return &model.MatchResult{MatchId: mainArena.currentMatch.Id, MatchType: mainArena.currentMatch.Type, - RedScore: mainArena.redRealtimeScore.CurrentScore, BlueScore: mainArena.blueRealtimeScore.CurrentScore, - RedCards: mainArena.redRealtimeScore.Cards, BlueCards: mainArena.blueRealtimeScore.Cards} +func (web *Web) getCurrentMatchResult() *model.MatchResult { + return &model.MatchResult{MatchId: web.arena.CurrentMatch.Id, MatchType: web.arena.CurrentMatch.Type, + RedScore: web.arena.RedRealtimeScore.CurrentScore, BlueScore: web.arena.BlueRealtimeScore.CurrentScore, + RedCards: web.arena.RedRealtimeScore.Cards, BlueCards: web.arena.BlueRealtimeScore.Cards} } // Saves the realtime result as the final score for the match currently loaded into the arena. -func CommitCurrentMatchScore() error { - return CommitMatchScore(mainArena.currentMatch, GetCurrentMatchResult(), true) +func (web *Web) commitCurrentMatchScore() error { + return web.commitMatchScore(web.arena.CurrentMatch, web.getCurrentMatchResult(), true) } // Helper function to implement the required interface for Sort. @@ -568,8 +568,8 @@ func (list MatchPlayList) Swap(i, j int) { } // Constructs the list of matches to display on the side of the match play interface. -func buildMatchPlayList(matchType string) (MatchPlayList, error) { - matches, err := db.GetMatchesByType(matchType) +func (web *Web) buildMatchPlayList(matchType string) (MatchPlayList, error) { + matches, err := web.arena.Database.GetMatchesByType(matchType) if err != nil { return MatchPlayList{}, err } @@ -596,7 +596,7 @@ func buildMatchPlayList(matchType string) (MatchPlayList, error) { default: matchPlayList[i].ColorClass = "" } - if mainArena.currentMatch != nil && matchPlayList[i].Id == mainArena.currentMatch.Id { + if web.arena.CurrentMatch != nil && matchPlayList[i].Id == web.arena.CurrentMatch.Id { matchPlayList[i].ColorClass = "success" } } diff --git a/match_play_test.go b/match_play_test.go index 61c3de5..f46ad91 100644 --- a/match_play_test.go +++ b/match_play_test.go @@ -6,6 +6,7 @@ package main import ( "bytes" "fmt" + "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/tournament" @@ -19,21 +20,21 @@ import ( ) func TestMatchPlay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match1 := model.Match{Type: "practice", DisplayName: "1", Status: "complete", Winner: "R"} match2 := model.Match{Type: "practice", DisplayName: "2"} match3 := model.Match{Type: "qualification", DisplayName: "1", Status: "complete", Winner: "B"} match4 := model.Match{Type: "elimination", DisplayName: "SF1-1", Status: "complete", Winner: "T"} match5 := model.Match{Type: "elimination", DisplayName: "SF1-2"} - db.CreateMatch(&match1) - db.CreateMatch(&match2) - db.CreateMatch(&match3) - db.CreateMatch(&match4) - db.CreateMatch(&match5) + web.arena.Database.CreateMatch(&match1) + web.arena.Database.CreateMatch(&match2) + web.arena.Database.CreateMatch(&match3) + web.arena.Database.CreateMatch(&match4) + web.arena.Database.CreateMatch(&match5) // Check that all matches are listed on the page. - recorder := getHttpResponse("/match_play") + recorder := web.getHttpResponse("/match_play") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "P1") assert.Contains(t, recorder.Body.String(), "P2") @@ -43,18 +44,18 @@ func TestMatchPlay(t *testing.T) { } func TestMatchPlayLoad(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateTeam(&model.Team{Id: 101}) - db.CreateTeam(&model.Team{Id: 102}) - db.CreateTeam(&model.Team{Id: 103}) - db.CreateTeam(&model.Team{Id: 104}) - db.CreateTeam(&model.Team{Id: 105}) - db.CreateTeam(&model.Team{Id: 106}) + web.arena.Database.CreateTeam(&model.Team{Id: 101}) + web.arena.Database.CreateTeam(&model.Team{Id: 102}) + web.arena.Database.CreateTeam(&model.Team{Id: 103}) + web.arena.Database.CreateTeam(&model.Team{Id: 104}) + web.arena.Database.CreateTeam(&model.Team{Id: 105}) + web.arena.Database.CreateTeam(&model.Team{Id: 106}) match := model.Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - db.CreateMatch(&match) - recorder := getHttpResponse("/match_play") + web.arena.Database.CreateMatch(&match) + recorder := web.getHttpResponse("/match_play") assert.Equal(t, 200, recorder.Code) assert.NotContains(t, recorder.Body.String(), "101") assert.NotContains(t, recorder.Body.String(), "102") @@ -64,9 +65,9 @@ func TestMatchPlayLoad(t *testing.T) { assert.NotContains(t, recorder.Body.String(), "106") // Load the match and check for the team numbers again. - recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/load", match.Id)) + recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/load", match.Id)) assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/match_play") + recorder = web.getHttpResponse("/match_play") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "101") assert.Contains(t, recorder.Body.String(), "102") @@ -76,9 +77,9 @@ func TestMatchPlayLoad(t *testing.T) { assert.Contains(t, recorder.Body.String(), "106") // Load a test match. - recorder = getHttpResponse("/match_play/0/load") + recorder = web.getHttpResponse("/match_play/0/load") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/match_play") + recorder = web.getHttpResponse("/match_play") assert.Equal(t, 200, recorder.Code) assert.NotContains(t, recorder.Body.String(), "101") assert.NotContains(t, recorder.Body.String(), "102") @@ -89,81 +90,81 @@ func TestMatchPlayLoad(t *testing.T) { } func TestMatchPlayShowResult(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/match_play/1/show_result") + recorder := web.getHttpResponse("/match_play/1/show_result") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "Invalid match") match := model.Match{Type: "qualification", DisplayName: "1", Status: "complete"} - db.CreateMatch(&match) - recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) + web.arena.Database.CreateMatch(&match) + recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "No result found") - db.CreateMatchResult(&model.MatchResult{MatchId: match.Id}) - recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) + web.arena.Database.CreateMatchResult(&model.MatchResult{MatchId: match.Id}) + recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) assert.Equal(t, 302, recorder.Code) - assert.Equal(t, match.Id, mainArena.savedMatch.Id) - assert.Equal(t, match.Id, mainArena.savedMatchResult.MatchId) + assert.Equal(t, match.Id, web.arena.SavedMatch.Id) + assert.Equal(t, match.Id, web.arena.SavedMatchResult.MatchId) } func TestMatchPlayErrors(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Load an invalid match. - recorder := getHttpResponse("/match_play/1114/load") + recorder := web.getHttpResponse("/match_play/1114/load") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "Invalid match") } func TestCommitMatch(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Committing test match should do nothing. match := &model.Match{Id: 0, Type: "test", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - err := CommitMatchScore(match, &model.MatchResult{MatchId: match.Id}, false) + err := web.commitMatchScore(match, &model.MatchResult{MatchId: match.Id}, false) assert.Nil(t, err) - matchResult, err := db.GetMatchResultForMatch(match.Id) + matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) assert.Nil(t, err) assert.Nil(t, matchResult) // Committing the same match more than once should create a second match result record. match.Id = 1 match.Type = "qualification" - db.CreateMatch(match) + web.arena.Database.CreateMatch(match) matchResult = model.NewMatchResult() matchResult.MatchId = match.Id matchResult.BlueScore = &game.Score{AutoMobility: 2} - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) assert.Equal(t, 1, matchResult.PlayNumber) - match, _ = db.GetMatchById(1) + match, _ = web.arena.Database.GetMatchById(1) assert.Equal(t, "B", match.Winner) matchResult = model.NewMatchResult() matchResult.MatchId = match.Id matchResult.RedScore = &game.Score{AutoMobility: 1} - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) assert.Equal(t, 2, matchResult.PlayNumber) - match, _ = db.GetMatchById(1) + match, _ = web.arena.Database.GetMatchById(1) assert.Equal(t, "R", match.Winner) matchResult = model.NewMatchResult() matchResult.MatchId = match.Id - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) assert.Equal(t, 3, matchResult.PlayNumber) - match, _ = db.GetMatchById(1) + match, _ = web.arena.Database.GetMatchById(1) assert.Equal(t, "T", match.Winner) // Verify TBA and STEMtv publishing by checking the log for the expected failure messages. - tbaClient.BaseUrl = "fakeUrl" - stemTvClient.BaseUrl = "fakeUrl" - eventSettings.TbaPublishingEnabled = true - eventSettings.StemTvPublishingEnabled = true + web.arena.TbaClient.BaseUrl = "fakeUrl" + web.arena.StemTvClient.BaseUrl = "fakeUrl" + web.arena.EventSettings.TbaPublishingEnabled = true + web.arena.EventSettings.StemTvPublishingEnabled = true var writer bytes.Buffer log.SetOutput(&writer) - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) time.Sleep(time.Millisecond * 10) // Allow some time for the asynchronous publishing to happen. assert.Contains(t, writer.String(), "Failed to publish matches") @@ -172,73 +173,73 @@ func TestCommitMatch(t *testing.T) { } func TestCommitEliminationTie(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match := &model.Match{Id: 0, Type: "qualification", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6} - db.CreateMatch(match) + web.arena.Database.CreateMatch(match) matchResult := &model.MatchResult{MatchId: match.Id, RedScore: &game.Score{FuelHigh: 15, Fouls: []game.Foul{{}}}, BlueScore: &game.Score{}} - err := CommitMatchScore(match, matchResult, false) + err := web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) - match, _ = db.GetMatchById(1) + match, _ = web.arena.Database.GetMatchById(1) assert.Equal(t, "T", match.Winner) match.Type = "elimination" - db.SaveMatch(match) - CommitMatchScore(match, matchResult, false) - match, _ = db.GetMatchById(1) + web.arena.Database.SaveMatch(match) + web.commitMatchScore(match, matchResult, false) + match, _ = web.arena.Database.GetMatchById(1) assert.Equal(t, "T", match.Winner) // No elimination tiebreakers. } func TestCommitCards(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Check that a yellow card sticks with a team. team := &model.Team{Id: 5} - db.CreateTeam(team) + web.arena.Database.CreateTeam(team) match := &model.Match{Id: 0, Type: "qualification", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6} - db.CreateMatch(match) + web.arena.Database.CreateMatch(match) matchResult := model.NewMatchResult() matchResult.MatchId = match.Id matchResult.BlueCards = map[string]string{"5": "yellow"} - err := CommitMatchScore(match, matchResult, false) + err := web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) - team, _ = db.GetTeamById(5) + team, _ = web.arena.Database.GetTeamById(5) assert.True(t, team.YellowCard) // Check that editing a match result removes a yellow card from a team. matchResult = model.NewMatchResult() matchResult.MatchId = match.Id - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) - team, _ = db.GetTeamById(5) + team, _ = web.arena.Database.GetTeamById(5) assert.False(t, team.YellowCard) // Check that a red card causes a yellow card to stick with a team. matchResult = model.NewMatchResult() matchResult.MatchId = match.Id matchResult.BlueCards = map[string]string{"5": "red"} - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) - team, _ = db.GetTeamById(5) + team, _ = web.arena.Database.GetTeamById(5) assert.True(t, team.YellowCard) // Check that a red card in eliminations zeroes out the score. - tournament.CreateTestAlliances(db, 2) + tournament.CreateTestAlliances(web.arena.Database, 2) match.Type = "elimination" - db.SaveMatch(match) + web.arena.Database.SaveMatch(match) matchResult = model.BuildTestMatchResult(match.Id, 10) matchResult.MatchType = match.Type matchResult.RedCards = map[string]string{"1": "red"} - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) assert.Nil(t, err) assert.Equal(t, 0, matchResult.RedScoreSummary().Score) assert.Equal(t, 533, matchResult.BlueScoreSummary().Score) } func TestMatchPlayWebsocketCommands(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil) assert.Nil(t, err) @@ -265,35 +266,35 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station") ws.Write("substituteTeam", map[string]interface{}{"team": 254, "position": "B1"}) readWebsocketType(t, ws, "status") - assert.Equal(t, 254, mainArena.currentMatch.Blue1) + assert.Equal(t, 254, web.arena.CurrentMatch.Blue1) ws.Write("substituteTeam", map[string]interface{}{"team": 0, "position": "B1"}) readWebsocketType(t, ws, "status") - assert.Equal(t, 0, mainArena.currentMatch.Blue1) + assert.Equal(t, 0, web.arena.CurrentMatch.Blue1) ws.Write("toggleBypass", nil) assert.Contains(t, readWebsocketError(t, ws), "Failed to parse") ws.Write("toggleBypass", "R4") assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station") ws.Write("toggleBypass", "R3") readWebsocketType(t, ws, "status") - assert.Equal(t, true, mainArena.AllianceStations["R3"].Bypass) + assert.Equal(t, true, web.arena.AllianceStations["R3"].Bypass) ws.Write("toggleBypass", "R3") readWebsocketType(t, ws, "status") - assert.Equal(t, false, mainArena.AllianceStations["R3"].Bypass) + assert.Equal(t, false, web.arena.AllianceStations["R3"].Bypass) // Go through match flow. ws.Write("abortMatch", nil) assert.Contains(t, readWebsocketError(t, ws), "Cannot abort match") ws.Write("startMatch", nil) assert.Contains(t, readWebsocketError(t, ws), "Cannot start match") - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true + web.arena.AllianceStations["R1"].Bypass = true + web.arena.AllianceStations["R2"].Bypass = true + web.arena.AllianceStations["R3"].Bypass = true + web.arena.AllianceStations["B1"].Bypass = true + web.arena.AllianceStations["B2"].Bypass = true + web.arena.AllianceStations["B3"].Bypass = true ws.Write("startMatch", nil) readWebsocketType(t, ws, "status") - assert.Equal(t, startMatch, mainArena.MatchState) + assert.Equal(t, field.StartMatch, web.arena.MatchState) ws.Write("commitResults", nil) assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match") ws.Write("discardResults", nil) @@ -301,33 +302,33 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { ws.Write("abortMatch", nil) readWebsocketType(t, ws, "status") readWebsocketType(t, ws, "setAudienceDisplay") - assert.Equal(t, postMatch, mainArena.MatchState) - mainArena.redRealtimeScore.CurrentScore.AutoMobility = 1 - mainArena.blueRealtimeScore.CurrentScore.AutoFuelLow = 2 + assert.Equal(t, field.PostMatch, web.arena.MatchState) + web.arena.RedRealtimeScore.CurrentScore.AutoMobility = 1 + web.arena.BlueRealtimeScore.CurrentScore.AutoFuelLow = 2 ws.Write("commitResults", nil) readWebsocketMultiple(t, ws, 3) // reload, realtimeScore, setAllianceStationDisplay - assert.Equal(t, 1, mainArena.savedMatchResult.RedScore.AutoMobility) - assert.Equal(t, 2, mainArena.savedMatchResult.BlueScore.AutoFuelLow) - assert.Equal(t, preMatch, mainArena.MatchState) + assert.Equal(t, 1, web.arena.SavedMatchResult.RedScore.AutoMobility) + assert.Equal(t, 2, web.arena.SavedMatchResult.BlueScore.AutoFuelLow) + assert.Equal(t, field.PreMatch, web.arena.MatchState) ws.Write("discardResults", nil) readWebsocketMultiple(t, ws, 3) // reload, realtimeScore, setAllianceStationDisplay - assert.Equal(t, preMatch, mainArena.MatchState) + assert.Equal(t, field.PreMatch, web.arena.MatchState) // Test changing the displays. ws.Write("setAudienceDisplay", "logo") readWebsocketType(t, ws, "setAudienceDisplay") - assert.Equal(t, "logo", mainArena.audienceDisplayScreen) + assert.Equal(t, "logo", web.arena.AudienceDisplayScreen) ws.Write("setAllianceStationDisplay", "logo") readWebsocketType(t, ws, "setAllianceStationDisplay") - assert.Equal(t, "logo", mainArena.allianceStationDisplayScreen) + assert.Equal(t, "logo", web.arena.AllianceStationDisplayScreen) } func TestMatchPlayWebsocketNotifications(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateTeam(&model.Team{Id: 254}) + web.arena.Database.CreateTeam(&model.Team{Id: 254}) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil) assert.Nil(t, err) @@ -342,14 +343,14 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) { readWebsocketType(t, ws, "setAudienceDisplay") readWebsocketType(t, ws, "scoringStatus") - mainArena.AllianceStations["R1"].Bypass = true - mainArena.AllianceStations["R2"].Bypass = true - mainArena.AllianceStations["R3"].Bypass = true - mainArena.AllianceStations["B1"].Bypass = true - mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].Bypass = true - mainArena.StartMatch() - mainArena.Update() + web.arena.AllianceStations["R1"].Bypass = true + web.arena.AllianceStations["R2"].Bypass = true + web.arena.AllianceStations["R3"].Bypass = true + web.arena.AllianceStations["B1"].Bypass = true + web.arena.AllianceStations["B2"].Bypass = true + web.arena.AllianceStations["B3"].Bypass = true + web.arena.StartMatch() + web.arena.Update() messages := readWebsocketMultiple(t, ws, 4) statusReceived, matchTime := getStatusMatchTime(t, messages) assert.Equal(t, true, statusReceived) @@ -359,18 +360,18 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) { assert.True(t, ok) _, ok = messages["setAllianceStationDisplay"] assert.True(t, ok) - mainArena.scoringStatusNotifier.Notify(nil) + web.arena.ScoringStatusNotifier.Notify(nil) readWebsocketType(t, ws, "scoringStatus") // Should get a tick notification when an integer second threshold is crossed. - mainArena.matchStartTime = time.Now().Add(-time.Second + 10*time.Millisecond) // Not crossed yet - mainArena.Update() - mainArena.matchStartTime = time.Now().Add(-time.Second - 10*time.Millisecond) // Crossed - mainArena.Update() - mainArena.matchStartTime = time.Now().Add(-2*time.Second + 10*time.Millisecond) // Not crossed yet - mainArena.Update() - mainArena.matchStartTime = time.Now().Add(-2*time.Second - 10*time.Millisecond) // Crossed - mainArena.Update() + web.arena.MatchStartTime = time.Now().Add(-time.Second + 10*time.Millisecond) // Not crossed yet + web.arena.Update() + web.arena.MatchStartTime = time.Now().Add(-time.Second - 10*time.Millisecond) // Crossed + web.arena.Update() + web.arena.MatchStartTime = time.Now().Add(-2*time.Second + 10*time.Millisecond) // Not crossed yet + web.arena.Update() + web.arena.MatchStartTime = time.Now().Add(-2*time.Second - 10*time.Millisecond) // Crossed + web.arena.Update() err = mapstructure.Decode(readWebsocketType(t, ws, "matchTime"), &matchTime) assert.Nil(t, err) assert.Equal(t, 2, matchTime.MatchState) @@ -381,8 +382,8 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) { assert.Equal(t, 2, matchTime.MatchTimeSec) // Check across a match state boundary. - mainArena.matchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec) * time.Second) - mainArena.Update() + web.arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.AutoDurationSec) * time.Second) + web.arena.Update() statusReceived, matchTime = readWebsocketStatusMatchTime(t, ws) assert.Equal(t, true, statusReceived) assert.Equal(t, 3, matchTime.MatchState) diff --git a/match_review.go b/match_review.go index a7ed9aa..1b50f2d 100644 --- a/match_review.go +++ b/match_review.go @@ -26,22 +26,22 @@ type MatchReviewListItem struct { } // Shows the match review interface. -func MatchReviewHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) matchReviewHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - practiceMatches, err := buildMatchReviewList("practice") + practiceMatches, err := web.buildMatchReviewList("practice") if err != nil { handleWebErr(w, err) return } - qualificationMatches, err := buildMatchReviewList("qualification") + qualificationMatches, err := web.buildMatchReviewList("qualification") if err != nil { handleWebErr(w, err) return } - eliminationMatches, err := buildMatchReviewList("elimination") + eliminationMatches, err := web.buildMatchReviewList("elimination") if err != nil { handleWebErr(w, err) return @@ -61,7 +61,7 @@ func MatchReviewHandler(w http.ResponseWriter, r *http.Request) { *model.EventSettings MatchesByType map[string][]MatchReviewListItem CurrentMatchType string - }{eventSettings, matchesByType, currentMatchType} + }{web.arena.EventSettings, matchesByType, currentMatchType} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -70,12 +70,12 @@ func MatchReviewHandler(w http.ResponseWriter, r *http.Request) { } // Shows the page to edit the results for a match. -func MatchReviewEditGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchReviewEditGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - match, matchResult, _, err := getMatchResultFromRequest(r) + match, matchResult, _, err := web.getMatchResultFromRequest(r) if err != nil { handleWebErr(w, err) return @@ -95,7 +95,7 @@ func MatchReviewEditGetHandler(w http.ResponseWriter, r *http.Request) { *model.EventSettings Match *model.Match MatchResultJson *model.MatchResultDb - }{eventSettings, match, matchResultJson} + }{web.arena.EventSettings, match, matchResultJson} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -104,12 +104,12 @@ func MatchReviewEditGetHandler(w http.ResponseWriter, r *http.Request) { } // Updates the results for a match. -func MatchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) matchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - match, matchResult, isCurrent, err := getMatchResultFromRequest(r) + match, matchResult, isCurrent, err := web.getMatchResultFromRequest(r) if err != nil { handleWebErr(w, err) return @@ -130,14 +130,14 @@ func MatchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) { if isCurrent { // If editing the current match, just save it back to memory. - mainArena.redRealtimeScore.CurrentScore = matchResult.RedScore - mainArena.blueRealtimeScore.CurrentScore = matchResult.BlueScore - mainArena.redRealtimeScore.Cards = matchResult.RedCards - mainArena.blueRealtimeScore.Cards = matchResult.BlueCards + web.arena.RedRealtimeScore.CurrentScore = matchResult.RedScore + web.arena.BlueRealtimeScore.CurrentScore = matchResult.BlueScore + web.arena.RedRealtimeScore.Cards = matchResult.RedCards + web.arena.BlueRealtimeScore.Cards = matchResult.BlueCards http.Redirect(w, r, "/match_play", 302) } else { - err = CommitMatchScore(match, matchResult, false) + err = web.commitMatchScore(match, matchResult, false) if err != nil { handleWebErr(w, err) return @@ -148,23 +148,23 @@ func MatchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) { } // Load the match result for the match referenced in the HTTP query string. -func getMatchResultFromRequest(r *http.Request) (*model.Match, *model.MatchResult, bool, error) { +func (web *Web) getMatchResultFromRequest(r *http.Request) (*model.Match, *model.MatchResult, bool, error) { vars := mux.Vars(r) // If editing the current match, get it from memory instead of the DB. if vars["matchId"] == "current" { - return mainArena.currentMatch, GetCurrentMatchResult(), true, nil + return web.arena.CurrentMatch, web.getCurrentMatchResult(), true, nil } matchId, _ := strconv.Atoi(vars["matchId"]) - match, err := db.GetMatchById(matchId) + match, err := web.arena.Database.GetMatchById(matchId) if err != nil { return nil, nil, false, err } if match == nil { return nil, nil, false, fmt.Errorf("Error: No such match: %d", matchId) } - matchResult, err := db.GetMatchResultForMatch(matchId) + matchResult, err := web.arena.Database.GetMatchResultForMatch(matchId) if err != nil { return nil, nil, false, err } @@ -178,8 +178,8 @@ func getMatchResultFromRequest(r *http.Request) (*model.Match, *model.MatchResul } // Constructs the list of matches to display in the match review interface. -func buildMatchReviewList(matchType string) ([]MatchReviewListItem, error) { - matches, err := db.GetMatchesByType(matchType) +func (web *Web) buildMatchReviewList(matchType string) ([]MatchReviewListItem, error) { + matches, err := web.arena.Database.GetMatchesByType(matchType) if err != nil { return []MatchReviewListItem{}, err } @@ -197,7 +197,7 @@ func buildMatchReviewList(matchType string) ([]MatchReviewListItem, error) { matchReviewList[i].Time = match.Time.Local().Format("Mon 1/02 03:04 PM") matchReviewList[i].RedTeams = []int{match.Red1, match.Red2, match.Red3} matchReviewList[i].BlueTeams = []int{match.Blue1, match.Blue2, match.Blue3} - matchResult, err := db.GetMatchResultForMatch(match.Id) + matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) if err != nil { return []MatchReviewListItem{}, err } diff --git a/match_review_test.go b/match_review_test.go index a343e3a..f657696 100644 --- a/match_review_test.go +++ b/match_review_test.go @@ -12,21 +12,21 @@ import ( ) func TestMatchReview(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match1 := model.Match{Type: "practice", DisplayName: "1", Status: "complete", Winner: "R"} match2 := model.Match{Type: "practice", DisplayName: "2"} match3 := model.Match{Type: "qualification", DisplayName: "1", Status: "complete", Winner: "B"} match4 := model.Match{Type: "elimination", DisplayName: "SF1-1", Status: "complete", Winner: "T"} match5 := model.Match{Type: "elimination", DisplayName: "SF1-2"} - db.CreateMatch(&match1) - db.CreateMatch(&match2) - db.CreateMatch(&match3) - db.CreateMatch(&match4) - db.CreateMatch(&match5) + web.arena.Database.CreateMatch(&match1) + web.arena.Database.CreateMatch(&match2) + web.arena.Database.CreateMatch(&match3) + web.arena.Database.CreateMatch(&match4) + web.arena.Database.CreateMatch(&match5) // Check that all matches are listed on the page. - recorder := getHttpResponse("/match_review") + recorder := web.getHttpResponse("/match_review") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "P1") assert.Contains(t, recorder.Body.String(), "P2") @@ -36,39 +36,39 @@ func TestMatchReview(t *testing.T) { } func TestMatchReviewEditExistingResult(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match := model.Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 1001, Red2: 1002, Red3: 1003, Blue1: 1004, Blue2: 1005, Blue3: 1006} - db.CreateMatch(&match) + web.arena.Database.CreateMatch(&match) matchResult := model.BuildTestMatchResult(match.Id, 1) matchResult.MatchType = match.Type - db.CreateMatchResult(matchResult) - tournament.CreateTestAlliances(db, 2) + web.arena.Database.CreateMatchResult(matchResult) + tournament.CreateTestAlliances(web.arena.Database, 2) - recorder := getHttpResponse("/match_review") + recorder := web.getHttpResponse("/match_review") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") assert.Contains(t, recorder.Body.String(), "210") // The red score assert.Contains(t, recorder.Body.String(), "533") // The blue score // Check response for non-existent match. - recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", 12345)) + recorder = web.getHttpResponse(fmt.Sprintf("/match_review/%d/edit", 12345)) assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "No such match") - recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id)) + recorder = web.getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id)) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") // Update the score to something else. postBody := "redScoreJson={\"AutoMobility\":3}&blueScoreJson={\"Rotors\":3," + "\"Fouls\":[{\"TeamId\":973,\"Rule\":\"G22\"}]}&redCardsJson={\"105\":\"yellow\"}&blueCardsJson={}" - recorder = postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody) + recorder = web.postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody) assert.Equal(t, 302, recorder.Code) // Check for the updated scores back on the match list page. - recorder = getHttpResponse("/match_review") + recorder = web.getHttpResponse("/match_review") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") assert.Contains(t, recorder.Body.String(), "20") // The red score @@ -76,31 +76,31 @@ func TestMatchReviewEditExistingResult(t *testing.T) { } func TestMatchReviewCreateNewResult(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match := model.Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 1001, Red2: 1002, Red3: 1003, Blue1: 1004, Blue2: 1005, Blue3: 1006} - db.CreateMatch(&match) - tournament.CreateTestAlliances(db, 2) + web.arena.Database.CreateMatch(&match) + tournament.CreateTestAlliances(web.arena.Database, 2) - recorder := getHttpResponse("/match_review") + recorder := web.getHttpResponse("/match_review") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") assert.NotContains(t, recorder.Body.String(), "210") // The red score assert.NotContains(t, recorder.Body.String(), "533") // The blue score - recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id)) + recorder = web.getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id)) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") // Update the score to something else. postBody := "redScoreJson={\"AutoRotors\":1}&blueScoreJson={\"FuelHigh\":30," + "\"Fouls\":[{\"TeamId\":973,\"Rule\":\"G22\"}]}&redCardsJson={\"105\":\"yellow\"}&blueCardsJson={}" - recorder = postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody) + recorder = web.postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody) assert.Equal(t, 302, recorder.Code) // Check for the updated scores back on the match list page. - recorder = getHttpResponse("/match_review") + recorder = web.getHttpResponse("/match_review") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "QF4-3") assert.Contains(t, recorder.Body.String(), "65") // The red score diff --git a/model/database.go b/model/database.go index 996a213..ff5606c 100644 --- a/model/database.go +++ b/model/database.go @@ -22,8 +22,9 @@ import ( const backupsDir = "db/backups" const migrationsDir = "db/migrations" +var BaseDir = "." // Mutable for testing + type Database struct { - baseDir string filename string db *sql.DB eventSettingsMap *modl.DbMap @@ -38,10 +39,10 @@ type Database struct { // Opens the SQLite database at the given path, creating it if it doesn't exist, and runs any pending // migrations. -func OpenDatabase(baseDir, filename string) (*Database, error) { +func OpenDatabase(filename string) (*Database, error) { // Find and run the migrations using goose. This also auto-creates the DB. - database := Database{baseDir: baseDir, filename: filename} - migrationsPath := filepath.Join(baseDir, migrationsDir) + database := Database{filename: filename} + migrationsPath := filepath.Join(BaseDir, migrationsDir) dbDriver := goose.DBDriver{"sqlite3", database.GetPath(), "github.com/mattn/go-sqlite3", &goose.Sqlite3Dialect{}} dbConf := goose.DBConf{MigrationsDir: migrationsPath, Env: "prod", Driver: dbDriver} target, err := goose.GetMostRecentDBVersion(migrationsPath) @@ -69,7 +70,7 @@ func (database *Database) Close() { // Creates a copy of the current database and saves it to the backups directory. func (database *Database) Backup(eventName, reason string) error { - backupsPath := filepath.Join(database.baseDir, backupsDir) + backupsPath := filepath.Join(BaseDir, backupsDir) err := os.MkdirAll(backupsPath, 0755) if err != nil { return err @@ -93,7 +94,7 @@ func (database *Database) Backup(eventName, reason string) error { } func (database *Database) GetPath() string { - return filepath.Join(database.baseDir, database.filename) + return filepath.Join(BaseDir, database.filename) } // Sets up table-object associations. diff --git a/model/database_test.go b/model/database_test.go index 66dbee2..7a6e065 100644 --- a/model/database_test.go +++ b/model/database_test.go @@ -9,10 +9,10 @@ import ( ) func TestOpenUnreachableDatabase(t *testing.T) { - _, err := OpenDatabase("..", "nonexistentdir/test.db") + _, err := OpenDatabase("nonexistentdir/test.db") assert.NotNil(t, err) } func setupTestDb(t *testing.T) *Database { - return SetupTestDb(t, "model", "..") + return SetupTestDb(t, "model") } diff --git a/model/test_helpers.go b/model/test_helpers.go index 6647ed0..ce4ca60 100644 --- a/model/test_helpers.go +++ b/model/test_helpers.go @@ -14,12 +14,11 @@ import ( "testing" ) -const testDbPath = "%s_test.db" - -func SetupTestDb(t *testing.T, uniqueName, baseDir string) *Database { - dbPath := fmt.Sprintf(testDbPath, uniqueName) - os.Remove(filepath.Join(baseDir, dbPath)) - database, err := OpenDatabase(baseDir, dbPath) +func SetupTestDb(t *testing.T, uniqueName string) *Database { + BaseDir = ".." + dbPath := fmt.Sprintf("%s_test.db", uniqueName) + os.Remove(filepath.Join(BaseDir, dbPath)) + database, err := OpenDatabase(dbPath) assert.Nil(t, err) return database } diff --git a/partner/tba_test.go b/partner/tba_test.go index b8fa9e2..17bf3d3 100644 --- a/partner/tba_test.go +++ b/partner/tba_test.go @@ -126,5 +126,5 @@ func TestPublishingErrors(t *testing.T) { } func setupTestDb(t *testing.T) *model.Database { - return model.SetupTestDb(t, "partner", "..") + return model.SetupTestDb(t, "partner") } diff --git a/pit_display.go b/pit_display.go index f23f5de..684e754 100644 --- a/pit_display.go +++ b/pit_display.go @@ -14,8 +14,8 @@ import ( ) // Renders the pit display which shows scrolling rankings. -func PitDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) pitDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } @@ -26,7 +26,7 @@ func PitDisplayHandler(w http.ResponseWriter, r *http.Request) { } data := struct { *model.EventSettings - }{eventSettings} + }{web.arena.EventSettings} err = template.Execute(w, data) if err != nil { handleWebErr(w, err) @@ -35,8 +35,8 @@ func PitDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the pit display, used only to force reloads remotely. -func PitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } @@ -47,7 +47,7 @@ func PitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Spin off a goroutine to listen for notifications and pass them on through the websocket. diff --git a/pit_display_test.go b/pit_display_test.go index 36f35e5..935602c 100644 --- a/pit_display_test.go +++ b/pit_display_test.go @@ -11,17 +11,17 @@ import ( ) func TestPitDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/pit") + recorder := web.getHttpResponse("/displays/pit") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Pit Display - Untitled Event - Cheesy Arena") } func TestPitDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil) assert.Nil(t, err) @@ -29,7 +29,7 @@ func TestPitDisplayWebsocket(t *testing.T) { ws := &Websocket{conn, new(sync.Mutex)} // Check forced reloading as that is the only purpose the pit websocket serves. - recorder := getHttpResponse("/setup/field/reload_displays") + recorder := web.getHttpResponse("/setup/field/reload_displays") assert.Equal(t, 302, recorder.Code) readWebsocketType(t, ws, "reload") } diff --git a/referee_display.go b/referee_display.go index 00f6786..863f30b 100644 --- a/referee_display.go +++ b/referee_display.go @@ -7,6 +7,7 @@ package main import ( "fmt" + "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/mitchellh/mapstructure" @@ -18,41 +19,41 @@ import ( ) // Renders the referee interface for assigning fouls. -func RefereeDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) refereeDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - template := template.New("").Funcs(templateHelpers) + template := template.New("").Funcs(web.templateHelpers) _, err := template.ParseFiles("templates/referee_display.html") if err != nil { handleWebErr(w, err) return } - match := mainArena.currentMatch + match := web.arena.CurrentMatch matchType := match.CapitalizedType() - red1 := mainArena.AllianceStations["R1"].Team + red1 := web.arena.AllianceStations["R1"].Team if red1 == nil { red1 = &model.Team{} } - red2 := mainArena.AllianceStations["R2"].Team + red2 := web.arena.AllianceStations["R2"].Team if red2 == nil { red2 = &model.Team{} } - red3 := mainArena.AllianceStations["R3"].Team + red3 := web.arena.AllianceStations["R3"].Team if red3 == nil { red3 = &model.Team{} } - blue1 := mainArena.AllianceStations["B1"].Team + blue1 := web.arena.AllianceStations["B1"].Team if blue1 == nil { blue1 = &model.Team{} } - blue2 := mainArena.AllianceStations["B2"].Team + blue2 := web.arena.AllianceStations["B2"].Team if blue2 == nil { blue2 = &model.Team{} } - blue3 := mainArena.AllianceStations["B3"].Team + blue3 := web.arena.AllianceStations["B3"].Team if blue3 == nil { blue3 = &model.Team{} } @@ -72,10 +73,10 @@ func RefereeDisplayHandler(w http.ResponseWriter, r *http.Request) { BlueCards map[string]string Rules []game.Rule EntryEnabled bool - }{eventSettings, matchType, match.DisplayName, red1, red2, red3, blue1, blue2, blue3, - mainArena.redRealtimeScore.CurrentScore.Fouls, mainArena.blueRealtimeScore.CurrentScore.Fouls, - mainArena.redRealtimeScore.Cards, mainArena.blueRealtimeScore.Cards, game.Rules, - !(mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted)} + }{web.arena.EventSettings, matchType, match.DisplayName, red1, red2, red3, blue1, blue2, blue3, + web.arena.RedRealtimeScore.CurrentScore.Fouls, web.arena.BlueRealtimeScore.CurrentScore.Fouls, + web.arena.RedRealtimeScore.Cards, web.arena.BlueRealtimeScore.Cards, game.Rules, + !(web.arena.RedRealtimeScore.FoulsCommitted && web.arena.BlueRealtimeScore.FoulsCommitted)} err = template.ExecuteTemplate(w, "referee_display.html", data) if err != nil { handleWebErr(w, err) @@ -84,7 +85,7 @@ func RefereeDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the refereee interface client to send control commands and receive status updates. -func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { +func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket. websocket, err := NewWebsocket(w, r) @@ -94,9 +95,9 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen() + matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen() defer close(matchLoadTeamsListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Spin off a goroutine to listen for notifications and pass them on through the websocket. @@ -154,15 +155,15 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // Add the foul to the correct alliance's list. foul := game.Foul{Rule: game.Rule{RuleNumber: args.Rule, IsTechnical: args.IsTechnical}, - TeamId: args.TeamId, TimeInMatchSec: mainArena.MatchTimeSec()} + TeamId: args.TeamId, TimeInMatchSec: web.arena.MatchTimeSec()} if args.Alliance == "red" { - mainArena.redRealtimeScore.CurrentScore.Fouls = - append(mainArena.redRealtimeScore.CurrentScore.Fouls, foul) + web.arena.RedRealtimeScore.CurrentScore.Fouls = + append(web.arena.RedRealtimeScore.CurrentScore.Fouls, foul) } else { - mainArena.blueRealtimeScore.CurrentScore.Fouls = - append(mainArena.blueRealtimeScore.CurrentScore.Fouls, foul) + web.arena.BlueRealtimeScore.CurrentScore.Fouls = + append(web.arena.BlueRealtimeScore.CurrentScore.Fouls, foul) } - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) case "deleteFoul": args := struct { Alliance string @@ -182,9 +183,9 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { TeamId: args.TeamId, TimeInMatchSec: args.TimeInMatchSec} var fouls *[]game.Foul if args.Alliance == "red" { - fouls = &mainArena.redRealtimeScore.CurrentScore.Fouls + fouls = &web.arena.RedRealtimeScore.CurrentScore.Fouls } else { - fouls = &mainArena.blueRealtimeScore.CurrentScore.Fouls + fouls = &web.arena.BlueRealtimeScore.CurrentScore.Fouls } for i, foul := range *fouls { if foul == deleteFoul { @@ -192,7 +193,7 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { break } } - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) case "card": args := struct { Alliance string @@ -208,32 +209,32 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // Set the card in the correct alliance's score. var cards map[string]string if args.Alliance == "red" { - cards = mainArena.redRealtimeScore.Cards + cards = web.arena.RedRealtimeScore.Cards } else { - cards = mainArena.blueRealtimeScore.Cards + cards = web.arena.BlueRealtimeScore.Cards } cards[strconv.Itoa(args.TeamId)] = args.Card continue case "signalReset": - if mainArena.MatchState != postMatch { + if web.arena.MatchState != field.PostMatch { // Don't allow clearing the field until the match is over. continue } - mainArena.fieldReset = true - mainArena.allianceStationDisplayScreen = "fieldReset" - mainArena.allianceStationDisplayNotifier.Notify(nil) + web.arena.FieldReset = true + web.arena.AllianceStationDisplayScreen = "fieldReset" + web.arena.AllianceStationDisplayNotifier.Notify(nil) continue // Don't reload. case "commitMatch": - if mainArena.MatchState != postMatch { + if web.arena.MatchState != field.PostMatch { // Don't allow committing the fouls until the match is over. continue } - mainArena.redRealtimeScore.FoulsCommitted = true - mainArena.blueRealtimeScore.FoulsCommitted = true - mainArena.fieldReset = true - mainArena.allianceStationDisplayScreen = "fieldReset" - mainArena.allianceStationDisplayNotifier.Notify(nil) - mainArena.scoringStatusNotifier.Notify(nil) + web.arena.RedRealtimeScore.FoulsCommitted = true + web.arena.BlueRealtimeScore.FoulsCommitted = true + web.arena.FieldReset = true + web.arena.AllianceStationDisplayScreen = "fieldReset" + web.arena.AllianceStationDisplayNotifier.Notify(nil) + web.arena.ScoringStatusNotifier.Notify(nil) default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) continue diff --git a/referee_display_test.go b/referee_display_test.go index c8bd789..02127b1 100644 --- a/referee_display_test.go +++ b/referee_display_test.go @@ -4,6 +4,7 @@ package main import ( + "github.com/Team254/cheesy-arena/field" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "sync" @@ -12,17 +13,17 @@ import ( ) func TestRefereeDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/referee") + recorder := web.getHttpResponse("/displays/referee") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Referee Display - Untitled Event - Cheesy Arena") } func TestRefereeDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/referee/websocket", nil) assert.Nil(t, err) @@ -47,38 +48,38 @@ func TestRefereeDisplayWebsocket(t *testing.T) { readWebsocketType(t, ws, "reload") readWebsocketType(t, ws, "reload") readWebsocketType(t, ws, "reload") - if assert.Equal(t, 2, len(mainArena.redRealtimeScore.CurrentScore.Fouls)) { - assert.Equal(t, 256, mainArena.redRealtimeScore.CurrentScore.Fouls[0].TeamId) - assert.Equal(t, "G22", mainArena.redRealtimeScore.CurrentScore.Fouls[0].RuleNumber) - assert.Equal(t, false, mainArena.redRealtimeScore.CurrentScore.Fouls[0].IsTechnical) - assert.Equal(t, 0.0, mainArena.redRealtimeScore.CurrentScore.Fouls[0].TimeInMatchSec) - assert.Equal(t, 359, mainArena.redRealtimeScore.CurrentScore.Fouls[1].TeamId) - assert.Equal(t, "G22", mainArena.redRealtimeScore.CurrentScore.Fouls[1].RuleNumber) - assert.Equal(t, true, mainArena.redRealtimeScore.CurrentScore.Fouls[1].IsTechnical) + if assert.Equal(t, 2, len(web.arena.RedRealtimeScore.CurrentScore.Fouls)) { + assert.Equal(t, 256, web.arena.RedRealtimeScore.CurrentScore.Fouls[0].TeamId) + assert.Equal(t, "G22", web.arena.RedRealtimeScore.CurrentScore.Fouls[0].RuleNumber) + assert.Equal(t, false, web.arena.RedRealtimeScore.CurrentScore.Fouls[0].IsTechnical) + assert.Equal(t, 0.0, web.arena.RedRealtimeScore.CurrentScore.Fouls[0].TimeInMatchSec) + assert.Equal(t, 359, web.arena.RedRealtimeScore.CurrentScore.Fouls[1].TeamId) + assert.Equal(t, "G22", web.arena.RedRealtimeScore.CurrentScore.Fouls[1].RuleNumber) + assert.Equal(t, true, web.arena.RedRealtimeScore.CurrentScore.Fouls[1].IsTechnical) } - if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.CurrentScore.Fouls)) { - assert.Equal(t, 1680, mainArena.blueRealtimeScore.CurrentScore.Fouls[0].TeamId) - assert.Equal(t, "G22", mainArena.blueRealtimeScore.CurrentScore.Fouls[0].RuleNumber) - assert.Equal(t, true, mainArena.blueRealtimeScore.CurrentScore.Fouls[0].IsTechnical) - assert.Equal(t, 0.0, mainArena.blueRealtimeScore.CurrentScore.Fouls[0].TimeInMatchSec) + if assert.Equal(t, 1, len(web.arena.BlueRealtimeScore.CurrentScore.Fouls)) { + assert.Equal(t, 1680, web.arena.BlueRealtimeScore.CurrentScore.Fouls[0].TeamId) + assert.Equal(t, "G22", web.arena.BlueRealtimeScore.CurrentScore.Fouls[0].RuleNumber) + assert.Equal(t, true, web.arena.BlueRealtimeScore.CurrentScore.Fouls[0].IsTechnical) + assert.Equal(t, 0.0, web.arena.BlueRealtimeScore.CurrentScore.Fouls[0].TimeInMatchSec) } - assert.False(t, mainArena.redRealtimeScore.FoulsCommitted) - assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted) + assert.False(t, web.arena.RedRealtimeScore.FoulsCommitted) + assert.False(t, web.arena.BlueRealtimeScore.FoulsCommitted) // Test foul deletion. ws.Write("deleteFoul", foulData) readWebsocketType(t, ws, "reload") - assert.Equal(t, 0, len(mainArena.blueRealtimeScore.CurrentScore.Fouls)) + assert.Equal(t, 0, len(web.arena.BlueRealtimeScore.CurrentScore.Fouls)) foulData.Alliance = "red" foulData.TeamId = 359 foulData.TimeInMatchSec = 29 // Make it not match. ws.Write("deleteFoul", foulData) readWebsocketType(t, ws, "reload") - assert.Equal(t, 2, len(mainArena.redRealtimeScore.CurrentScore.Fouls)) + assert.Equal(t, 2, len(web.arena.RedRealtimeScore.CurrentScore.Fouls)) foulData.TimeInMatchSec = 0 ws.Write("deleteFoul", foulData) readWebsocketType(t, ws, "reload") - assert.Equal(t, 1, len(mainArena.redRealtimeScore.CurrentScore.Fouls)) + assert.Equal(t, 1, len(web.arena.RedRealtimeScore.CurrentScore.Fouls)) // Test card setting. cardData := struct { @@ -92,28 +93,28 @@ func TestRefereeDisplayWebsocket(t *testing.T) { cardData.Card = "red" ws.Write("card", cardData) time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed. - if assert.Equal(t, 1, len(mainArena.redRealtimeScore.Cards)) { - assert.Equal(t, "yellow", mainArena.redRealtimeScore.Cards["256"]) + if assert.Equal(t, 1, len(web.arena.RedRealtimeScore.Cards)) { + assert.Equal(t, "yellow", web.arena.RedRealtimeScore.Cards["256"]) } - if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.Cards)) { - assert.Equal(t, "red", mainArena.blueRealtimeScore.Cards["1680"]) + if assert.Equal(t, 1, len(web.arena.BlueRealtimeScore.Cards)) { + assert.Equal(t, "red", web.arena.BlueRealtimeScore.Cards["1680"]) } // Test field reset and match committing. - mainArena.MatchState = postMatch + web.arena.MatchState = field.PostMatch ws.Write("signalReset", nil) time.Sleep(time.Millisecond * 10) - assert.Equal(t, "fieldReset", mainArena.allianceStationDisplayScreen) - assert.False(t, mainArena.redRealtimeScore.FoulsCommitted) - assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted) - mainArena.allianceStationDisplayScreen = "logo" + assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayScreen) + assert.False(t, web.arena.RedRealtimeScore.FoulsCommitted) + assert.False(t, web.arena.BlueRealtimeScore.FoulsCommitted) + web.arena.AllianceStationDisplayScreen = "logo" ws.Write("commitMatch", nil) readWebsocketType(t, ws, "reload") - assert.Equal(t, "fieldReset", mainArena.allianceStationDisplayScreen) - assert.True(t, mainArena.redRealtimeScore.FoulsCommitted) - assert.True(t, mainArena.blueRealtimeScore.FoulsCommitted) + assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayScreen) + assert.True(t, web.arena.RedRealtimeScore.FoulsCommitted) + assert.True(t, web.arena.BlueRealtimeScore.FoulsCommitted) // Should refresh the page when the next match is loaded. - mainArena.matchLoadTeamsNotifier.Notify(nil) + web.arena.MatchLoadTeamsNotifier.Notify(nil) readWebsocketType(t, ws, "reload") } diff --git a/reports.go b/reports.go index 8579b10..47d557a 100644 --- a/reports.go +++ b/reports.go @@ -16,12 +16,12 @@ import ( ) // Generates a CSV-formatted report of the qualification rankings. -func RankingsCsvReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) rankingsCsvReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - rankings, err := db.GetAllRankings() + rankings, err := web.arena.Database.GetAllRankings() if err != nil { handleWebErr(w, err) return @@ -42,12 +42,12 @@ func RankingsCsvReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a PDF-formatted report of the qualification rankings. -func RankingsPdfReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) rankingsPdfReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - rankings, err := db.GetAllRankings() + rankings, err := web.arena.Database.GetAllRankings() if err != nil { handleWebErr(w, err) return @@ -64,7 +64,7 @@ func RankingsPdfReportHandler(w http.ResponseWriter, r *http.Request) { // Render table header row. pdf.SetFont("Arial", "B", 10) pdf.SetFillColor(220, 220, 220) - pdf.CellFormat(195, rowHeight, "Team Standings - "+eventSettings.Name, "", 1, "C", false, 0, "") + pdf.CellFormat(195, rowHeight, "Team Standings - "+web.arena.EventSettings.Name, "", 1, "C", false, 0, "") pdf.CellFormat(colWidths["Rank"], rowHeight, "Rank", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["Team"], rowHeight, "Team", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["RP"], rowHeight, "RP", "1", 0, "C", true, 0, "") @@ -104,13 +104,13 @@ func RankingsPdfReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a CSV-formatted report of the match schedule. -func ScheduleCsvReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) scheduleCsvReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } vars := mux.Vars(r) - matches, err := db.GetMatchesByType(vars["type"]) + matches, err := web.arena.Database.GetMatchesByType(vars["type"]) if err != nil { handleWebErr(w, err) return @@ -131,18 +131,18 @@ func ScheduleCsvReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a PDF-formatted report of the match schedule. -func SchedulePdfReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) schedulePdfReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } vars := mux.Vars(r) - matches, err := db.GetMatchesByType(vars["type"]) + matches, err := web.arena.Database.GetMatchesByType(vars["type"]) if err != nil { handleWebErr(w, err) return } - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -162,7 +162,7 @@ func SchedulePdfReportHandler(w http.ResponseWriter, r *http.Request) { // Render table header row. pdf.SetFont("Arial", "B", 10) pdf.SetFillColor(220, 220, 220) - pdf.CellFormat(195, rowHeight, "Match Schedule - "+eventSettings.Name, "", 1, "C", false, 0, "") + pdf.CellFormat(195, rowHeight, "Match Schedule - "+web.arena.EventSettings.Name, "", 1, "C", false, 0, "") pdf.CellFormat(colWidths["Time"], rowHeight, "Time", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["Type"], rowHeight, "Type", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["Match"], rowHeight, "Match", "1", 0, "C", true, 0, "") @@ -240,12 +240,12 @@ func SchedulePdfReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a CSV-formatted report of the team list. -func TeamsCsvReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) teamsCsvReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -266,12 +266,12 @@ func TeamsCsvReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a PDF-formatted report of the team list. -func TeamsPdfReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsReader(w, r) { +func (web *Web) teamsPdfReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsReader(w, r) { return } - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -287,7 +287,7 @@ func TeamsPdfReportHandler(w http.ResponseWriter, r *http.Request) { pdf.SetFillColor(220, 220, 220) // Render table header row. - pdf.CellFormat(195, rowHeight, "Team List - "+eventSettings.Name, "", 1, "C", false, 0, "") + pdf.CellFormat(195, rowHeight, "Team List - "+web.arena.EventSettings.Name, "", 1, "C", false, 0, "") pdf.CellFormat(colWidths["Id"], rowHeight, "Team", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["Name"], rowHeight, "Name", "1", 0, "C", true, 0, "") pdf.CellFormat(colWidths["Location"], rowHeight, "Location", "1", 0, "C", true, 0, "") @@ -312,12 +312,12 @@ func TeamsPdfReportHandler(w http.ResponseWriter, r *http.Request) { } // Generates a CSV-formatted report of the WPA keys, for import into the radio kiosk. -func WpaKeysCsvReportHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) wpaKeysCsvReportHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return diff --git a/reports_test.go b/reports_test.go index 635734b..8a63ce4 100644 --- a/reports_test.go +++ b/reports_test.go @@ -12,14 +12,14 @@ import ( ) func TestRankingsCsvReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) ranking1 := game.TestRanking2() ranking2 := game.TestRanking1() - db.CreateRanking(ranking1) - db.CreateRanking(ranking2) + web.arena.Database.CreateRanking(ranking1) + web.arena.Database.CreateRanking(ranking2) - recorder := getHttpResponse("/reports/csv/rankings") + recorder := web.getHttpResponse("/reports/csv/rankings") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "text/plain", recorder.HeaderMap["Content-Type"][0]) expectedBody := "Rank,TeamId,RankingPoints,MatchPoints,AutoPoints,RotorPoints,TakeoffPoints,PressurePoints,Wins," + @@ -29,21 +29,21 @@ func TestRankingsCsvReport(t *testing.T) { } func TestRankingsPdfReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) ranking1 := game.TestRanking2() ranking2 := game.TestRanking1() - db.CreateRanking(ranking1) - db.CreateRanking(ranking2) + web.arena.Database.CreateRanking(ranking1) + web.arena.Database.CreateRanking(ranking2) // Can't really parse the PDF content and check it, so just check that what's sent back is a PDF. - recorder := getHttpResponse("/reports/pdf/rankings") + recorder := web.getHttpResponse("/reports/pdf/rankings") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/pdf", recorder.HeaderMap["Content-Type"][0]) } func TestScheduleCsvReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match1 := model.Match{Type: "qualification", DisplayName: "1", Time: time.Unix(0, 0), Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6, Blue1IsSurrogate: true, Blue2IsSurrogate: true, Blue3IsSurrogate: true} @@ -51,11 +51,11 @@ func TestScheduleCsvReport(t *testing.T) { Blue1: 10, Blue2: 11, Blue3: 12, Red1IsSurrogate: true, Red2IsSurrogate: true, Red3IsSurrogate: true} match3 := model.Match{Type: "practice", DisplayName: "1", Time: time.Now(), Red1: 6, Red2: 5, Red3: 4, Blue1: 3, Blue2: 2, Blue3: 1} - db.CreateMatch(&match1) - db.CreateMatch(&match2) - db.CreateMatch(&match3) + web.arena.Database.CreateMatch(&match1) + web.arena.Database.CreateMatch(&match2) + web.arena.Database.CreateMatch(&match3) - recorder := getHttpResponse("/reports/csv/schedule/qualification") + recorder := web.getHttpResponse("/reports/csv/schedule/qualification") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "text/plain", recorder.HeaderMap["Content-Type"][0]) expectedBody := "Match,Type,Time,Red1,Red1IsSurrogate,Red2,Red2IsSurrogate,Red3,Red3IsSurrogate,Blue1," + @@ -66,32 +66,32 @@ func TestScheduleCsvReport(t *testing.T) { } func TestSchedulePdfReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) match := model.Match{Type: "practice", DisplayName: "1", Time: time.Unix(0, 0), Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6, Blue1IsSurrogate: true, Blue2IsSurrogate: true, Blue3IsSurrogate: true} - db.CreateMatch(&match) + web.arena.Database.CreateMatch(&match) team := model.Team{Id: 254, Name: "NASA", Nickname: "The Cheesy Poofs", City: "San Jose", StateProv: "CA", Country: "USA", RookieYear: 1999, RobotName: "Barrage"} - db.CreateTeam(&team) + web.arena.Database.CreateTeam(&team) // Can't really parse the PDF content and check it, so just check that what's sent back is a PDF. - recorder := getHttpResponse("/reports/pdf/schedule/practice") + recorder := web.getHttpResponse("/reports/pdf/schedule/practice") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/pdf", recorder.HeaderMap["Content-Type"][0]) } func TestTeamsCsvReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) team1 := model.Team{Id: 254, Name: "NASA", Nickname: "The Cheesy Poofs", City: "San Jose", StateProv: "CA", Country: "USA", RookieYear: 1999, RobotName: "Barrage"} team2 := model.Team{Id: 1114, Name: "GM", Nickname: "Simbotics", City: "St. Catharines", StateProv: "ON", Country: "Canada", RookieYear: 2003, RobotName: "Simbot Evolution"} - db.CreateTeam(&team1) - db.CreateTeam(&team2) + web.arena.Database.CreateTeam(&team1) + web.arena.Database.CreateTeam(&team2) - recorder := getHttpResponse("/reports/csv/teams") + recorder := web.getHttpResponse("/reports/csv/teams") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "text/plain", recorder.HeaderMap["Content-Type"][0]) expectedBody := "Number,Name,Nickname,City,StateProv,Country,RookieYear,RobotName\n254,\"NASA\"," + @@ -101,27 +101,27 @@ func TestTeamsCsvReport(t *testing.T) { } func TestTeamsPdfReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) team := model.Team{Id: 254, Name: "NASA", Nickname: "The Cheesy Poofs", City: "San Jose", StateProv: "CA", Country: "USA", RookieYear: 1999, RobotName: "Barrage"} - db.CreateTeam(&team) + web.arena.Database.CreateTeam(&team) // Can't really parse the PDF content and check it, so just check that what's sent back is a PDF. - recorder := getHttpResponse("/reports/pdf/teams") + recorder := web.getHttpResponse("/reports/pdf/teams") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/pdf", recorder.HeaderMap["Content-Type"][0]) } func TestWpaKeysCsvReport(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) team1 := model.Team{Id: 254, WpaKey: "12345678"} team2 := model.Team{Id: 1114, WpaKey: "9876543210"} - db.CreateTeam(&team1) - db.CreateTeam(&team2) + web.arena.Database.CreateTeam(&team1) + web.arena.Database.CreateTeam(&team2) - recorder := getHttpResponse("/reports/csv/wpa_keys") + recorder := web.getHttpResponse("/reports/csv/wpa_keys") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "text/csv", recorder.HeaderMap["Content-Type"][0]) assert.Equal(t, "attachment; filename=wpa_keys.csv", recorder.HeaderMap["Content-Disposition"][0]) diff --git a/scoring_display.go b/scoring_display.go index 36ade56..3d70202 100644 --- a/scoring_display.go +++ b/scoring_display.go @@ -7,6 +7,7 @@ package main import ( "fmt" + "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/gorilla/mux" @@ -17,8 +18,8 @@ import ( ) // Renders the scoring interface which enables input of scores in real-time. -func ScoringDisplayHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) scoringDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -37,7 +38,7 @@ func ScoringDisplayHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings Alliance string - }{eventSettings, alliance} + }{web.arena.EventSettings, alliance} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -46,8 +47,8 @@ func ScoringDisplayHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the scoring interface client to send control commands and receive status updates. -func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) scoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -57,14 +58,14 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, fmt.Errorf("Invalid alliance '%s'.", alliance)) return } - var score **RealtimeScore + var score **field.RealtimeScore var scoreSummaryFunc func() *game.ScoreSummary if alliance == "red" { - score = &mainArena.redRealtimeScore - scoreSummaryFunc = mainArena.RedScoreSummary + score = &web.arena.RedRealtimeScore + scoreSummaryFunc = web.arena.RedScoreSummary } else { - score = &mainArena.blueRealtimeScore - scoreSummaryFunc = mainArena.BlueScoreSummary + score = &web.arena.BlueRealtimeScore + scoreSummaryFunc = web.arena.BlueScoreSummary } autoCommitted := false @@ -75,16 +76,16 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer websocket.Close() - matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen() + matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen() defer close(matchLoadTeamsListener) - matchTimeListener := mainArena.matchTimeNotifier.Listen() + matchTimeListener := web.arena.MatchTimeNotifier.Listen() defer close(matchTimeListener) - reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen() + reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen() defer close(reloadDisplaysListener) // Send the various notifications immediately upon connection. data := struct { - Score *RealtimeScore + Score *field.RealtimeScore ScoreSummary *game.ScoreSummary AutoCommitted bool }{*score, scoreSummaryFunc(), autoCommitted} @@ -93,7 +94,7 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Websocket error: %s", err) return } - err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}) + err = websocket.Write("matchTime", MatchTimeMessage{web.arena.MatchState, int(web.arena.LastMatchTimeSec)}) if err != nil { log.Printf("Websocket error: %s", err) return @@ -116,7 +117,7 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { return } messageType = "matchTime" - message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} + message = MatchTimeMessage{web.arena.MatchState, matchTimeSec.(int)} case _, ok := <-reloadDisplaysListener: if !ok { return @@ -158,13 +159,13 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } } case "commit": - if mainArena.MatchState != preMatch || mainArena.currentMatch.Type == "test" { + if web.arena.MatchState != field.PreMatch || web.arena.CurrentMatch.Type == "test" { autoCommitted = true } case "uncommitAuto": autoCommitted = false case "commitMatch": - if mainArena.MatchState != postMatch { + if web.arena.MatchState != field.PostMatch { // Don't allow committing the score until the match is over. websocket.WriteError("Cannot commit score: Match is not over.") continue @@ -172,17 +173,17 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { autoCommitted = true (*score).TeleopCommitted = true - mainArena.scoringStatusNotifier.Notify(nil) + web.arena.ScoringStatusNotifier.Notify(nil) default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) continue } - mainArena.realtimeScoreNotifier.Notify(nil) + web.arena.RealtimeScoreNotifier.Notify(nil) // Send out the score again after handling the command, as it most likely changed as a result. data = struct { - Score *RealtimeScore + Score *field.RealtimeScore ScoreSummary *game.ScoreSummary AutoCommitted bool }{*score, scoreSummaryFunc(), autoCommitted} diff --git a/scoring_display_test.go b/scoring_display_test.go index 489c860..4bc511c 100644 --- a/scoring_display_test.go +++ b/scoring_display_test.go @@ -4,6 +4,7 @@ package main import ( + "github.com/Team254/cheesy-arena/field" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "sync" @@ -11,22 +12,22 @@ import ( ) func TestScoringDisplay(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/displays/scoring/invalidalliance") + recorder := web.getHttpResponse("/displays/scoring/invalidalliance") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "Invalid alliance") - recorder = getHttpResponse("/displays/scoring/red") + recorder = web.getHttpResponse("/displays/scoring/red") assert.Equal(t, 200, recorder.Code) - recorder = getHttpResponse("/displays/scoring/blue") + recorder = web.getHttpResponse("/displays/scoring/blue") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Scoring - Untitled Event - Cheesy Arena") } func TestScoringDisplayWebsocket(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() _, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil) assert.NotNil(t, err) @@ -63,8 +64,8 @@ func TestScoringDisplayWebsocket(t *testing.T) { readWebsocketType(t, blueWs, "score") } - assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoMobility) - assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.AutoMobility) + assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoMobility) + assert.Equal(t, 2, web.arena.BlueRealtimeScore.CurrentScore.AutoMobility) redWs.Write("mobility", nil) for i := 0; i < 1; i++ { @@ -75,23 +76,23 @@ func TestScoringDisplayWebsocket(t *testing.T) { } // Make sure auto scores haven't changed in teleop. - assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoMobility) - assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.AutoMobility) + assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoMobility) + assert.Equal(t, 2, web.arena.BlueRealtimeScore.CurrentScore.AutoMobility) // Test committing logic. redWs.Write("commitMatch", nil) readWebsocketType(t, redWs, "error") - mainArena.MatchState = postMatch + web.arena.MatchState = field.PostMatch redWs.Write("commitMatch", nil) blueWs.Write("commitMatch", nil) readWebsocketType(t, redWs, "score") readWebsocketType(t, blueWs, "score") // Load another match to reset the results. - mainArena.ResetMatch() - mainArena.LoadTestMatch() + web.arena.ResetMatch() + web.arena.LoadTestMatch() readWebsocketType(t, redWs, "reload") readWebsocketType(t, blueWs, "reload") - assert.Equal(t, NewRealtimeScore(), mainArena.redRealtimeScore) - assert.Equal(t, NewRealtimeScore(), mainArena.blueRealtimeScore) + assert.Equal(t, field.NewRealtimeScore(), web.arena.RedRealtimeScore) + assert.Equal(t, field.NewRealtimeScore(), web.arena.BlueRealtimeScore) } diff --git a/setup_alliance_selection.go b/setup_alliance_selection.go index 4432124..6877243 100644 --- a/setup_alliance_selection.go +++ b/setup_alliance_selection.go @@ -26,22 +26,22 @@ var cachedAlliances [][]*model.AllianceTeam var cachedRankedTeams []*RankedTeam // Shows the alliance selection page. -func AllianceSelectionGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) allianceSelectionGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - renderAllianceSelection(w, r, "") + web.renderAllianceSelection(w, r, "") } // Updates the cache with the latest input from the client. -func AllianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyAllianceSelection() { - renderAllianceSelection(w, r, "Alliance selection has already been finalized.") + if !web.canModifyAllianceSelection() { + web.renderAllianceSelection(w, r, "Alliance selection has already been finalized.") return } @@ -60,14 +60,14 @@ func AllianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) { } else { teamId, err := strconv.Atoi(teamString) if err != nil { - renderAllianceSelection(w, r, fmt.Sprintf("Invalid team number value '%s'.", teamString)) + web.renderAllianceSelection(w, r, fmt.Sprintf("Invalid team number value '%s'.", teamString)) return } found := false for _, team := range newRankedTeams { if team.TeamId == teamId { if team.Picked { - renderAllianceSelection(w, r, fmt.Sprintf("Team %d is already part of an alliance.", teamId)) + web.renderAllianceSelection(w, r, fmt.Sprintf("Team %d is already part of an alliance.", teamId)) return } found = true @@ -77,7 +77,7 @@ func AllianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) { } } if !found { - renderAllianceSelection(w, r, fmt.Sprintf("Team %d is not present at this event.", teamId)) + web.renderAllianceSelection(w, r, fmt.Sprintf("Team %d is not present at this event.", teamId)) return } } @@ -85,32 +85,32 @@ func AllianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) { } cachedRankedTeams = newRankedTeams - mainArena.allianceSelectionNotifier.Notify(nil) + web.arena.AllianceSelectionNotifier.Notify(nil) http.Redirect(w, r, "/setup/alliance_selection", 302) } // Sets up the empty alliances and populates the ranked team list. -func AllianceSelectionStartHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) allianceSelectionStartHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } if len(cachedAlliances) != 0 { - renderAllianceSelection(w, r, "Can't start alliance selection when it is already in progress.") + web.renderAllianceSelection(w, r, "Can't start alliance selection when it is already in progress.") return } - if !canModifyAllianceSelection() { - renderAllianceSelection(w, r, "Alliance selection has already been finalized.") + if !web.canModifyAllianceSelection() { + web.renderAllianceSelection(w, r, "Alliance selection has already been finalized.") return } // Create a blank alliance set matching the event configuration. - cachedAlliances = make([][]*model.AllianceTeam, eventSettings.NumElimAlliances) + cachedAlliances = make([][]*model.AllianceTeam, web.arena.EventSettings.NumElimAlliances) teamsPerAlliance := 3 - if eventSettings.SelectionRound3Order != "" { + if web.arena.EventSettings.SelectionRound3Order != "" { teamsPerAlliance = 4 } - for i := 0; i < eventSettings.NumElimAlliances; i++ { + for i := 0; i < web.arena.EventSettings.NumElimAlliances; i++ { cachedAlliances[i] = make([]*model.AllianceTeam, teamsPerAlliance) for j := 0; j < teamsPerAlliance; j++ { cachedAlliances[i][j] = &model.AllianceTeam{AllianceId: i + 1, PickPosition: j} @@ -118,7 +118,7 @@ func AllianceSelectionStartHandler(w http.ResponseWriter, r *http.Request) { } // Populate the ranked list of teams. - rankings, err := db.GetAllRankings() + rankings, err := web.arena.Database.GetAllRankings() if err != nil { handleWebErr(w, err) return @@ -128,42 +128,42 @@ func AllianceSelectionStartHandler(w http.ResponseWriter, r *http.Request) { cachedRankedTeams[i] = &RankedTeam{i + 1, ranking.TeamId, false} } - mainArena.allianceSelectionNotifier.Notify(nil) + web.arena.AllianceSelectionNotifier.Notify(nil) http.Redirect(w, r, "/setup/alliance_selection", 302) } // Resets the alliance selection process back to the starting point. -func AllianceSelectionResetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) allianceSelectionResetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyAllianceSelection() { - renderAllianceSelection(w, r, "Alliance selection has already been finalized.") + if !web.canModifyAllianceSelection() { + web.renderAllianceSelection(w, r, "Alliance selection has already been finalized.") return } cachedAlliances = [][]*model.AllianceTeam{} cachedRankedTeams = []*RankedTeam{} - mainArena.allianceSelectionNotifier.Notify(nil) + web.arena.AllianceSelectionNotifier.Notify(nil) http.Redirect(w, r, "/setup/alliance_selection", 302) } // Saves the selected alliances to the database and generates the first round of elimination matches. -func AllianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyAllianceSelection() { - renderAllianceSelection(w, r, "Alliance selection has already been finalized.") + if !web.canModifyAllianceSelection() { + web.renderAllianceSelection(w, r, "Alliance selection has already been finalized.") return } location, _ := time.LoadLocation("Local") startTime, err := time.ParseInLocation("2006-01-02 03:04:05 PM", r.PostFormValue("startTime"), location) if err != nil { - renderAllianceSelection(w, r, "Must specify a valid start time for the playoff rounds.") + web.renderAllianceSelection(w, r, "Must specify a valid start time for the playoff rounds.") return } @@ -171,7 +171,7 @@ func AllianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { for _, alliance := range cachedAlliances { for _, team := range alliance { if team.TeamId <= 0 { - renderAllianceSelection(w, r, "Can't finalize alliance selection until all spots have been filled.") + web.renderAllianceSelection(w, r, "Can't finalize alliance selection until all spots have been filled.") return } } @@ -180,7 +180,7 @@ func AllianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { // Save alliances to the database. for _, alliance := range cachedAlliances { for _, team := range alliance { - err := db.CreateAllianceTeam(team) + err := web.arena.Database.CreateAllianceTeam(team) if err != nil { handleWebErr(w, err) return @@ -189,36 +189,36 @@ func AllianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { } // Generate the first round of elimination matches. - _, err = tournament.UpdateEliminationSchedule(db, startTime) + _, err = tournament.UpdateEliminationSchedule(web.arena.Database, startTime) if err != nil { handleWebErr(w, err) return } // Reset yellow cards. - err = tournament.CalculateTeamCards(db, "elimination") + err = tournament.CalculateTeamCards(web.arena.Database, "elimination") if err != nil { handleWebErr(w, err) return } // Back up the database. - err = db.Backup(eventSettings.Name, "post_alliance_selection") + err = web.arena.Database.Backup(web.arena.EventSettings.Name, "post_alliance_selection") if err != nil { handleWebErr(w, err) return } - if eventSettings.TbaPublishingEnabled { + if web.arena.EventSettings.TbaPublishingEnabled { // Publish alliances and schedule to The Blue Alliance. - err = tbaClient.PublishAlliances(db) + err = web.arena.TbaClient.PublishAlliances(web.arena.Database) if err != nil { - renderAllianceSelection(w, r, fmt.Sprintf("Failed to publish alliances: %s", err.Error())) + web.renderAllianceSelection(w, r, fmt.Sprintf("Failed to publish alliances: %s", err.Error())) return } - err = tbaClient.PublishMatches(db) + err = web.arena.TbaClient.PublishMatches(web.arena.Database) if err != nil { - renderAllianceSelection(w, r, fmt.Sprintf("Failed to publish matches: %s", err.Error())) + web.renderAllianceSelection(w, r, fmt.Sprintf("Failed to publish matches: %s", err.Error())) return } } @@ -226,13 +226,13 @@ func AllianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/setup/alliance_selection", 302) } -func renderAllianceSelection(w http.ResponseWriter, r *http.Request, errorMessage string) { +func (web *Web) renderAllianceSelection(w http.ResponseWriter, r *http.Request, errorMessage string) { template, err := template.ParseFiles("templates/setup_alliance_selection.html", "templates/base.html") if err != nil { handleWebErr(w, err) return } - nextRow, nextCol := determineNextCell() + nextRow, nextCol := web.determineNextCell() data := struct { *model.EventSettings Alliances [][]*model.AllianceTeam @@ -240,7 +240,7 @@ func renderAllianceSelection(w http.ResponseWriter, r *http.Request, errorMessag NextRow int NextCol int ErrorMessage string - }{eventSettings, cachedAlliances, cachedRankedTeams, nextRow, nextCol, errorMessage} + }{web.arena.EventSettings, cachedAlliances, cachedRankedTeams, nextRow, nextCol, errorMessage} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -249,8 +249,8 @@ func renderAllianceSelection(w http.ResponseWriter, r *http.Request, errorMessag } // Returns true if it is safe to change the alliance selection (i.e. no elimination matches exist yet). -func canModifyAllianceSelection() bool { - matches, err := db.GetMatchesByType("elimination") +func (web *Web) canModifyAllianceSelection() bool { + matches, err := web.arena.Database.GetMatchesByType("elimination") if err != nil || len(matches) > 0 { return false } @@ -258,7 +258,7 @@ func canModifyAllianceSelection() bool { } // Returns the row and column of the next alliance selection spot that should have keyboard autofocus. -func determineNextCell() (int, int) { +func (web *Web) determineNextCell() (int, int) { // Check the first two columns. for i, alliance := range cachedAlliances { if alliance[0].TeamId == 0 { @@ -270,7 +270,7 @@ func determineNextCell() (int, int) { } // Check the third column. - if eventSettings.SelectionRound2Order == "F" { + if web.arena.EventSettings.SelectionRound2Order == "F" { for i, alliance := range cachedAlliances { if alliance[2].TeamId == 0 { return i, 2 @@ -285,13 +285,13 @@ func determineNextCell() (int, int) { } // Check the fourth column. - if eventSettings.SelectionRound3Order == "F" { + if web.arena.EventSettings.SelectionRound3Order == "F" { for i, alliance := range cachedAlliances { if alliance[3].TeamId == 0 { return i, 3 } } - } else if eventSettings.SelectionRound3Order == "L" { + } else if web.arena.EventSettings.SelectionRound3Order == "L" { for i := len(cachedAlliances) - 1; i >= 0; i-- { if cachedAlliances[i][3].TeamId == 0 { return i, 3 diff --git a/setup_alliance_selection_test.go b/setup_alliance_selection_test.go index 1d0b553..fc74b7c 100644 --- a/setup_alliance_selection_test.go +++ b/setup_alliance_selection_test.go @@ -11,243 +11,240 @@ import ( ) func TestSetupAllianceSelection(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) cachedAlliances = [][]*model.AllianceTeam{} cachedRankedTeams = []*RankedTeam{} - eventSettings, _ = db.GetEventSettings() - eventSettings.NumElimAlliances = 15 - eventSettings.SelectionRound3Order = "L" + web.arena.EventSettings.NumElimAlliances = 15 + web.arena.EventSettings.SelectionRound3Order = "L" for i := 1; i <= 10; i++ { - db.CreateRanking(&game.Ranking{TeamId: 100 + i, Rank: i}) + web.arena.Database.CreateRanking(&game.Ranking{TeamId: 100 + i, Rank: i}) } // Check that there are no alliance placeholders to start. - recorder := getHttpResponse("/setup/alliance_selection") + recorder := web.getHttpResponse("/setup/alliance_selection") assert.Equal(t, 200, recorder.Code) assert.NotContains(t, recorder.Body.String(), "Captain") assert.NotContains(t, recorder.Body.String(), ">110<") // Start the alliance selection. - recorder = postHttpResponse("/setup/alliance_selection/start", "") + recorder = web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 302, recorder.Code) if assert.Equal(t, 15, len(cachedAlliances)) { assert.Equal(t, 4, len(cachedAlliances[0])) } - recorder = getHttpResponse("/setup/alliance_selection") + recorder = web.getHttpResponse("/setup/alliance_selection") assert.Contains(t, recorder.Body.String(), "Captain") assert.Contains(t, recorder.Body.String(), ">110<") // Reset the alliance selection. - recorder = postHttpResponse("/setup/alliance_selection/reset", "") + recorder = web.postHttpResponse("/setup/alliance_selection/reset", "") assert.Equal(t, 302, recorder.Code) assert.NotContains(t, recorder.Body.String(), "Captain") assert.NotContains(t, recorder.Body.String(), ">110<") - eventSettings.NumElimAlliances = 3 - eventSettings.SelectionRound3Order = "" - recorder = postHttpResponse("/setup/alliance_selection/start", "") + web.arena.EventSettings.NumElimAlliances = 3 + web.arena.EventSettings.SelectionRound3Order = "" + recorder = web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 302, recorder.Code) if assert.Equal(t, 3, len(cachedAlliances)) { assert.Equal(t, 3, len(cachedAlliances[0])) } // Update one team at a time. - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=110") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=110") assert.Equal(t, 302, recorder.Code) assert.Equal(t, 110, cachedAlliances[0][0].TeamId) - recorder = getHttpResponse("/setup/alliance_selection") + recorder = web.getHttpResponse("/setup/alliance_selection") assert.Contains(t, recorder.Body.String(), "\"110\"") assert.NotContains(t, recorder.Body.String(), ">110<") // Update multiple teams at a time. - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&selection1_0=103") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&selection1_0=103") assert.Equal(t, 302, recorder.Code) assert.Equal(t, 101, cachedAlliances[0][0].TeamId) assert.Equal(t, 102, cachedAlliances[0][1].TeamId) assert.Equal(t, 103, cachedAlliances[1][0].TeamId) - recorder = getHttpResponse("/setup/alliance_selection") + recorder = web.getHttpResponse("/setup/alliance_selection") assert.Contains(t, recorder.Body.String(), ">110<") // Update remainder of teams. - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&"+ + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&"+ "selection0_2=103&selection1_0=104&selection1_1=105&selection1_2=106&selection2_0=107&selection2_1=108&"+ "selection2_2=109") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/alliance_selection") + recorder = web.getHttpResponse("/setup/alliance_selection") assert.Contains(t, recorder.Body.String(), ">110<") // Finalize alliance selection. - db.CreateTeam(&model.Team{Id: 254, YellowCard: true}) - recorder = postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") + web.arena.Database.CreateTeam(&model.Team{Id: 254, YellowCard: true}) + recorder = web.postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") assert.Equal(t, 302, recorder.Code) - alliances, err := db.GetAllAlliances() + alliances, err := web.arena.Database.GetAllAlliances() assert.Nil(t, err) if assert.Equal(t, 3, len(alliances)) { assert.Equal(t, 101, alliances[0][0].TeamId) assert.Equal(t, 105, alliances[1][1].TeamId) assert.Equal(t, 109, alliances[2][2].TeamId) } - matches, err := db.GetMatchesByType("elimination") + matches, err := web.arena.Database.GetMatchesByType("elimination") assert.Nil(t, err) assert.Equal(t, 6, len(matches)) - team, _ := db.GetTeamById(254) + team, _ := web.arena.Database.GetTeamById(254) assert.False(t, team.YellowCard) } func TestSetupAllianceSelectionErrors(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) cachedAlliances = [][]*model.AllianceTeam{} cachedRankedTeams = []*RankedTeam{} - eventSettings, _ = db.GetEventSettings() - eventSettings.NumElimAlliances = 2 + web.arena.EventSettings.NumElimAlliances = 2 for i := 1; i <= 6; i++ { - db.CreateRanking(&game.Ranking{TeamId: 100 + i, Rank: i}) + web.arena.Database.CreateRanking(&game.Ranking{TeamId: 100 + i, Rank: i}) } // Start an alliance selection that is already underway. - recorder := postHttpResponse("/setup/alliance_selection/start", "") + recorder := web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 302, recorder.Code) - recorder = postHttpResponse("/setup/alliance_selection/start", "") + recorder = web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already in progress") // Select invalid teams. - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=asdf") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=asdf") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Invalid team number") - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=100") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=100") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "not present at this event") - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection1_1=101") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection1_1=101") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already part of an alliance") // Finalize early and without required parameters. - recorder = postHttpResponse("/setup/alliance_selection/finalize", + recorder = web.postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM&matchSpacingSec=360") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "until all spots have been filled") - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&"+ + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=101&selection0_1=102&"+ "selection0_2=103&selection1_0=104&selection1_1=105&selection1_2=106") assert.Equal(t, 302, recorder.Code) - recorder = postHttpResponse("/setup/alliance_selection/finalize", "startTime=asdf") + recorder = web.postHttpResponse("/setup/alliance_selection/finalize", "startTime=asdf") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "valid start time") // Finalize for real and check that TBA publishing is triggered. - tbaClient.BaseUrl = "fakeurl" - eventSettings.TbaPublishingEnabled = true - recorder = postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") + web.arena.TbaClient.BaseUrl = "fakeurl" + web.arena.EventSettings.TbaPublishingEnabled = true + recorder = web.postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Failed to publish alliances") // Do other things after finalization. - recorder = postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") + recorder = web.postHttpResponse("/setup/alliance_selection/finalize", "startTime=2014-01-01 01:00:00 PM") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already been finalized") - recorder = postHttpResponse("/setup/alliance_selection/reset", "") + recorder = web.postHttpResponse("/setup/alliance_selection/reset", "") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already been finalized") - recorder = postHttpResponse("/setup/alliance_selection", "selection0_0=asdf") + recorder = web.postHttpResponse("/setup/alliance_selection", "selection0_0=asdf") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already been finalized") cachedAlliances = [][]*model.AllianceTeam{} cachedRankedTeams = []*RankedTeam{} - recorder = postHttpResponse("/setup/alliance_selection/start", "") + recorder = web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "already been finalized") } func TestSetupAllianceSelectionAutofocus(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) cachedAlliances = [][]*model.AllianceTeam{} cachedRankedTeams = []*RankedTeam{} - eventSettings, _ = db.GetEventSettings() - eventSettings.NumElimAlliances = 2 + web.arena.EventSettings.NumElimAlliances = 2 // Straight draft. - eventSettings.SelectionRound2Order = "F" - eventSettings.SelectionRound3Order = "F" - recorder := postHttpResponse("/setup/alliance_selection/start", "") + web.arena.EventSettings.SelectionRound2Order = "F" + web.arena.EventSettings.SelectionRound3Order = "F" + recorder := web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 302, recorder.Code) - i, j := determineNextCell() + i, j := web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 0, j) cachedAlliances[0][0].TeamId = 1 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 1, j) cachedAlliances[0][1].TeamId = 2 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 0, j) cachedAlliances[1][0].TeamId = 3 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 1, j) cachedAlliances[1][1].TeamId = 4 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 2, j) cachedAlliances[0][2].TeamId = 5 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 2, j) cachedAlliances[1][2].TeamId = 6 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 3, j) cachedAlliances[0][3].TeamId = 7 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 3, j) cachedAlliances[1][3].TeamId = 8 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, -1, i) assert.Equal(t, -1, j) // Double-serpentine draft. - eventSettings.SelectionRound2Order = "L" - eventSettings.SelectionRound3Order = "L" - recorder = postHttpResponse("/setup/alliance_selection/reset", "") + web.arena.EventSettings.SelectionRound2Order = "L" + web.arena.EventSettings.SelectionRound3Order = "L" + recorder = web.postHttpResponse("/setup/alliance_selection/reset", "") assert.Equal(t, 302, recorder.Code) - recorder = postHttpResponse("/setup/alliance_selection/start", "") + recorder = web.postHttpResponse("/setup/alliance_selection/start", "") assert.Equal(t, 302, recorder.Code) - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 0, j) cachedAlliances[0][0].TeamId = 1 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 1, j) cachedAlliances[0][1].TeamId = 2 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 0, j) cachedAlliances[1][0].TeamId = 3 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 1, j) cachedAlliances[1][1].TeamId = 4 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 2, j) cachedAlliances[1][2].TeamId = 5 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 2, j) cachedAlliances[0][2].TeamId = 6 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 1, i) assert.Equal(t, 3, j) cachedAlliances[1][3].TeamId = 7 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, 0, i) assert.Equal(t, 3, j) cachedAlliances[0][3].TeamId = 8 - i, j = determineNextCell() + i, j = web.determineNextCell() assert.Equal(t, -1, i) assert.Equal(t, -1, j) } diff --git a/setup_field.go b/setup_field.go index 2df1718..5da4611 100644 --- a/setup_field.go +++ b/setup_field.go @@ -12,8 +12,8 @@ import ( ) // Shows the field configuration page. -func FieldGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) fieldGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -25,8 +25,7 @@ func FieldGetHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings AllianceStationDisplays map[string]string - LightsMode string - }{eventSettings, mainArena.allianceStationDisplays, mainArena.lights.currentMode} + }{web.arena.EventSettings, web.arena.AllianceStationDisplays} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -35,34 +34,24 @@ func FieldGetHandler(w http.ResponseWriter, r *http.Request) { } // Updates the display-station mapping for a single display. -func FieldPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) fieldPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } displayId := r.PostFormValue("displayId") allianceStation := r.PostFormValue("allianceStation") - mainArena.allianceStationDisplays[displayId] = allianceStation - mainArena.matchLoadTeamsNotifier.Notify(nil) + web.arena.AllianceStationDisplays[displayId] = allianceStation + web.arena.MatchLoadTeamsNotifier.Notify(nil) http.Redirect(w, r, "/setup/field", 302) } // Force-reloads all the websocket-connected displays. -func FieldReloadDisplaysHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) fieldReloadDisplaysHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - mainArena.reloadDisplaysNotifier.Notify(nil) - http.Redirect(w, r, "/setup/field", 302) -} - -// Controls the field LEDs for testing or effect. -func FieldLightsPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { - return - } - - mainArena.lights.SetMode(r.PostFormValue("mode")) + web.arena.ReloadDisplaysNotifier.Notify(nil) http.Redirect(w, r, "/setup/field", 302) } diff --git a/setup_field_test.go b/setup_field_test.go index c0b6fea..f9aa283 100644 --- a/setup_field_test.go +++ b/setup_field_test.go @@ -9,21 +9,24 @@ import ( ) func TestSetupField(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - mainArena.allianceStationDisplays["12345"] = "" - recorder := getHttpResponse("/setup/field") + web.arena.AllianceStationDisplays["12345"] = "" + recorder := web.getHttpResponse("/setup/field") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "12345") assert.NotContains(t, recorder.Body.String(), "selected") - recorder = postHttpResponse("/setup/field", "displayId=12345&allianceStation=B1") + recorder = web.postHttpResponse("/setup/field", "displayId=12345&allianceStation=B1") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/field") + recorder = web.getHttpResponse("/setup/field") assert.Contains(t, recorder.Body.String(), "12345") assert.Contains(t, recorder.Body.String(), "selected") - recorder = postHttpResponse("/setup/field/lights", "mode=strobe") - assert.Equal(t, 302, recorder.Code) - assert.Equal(t, "strobe", mainArena.lights.currentMode) + // TODO(patrick): Replace with PLC mode. + /* + recorder = web.postHttpResponse("/setup/field/lights", "mode=strobe") + assert.Equal(t, 302, recorder.Code) + assert.Equal(t, "strobe", web.arena.Lights.currentMode) + */ } diff --git a/setup_lower_thirds.go b/setup_lower_thirds.go index a399907..65c24d7 100644 --- a/setup_lower_thirds.go +++ b/setup_lower_thirds.go @@ -16,8 +16,8 @@ import ( ) // Shows the lower third configuration page. -func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) lowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -26,7 +26,7 @@ func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, err) return } - lowerThirds, err := db.GetAllLowerThirds() + lowerThirds, err := web.arena.Database.GetAllLowerThirds() if err != nil { handleWebErr(w, err) return @@ -34,7 +34,7 @@ func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings LowerThirds []model.LowerThird - }{eventSettings, lowerThirds} + }{web.arena.EventSettings, lowerThirds} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -43,8 +43,8 @@ func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) { } // The websocket endpoint for the lower thirds client to send control commands. -func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -75,7 +75,7 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - saveLowerThird(&lowerThird) + web.saveLowerThird(&lowerThird) case "deleteLowerThird": var lowerThird model.LowerThird err = mapstructure.Decode(data, &lowerThird) @@ -83,7 +83,7 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - err = db.DeleteLowerThird(&lowerThird) + err = web.arena.Database.DeleteLowerThird(&lowerThird) if err != nil { websocket.WriteError(err.Error()) continue @@ -95,10 +95,10 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - saveLowerThird(&lowerThird) - mainArena.lowerThirdNotifier.Notify(lowerThird) - mainArena.audienceDisplayScreen = "lowerThird" - mainArena.audienceDisplayNotifier.Notify(nil) + web.saveLowerThird(&lowerThird) + web.arena.LowerThirdNotifier.Notify(lowerThird) + web.arena.AudienceDisplayScreen = "lowerThird" + web.arena.AudienceDisplayNotifier.Notify(nil) continue case "hideLowerThird": var lowerThird model.LowerThird @@ -107,9 +107,9 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - saveLowerThird(&lowerThird) - mainArena.audienceDisplayScreen = "blank" - mainArena.audienceDisplayNotifier.Notify(nil) + web.saveLowerThird(&lowerThird) + web.arena.AudienceDisplayScreen = "blank" + web.arena.AudienceDisplayNotifier.Notify(nil) continue case "reorderLowerThird": args := struct { @@ -121,7 +121,7 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { websocket.WriteError(err.Error()) continue } - err = reorderLowerThird(args.Id, args.MoveUp) + err = web.reorderLowerThird(args.Id, args.MoveUp) if err != nil { websocket.WriteError(err.Error()) continue @@ -140,17 +140,17 @@ func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) { } } -func saveLowerThird(lowerThird *model.LowerThird) error { - oldLowerThird, err := db.GetLowerThirdById(lowerThird.Id) +func (web *Web) saveLowerThird(lowerThird *model.LowerThird) error { + oldLowerThird, err := web.arena.Database.GetLowerThirdById(lowerThird.Id) if err != nil { return err } // Create or update lower third. if oldLowerThird == nil { - err = db.CreateLowerThird(lowerThird) + err = web.arena.Database.CreateLowerThird(lowerThird) } else { - err = db.SaveLowerThird(lowerThird) + err = web.arena.Database.SaveLowerThird(lowerThird) } if err != nil { return err @@ -158,14 +158,14 @@ func saveLowerThird(lowerThird *model.LowerThird) error { return nil } -func reorderLowerThird(id int, moveUp bool) error { - lowerThird, err := db.GetLowerThirdById(id) +func (web *Web) reorderLowerThird(id int, moveUp bool) error { + lowerThird, err := web.arena.Database.GetLowerThirdById(id) if err != nil { return err } // Get the lower third to swap positions with. - lowerThirds, err := db.GetAllLowerThirds() + lowerThirds, err := web.arena.Database.GetAllLowerThirds() if err != nil { return err } @@ -185,7 +185,7 @@ func reorderLowerThird(id int, moveUp bool) error { // The one to move is already at the limit; return an error to prevent a page reload. return fmt.Errorf("Already at the limit.") } - adjacentLowerThird, err := db.GetLowerThirdById(lowerThirds[lowerThirdIndex].Id) + adjacentLowerThird, err := web.arena.Database.GetLowerThirdById(lowerThirds[lowerThirdIndex].Id) if err != nil { return err } @@ -193,11 +193,11 @@ func reorderLowerThird(id int, moveUp bool) error { // Swap their display orders and save. lowerThird.DisplayOrder, adjacentLowerThird.DisplayOrder = adjacentLowerThird.DisplayOrder, lowerThird.DisplayOrder - err = db.SaveLowerThird(lowerThird) + err = web.arena.Database.SaveLowerThird(lowerThird) if err != nil { return err } - err = db.SaveLowerThird(adjacentLowerThird) + err = web.arena.Database.SaveLowerThird(adjacentLowerThird) if err != nil { return err } diff --git a/setup_lower_thirds_test.go b/setup_lower_thirds_test.go index bce746c..a548d48 100644 --- a/setup_lower_thirds_test.go +++ b/setup_lower_thirds_test.go @@ -13,18 +13,18 @@ import ( ) func TestSetupLowerThirds(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateLowerThird(&model.LowerThird{0, "Top Text 1", "Bottom Text 1", 0}) - db.CreateLowerThird(&model.LowerThird{0, "Top Text 2", "Bottom Text 2", 1}) - db.CreateLowerThird(&model.LowerThird{0, "Top Text 3", "Bottom Text 3", 2}) + web.arena.Database.CreateLowerThird(&model.LowerThird{0, "Top Text 1", "Bottom Text 1", 0}) + web.arena.Database.CreateLowerThird(&model.LowerThird{0, "Top Text 2", "Bottom Text 2", 1}) + web.arena.Database.CreateLowerThird(&model.LowerThird{0, "Top Text 3", "Bottom Text 3", 2}) - recorder := getHttpResponse("/setup/lower_thirds") + recorder := web.getHttpResponse("/setup/lower_thirds") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Top Text 1") assert.Contains(t, recorder.Body.String(), "Bottom Text 2") - server, wsUrl := startTestServer() + server, wsUrl := web.startTestServer() defer server.Close() conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/setup/lower_thirds/websocket", nil) assert.Nil(t, err) @@ -33,30 +33,30 @@ func TestSetupLowerThirds(t *testing.T) { ws.Write("saveLowerThird", model.LowerThird{1, "Top Text 4", "Bottom Text 1", 0}) time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed. - lowerThird, _ := db.GetLowerThirdById(1) + lowerThird, _ := web.arena.Database.GetLowerThirdById(1) assert.Equal(t, "Top Text 4", lowerThird.TopText) ws.Write("deleteLowerThird", model.LowerThird{1, "Top Text 4", "Bottom Text 1", 0}) time.Sleep(time.Millisecond * 10) - lowerThird, _ = db.GetLowerThirdById(1) + lowerThird, _ = web.arena.Database.GetLowerThirdById(1) assert.Nil(t, lowerThird) - assert.Equal(t, "blank", mainArena.audienceDisplayScreen) + assert.Equal(t, "blank", web.arena.AudienceDisplayScreen) ws.Write("showLowerThird", model.LowerThird{2, "Top Text 5", "Bottom Text 1", 0}) time.Sleep(time.Millisecond * 10) - lowerThird, _ = db.GetLowerThirdById(2) + lowerThird, _ = web.arena.Database.GetLowerThirdById(2) assert.Equal(t, "Top Text 5", lowerThird.TopText) - assert.Equal(t, "lowerThird", mainArena.audienceDisplayScreen) + assert.Equal(t, "lowerThird", web.arena.AudienceDisplayScreen) ws.Write("hideLowerThird", model.LowerThird{2, "Top Text 6", "Bottom Text 1", 0}) time.Sleep(time.Millisecond * 10) - lowerThird, _ = db.GetLowerThirdById(2) + lowerThird, _ = web.arena.Database.GetLowerThirdById(2) assert.Equal(t, "Top Text 6", lowerThird.TopText) - assert.Equal(t, "blank", mainArena.audienceDisplayScreen) + assert.Equal(t, "blank", web.arena.AudienceDisplayScreen) ws.Write("reorderLowerThird", map[string]interface{}{"Id": 2, "moveUp": false}) time.Sleep(time.Millisecond * 100) - lowerThirds, _ := db.GetAllLowerThirds() + lowerThirds, _ := web.arena.Database.GetAllLowerThirds() assert.Equal(t, 3, lowerThirds[0].Id) assert.Equal(t, 2, lowerThirds[1].Id) } diff --git a/setup_schedule.go b/setup_schedule.go index 217b907..d774d1d 100644 --- a/setup_schedule.go +++ b/setup_schedule.go @@ -22,8 +22,8 @@ var cachedMatches []model.Match var cachedTeamFirstMatches map[int]string // Shows the schedule editing page. -func ScheduleGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) scheduleGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -34,12 +34,12 @@ func ScheduleGetHandler(w http.ResponseWriter, r *http.Request) { cachedScheduleBlocks = append(cachedScheduleBlocks, tournament.ScheduleBlock{startTime, 10, 360}) cachedMatchType = "practice" } - renderSchedule(w, r, "") + web.renderSchedule(w, r, "") } // Generates the schedule and presents it for review without saving it to the database. -func ScheduleGeneratePostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) scheduleGeneratePostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -48,29 +48,29 @@ func ScheduleGeneratePostHandler(w http.ResponseWriter, r *http.Request) { scheduleBlocks, err := getScheduleBlocks(r) cachedScheduleBlocks = scheduleBlocks // Show the same blocks even if there is an error. if err != nil { - renderSchedule(w, r, "Incomplete or invalid schedule block parameters specified.") + web.renderSchedule(w, r, "Incomplete or invalid schedule block parameters specified.") return } // Build the schedule. - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return } if len(teams) == 0 { - renderSchedule(w, r, "No team list is configured. Set up the list of teams at the event before "+ + web.renderSchedule(w, r, "No team list is configured. Set up the list of teams at the event before "+ "generating the schedule.") return } if len(teams) < 18 { - renderSchedule(w, r, fmt.Sprintf("There are only %d teams. There must be at least 18 teams to generate "+ + web.renderSchedule(w, r, fmt.Sprintf("There are only %d teams. There must be at least 18 teams to generate "+ "a schedule.", len(teams))) return } matches, err := tournament.BuildRandomSchedule(teams, scheduleBlocks, r.PostFormValue("matchType")) if err != nil { - renderSchedule(w, r, fmt.Sprintf("Error generating schedule: %s.", err.Error())) + web.renderSchedule(w, r, fmt.Sprintf("Error generating schedule: %s.", err.Error())) return } cachedMatches = matches @@ -97,15 +97,15 @@ func ScheduleGeneratePostHandler(w http.ResponseWriter, r *http.Request) { } // Publishes the schedule in the database to TBA -func ScheduleRepublishPostHandler(w http.ResponseWriter, r *http.Request) { - if eventSettings.TbaPublishingEnabled { +func (web *Web) scheduleRepublishPostHandler(w http.ResponseWriter, r *http.Request) { + if web.arena.EventSettings.TbaPublishingEnabled { // Publish schedule to The Blue Alliance. - err := tbaClient.DeletePublishedMatches() + err := web.arena.TbaClient.DeletePublishedMatches() if err != nil { http.Error(w, "Failed to delete published matches: "+err.Error(), 500) return } - err = tbaClient.PublishMatches(db) + err = web.arena.TbaClient.PublishMatches(web.arena.Database) if err != nil { http.Error(w, "Failed to publish matches: "+err.Error(), 500) return @@ -119,24 +119,24 @@ func ScheduleRepublishPostHandler(w http.ResponseWriter, r *http.Request) { } // Saves the generated schedule to the database. -func ScheduleSavePostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) scheduleSavePostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - existingMatches, err := db.GetMatchesByType(cachedMatchType) + existingMatches, err := web.arena.Database.GetMatchesByType(cachedMatchType) if err != nil { handleWebErr(w, err) return } if len(existingMatches) > 0 { - renderSchedule(w, r, fmt.Sprintf("Can't save schedule because a schedule of %d %s matches already "+ + web.renderSchedule(w, r, fmt.Sprintf("Can't save schedule because a schedule of %d %s matches already "+ "exists. Clear it first on the Settings page.", len(existingMatches), cachedMatchType)) return } for _, match := range cachedMatches { - err = db.CreateMatch(&match) + err = web.arena.Database.CreateMatch(&match) if err != nil { handleWebErr(w, err) return @@ -144,20 +144,20 @@ func ScheduleSavePostHandler(w http.ResponseWriter, r *http.Request) { } // Back up the database. - err = db.Backup(eventSettings.Name, "post_scheduling") + err = web.arena.Database.Backup(web.arena.EventSettings.Name, "post_scheduling") if err != nil { handleWebErr(w, err) return } - if eventSettings.TbaPublishingEnabled && cachedMatchType != "practice" { + if web.arena.EventSettings.TbaPublishingEnabled && cachedMatchType != "practice" { // Publish schedule to The Blue Alliance. - err = tbaClient.DeletePublishedMatches() + err = web.arena.TbaClient.DeletePublishedMatches() if err != nil { http.Error(w, "Failed to delete published matches: "+err.Error(), 500) return } - err = tbaClient.PublishMatches(db) + err = web.arena.TbaClient.PublishMatches(web.arena.Database) if err != nil { http.Error(w, "Failed to publish matches: "+err.Error(), 500) return @@ -167,8 +167,8 @@ func ScheduleSavePostHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/setup/schedule", 302) } -func renderSchedule(w http.ResponseWriter, r *http.Request, errorMessage string) { - teams, err := db.GetAllTeams() +func (web *Web) renderSchedule(w http.ResponseWriter, r *http.Request, errorMessage string) { + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -186,7 +186,7 @@ func renderSchedule(w http.ResponseWriter, r *http.Request, errorMessage string) Matches []model.Match TeamFirstMatches map[int]string ErrorMessage string - }{eventSettings, cachedMatchType, cachedScheduleBlocks, len(teams), cachedMatches, cachedTeamFirstMatches, + }{web.arena.EventSettings, cachedMatchType, cachedScheduleBlocks, len(teams), cachedMatches, cachedTeamFirstMatches, errorMessage} err = template.ExecuteTemplate(w, "base", data) if err != nil { diff --git a/setup_schedule_test.go b/setup_schedule_test.go index 55d5d84..06b1d75 100644 --- a/setup_schedule_test.go +++ b/setup_schedule_test.go @@ -10,15 +10,15 @@ import ( ) func TestSetupSchedule(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) for i := 0; i < 38; i++ { - db.CreateTeam(&model.Team{Id: i + 101}) + web.arena.Database.CreateTeam(&model.Team{Id: i + 101}) } - db.CreateMatch(&model.Match{Type: "practice", DisplayName: "1"}) + web.arena.Database.CreateMatch(&model.Match{Type: "practice", DisplayName: "1"}) // Check the default setting values. - recorder := getHttpResponse("/setup/schedule") + recorder := web.getHttpResponse("/setup/schedule") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "360") // The default match spacing. @@ -26,18 +26,18 @@ func TestSetupSchedule(t *testing.T) { postData := "numScheduleBlocks=3&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" + "startTime1=2014-01-02 09:56:00 AM&numMatches1=17&matchSpacingSec1=420&startTime2=2014-01-03 01:00:00 PM&" + "numMatches2=40&matchSpacingSec2=360&matchType=qualification" - recorder = postHttpResponse("/setup/schedule/generate", postData) + recorder = web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/schedule") + recorder = web.getHttpResponse("/setup/schedule") assert.Contains(t, recorder.Body.String(), "2014-01-01 09:48:00") // Last match of first block. assert.Contains(t, recorder.Body.String(), "2014-01-02 11:48:00") // Last match of second block. assert.Contains(t, recorder.Body.String(), "2014-01-03 16:54:00") // Last match of third block. // Save schedule and check that it is published to TBA. - tbaClient.BaseUrl = "fakeUrl" - eventSettings.TbaPublishingEnabled = true - recorder = postHttpResponse("/setup/schedule/save", "") - matches, err := db.GetMatchesByType("qualification") + web.arena.TbaClient.BaseUrl = "fakeUrl" + web.arena.EventSettings.TbaPublishingEnabled = true + recorder = web.postHttpResponse("/setup/schedule/save", "") + matches, err := web.arena.Database.GetMatchesByType("qualification") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "Failed to delete published matches") assert.Nil(t, err) @@ -48,51 +48,51 @@ func TestSetupSchedule(t *testing.T) { } func TestSetupScheduleErrors(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // No teams. postData := "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" + "matchType=practice" - recorder := postHttpResponse("/setup/schedule/generate", postData) + recorder := web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "No team list is configured.") // Insufficient number of teams. for i := 0; i < 17; i++ { - db.CreateTeam(&model.Team{Id: i + 101}) + web.arena.Database.CreateTeam(&model.Team{Id: i + 101}) } postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" + "matchType=practice" - recorder = postHttpResponse("/setup/schedule/generate", postData) + recorder = web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "There must be at least 18 teams to generate a schedule.") // More matches per team than schedules exist for. - db.CreateTeam(&model.Team{Id: 118}) + web.arena.Database.CreateTeam(&model.Team{Id: 118}) postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=700&matchSpacingSec0=480&" + "matchType=practice" - recorder = postHttpResponse("/setup/schedule/generate", postData) + recorder = web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "No schedule template exists for 18 teams and 233 matches") // Incomplete scheduling data received. postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=&matchSpacingSec0=480&" + "matchType=practice" - recorder = postHttpResponse("/setup/schedule/generate", postData) + recorder = web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Incomplete or invalid schedule block parameters specified.") // Previous schedule already exists. for i := 18; i < 38; i++ { - db.CreateTeam(&model.Team{Id: i + 101}) + web.arena.Database.CreateTeam(&model.Team{Id: i + 101}) } - db.CreateMatch(&model.Match{Type: "practice", DisplayName: "1"}) - db.CreateMatch(&model.Match{Type: "practice", DisplayName: "2"}) + web.arena.Database.CreateMatch(&model.Match{Type: "practice", DisplayName: "1"}) + web.arena.Database.CreateMatch(&model.Match{Type: "practice", DisplayName: "2"}) postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=64&matchSpacingSec0=480&" + "matchType=practice" - recorder = postHttpResponse("/setup/schedule/generate", postData) + recorder = web.postHttpResponse("/setup/schedule/generate", postData) assert.Equal(t, 302, recorder.Code) - recorder = postHttpResponse("/setup/schedule/save", postData) + recorder = web.postHttpResponse("/setup/schedule/save", postData) assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "schedule of 2 practice matches already exists") } diff --git a/setup_settings.go b/setup_settings.go index cf92d07..fa651d0 100644 --- a/setup_settings.go +++ b/setup_settings.go @@ -8,7 +8,6 @@ package main import ( "fmt" "github.com/Team254/cheesy-arena/model" - "github.com/Team254/cheesy-arena/partner" "html/template" "io" "io/ioutil" @@ -21,31 +20,32 @@ import ( ) // Shows the event settings editing page. -func SettingsGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) settingsGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - renderSettings(w, r, "") + web.renderSettings(w, r, "") } // Saves the event settings. -func SettingsPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } + eventSettings := web.arena.EventSettings eventSettings.Name = r.PostFormValue("name") eventSettings.Code = r.PostFormValue("code") match, _ := regexp.MatchString("^#([0-9A-Fa-f]{3}){1,2}$", r.PostFormValue("displayBackgroundColor")) if !match { - renderSettings(w, r, "Display background color must be a valid hex color value.") + web.renderSettings(w, r, "Display background color must be a valid hex color value.") return } eventSettings.DisplayBackgroundColor = r.PostFormValue("displayBackgroundColor") numAlliances, _ := strconv.Atoi(r.PostFormValue("numElimAlliances")) if numAlliances < 2 || numAlliances > 16 { - renderSettings(w, r, "Number of alliances must be between 2 and 16.") + web.renderSettings(w, r, "Number of alliances must be between 2 and 16.") return } @@ -69,53 +69,49 @@ func SettingsPostHandler(w http.ResponseWriter, r *http.Request) { eventSettings.AdminPassword = r.PostFormValue("adminPassword") eventSettings.ReaderPassword = r.PostFormValue("readerPassword") - err := db.SaveEventSettings(eventSettings) + err := web.arena.Database.SaveEventSettings(eventSettings) if err != nil { handleWebErr(w, err) return } - // Set up the light controller connections again in case the address changed. - err = mainArena.lights.SetupConnections() + // Refresh the arena in case any of the settings changed. + err = web.arena.LoadSettings() if err != nil { handleWebErr(w, err) return } - // Refresh the partner clients in case they changed. - tbaClient = partner.NewTbaClient(eventSettings.TbaEventCode, eventSettings.TbaSecretId, eventSettings.TbaSecret) - stemTvClient = partner.NewStemTvClient(eventSettings.StemTvEventCode) - http.Redirect(w, r, "/setup/settings", 302) } // Sends a copy of the event database file to the client as a download. -func SaveDbHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) saveDbHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - dbFile, err := os.Open(db.GetPath()) + dbFile, err := os.Open(web.arena.Database.GetPath()) defer dbFile.Close() if err != nil { handleWebErr(w, err) return } - filename := fmt.Sprintf("%s-%s.db", strings.Replace(eventSettings.Name, " ", "_", -1), + filename := fmt.Sprintf("%s-%s.db", strings.Replace(web.arena.EventSettings.Name, " ", "_", -1), time.Now().Format("20060102150405")) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) http.ServeContent(w, r, "", time.Now(), dbFile) } // Accepts an event database file as an upload and loads it. -func RestoreDbHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) restoreDbHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } file, _, err := r.FormFile("databaseFile") if err != nil { - renderSettings(w, r, "No database backup file was specified.") + web.renderSettings(w, r, "No database backup file was specified.") return } @@ -134,23 +130,23 @@ func RestoreDbHandler(w http.ResponseWriter, r *http.Request) { return } tempFile.Close() - tempDb, err := model.OpenDatabase(".", tempFilePath) + tempDb, err := model.OpenDatabase(tempFilePath) if err != nil { - renderSettings(w, r, "Could not read uploaded database backup file. Please verify that it a valid "+ + web.renderSettings(w, r, "Could not read uploaded database backup file. Please verify that it a valid "+ "database file.") return } tempDb.Close() // Back up the current database. - err = db.Backup(eventSettings.Name, "pre_restore") + err = web.arena.Database.Backup(web.arena.EventSettings.Name, "pre_restore") if err != nil { handleWebErr(w, err) return } // Replace the current database with the new one. - db.Close() + web.arena.Database.Close() err = os.Remove(eventDbPath) if err != nil { handleWebErr(w, err) @@ -161,40 +157,49 @@ func RestoreDbHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, err) return } - initDb() + web.arena.Database, err = model.OpenDatabase(eventDbPath) + if err != nil { + handleWebErr(w, err) + return + } + err = web.arena.LoadSettings() + if err != nil { + handleWebErr(w, err) + return + } http.Redirect(w, r, "/setup/settings", 302) } // Deletes all data except for the team list. -func ClearDbHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) clearDbHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } // Back up the database. - err := db.Backup(eventSettings.Name, "pre_clear") + err := web.arena.Database.Backup(web.arena.EventSettings.Name, "pre_clear") if err != nil { handleWebErr(w, err) return } - err = db.TruncateMatches() + err = web.arena.Database.TruncateMatches() if err != nil { handleWebErr(w, err) return } - err = db.TruncateMatchResults() + err = web.arena.Database.TruncateMatchResults() if err != nil { handleWebErr(w, err) return } - err = db.TruncateRankings() + err = web.arena.Database.TruncateRankings() if err != nil { handleWebErr(w, err) return } - err = db.TruncateAllianceTeams() + err = web.arena.Database.TruncateAllianceTeams() if err != nil { handleWebErr(w, err) return @@ -202,7 +207,7 @@ func ClearDbHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/setup/settings", 302) } -func renderSettings(w http.ResponseWriter, r *http.Request, errorMessage string) { +func (web *Web) renderSettings(w http.ResponseWriter, r *http.Request, errorMessage string) { template, err := template.ParseFiles("templates/setup_settings.html", "templates/base.html") if err != nil { handleWebErr(w, err) @@ -211,7 +216,7 @@ func renderSettings(w http.ResponseWriter, r *http.Request, errorMessage string) data := struct { *model.EventSettings ErrorMessage string - }{eventSettings, errorMessage} + }{web.arena.EventSettings, errorMessage} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) diff --git a/setup_settings_test.go b/setup_settings_test.go index d05f2ac..7643b3a 100644 --- a/setup_settings_test.go +++ b/setup_settings_test.go @@ -18,10 +18,10 @@ import ( ) func TestSetupSettings(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Check the default setting values. - recorder := getHttpResponse("/setup/settings") + recorder := web.getHttpResponse("/setup/settings") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Untitled Event") assert.Contains(t, recorder.Body.String(), "UE") @@ -30,10 +30,10 @@ func TestSetupSettings(t *testing.T) { assert.NotContains(t, recorder.Body.String(), "tbaPublishingEnabled\" checked") // Change the settings and check the response. - recorder = postHttpResponse("/setup/settings", "name=Chezy Champs&code=CC&displayBackgroundColor=#ff00ff&"+ + recorder = web.postHttpResponse("/setup/settings", "name=Chezy Champs&code=CC&displayBackgroundColor=#ff00ff&"+ "numElimAlliances=16&tbaPublishingEnabled=on&tbaEventCode=2014cc&tbaSecretId=secretId&tbaSecret=tbasec") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/settings") + recorder = web.getHttpResponse("/setup/settings") assert.Contains(t, recorder.Body.String(), "Chezy Champs") assert.Contains(t, recorder.Body.String(), "CC") assert.Contains(t, recorder.Body.String(), "#ff00ff") @@ -45,74 +45,75 @@ func TestSetupSettings(t *testing.T) { } func TestSetupSettingsInvalidValues(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Invalid color value. - recorder := postHttpResponse("/setup/settings", "numAlliances=8&displayBackgroundColor=blorpy") + recorder := web.postHttpResponse("/setup/settings", "numAlliances=8&displayBackgroundColor=blorpy") assert.Contains(t, recorder.Body.String(), "must be a valid hex color value") // Invalid number of alliances. - recorder = postHttpResponse("/setup/settings", "numAlliances=1&displayBackgroundColor=#000") + recorder = web.postHttpResponse("/setup/settings", "numAlliances=1&displayBackgroundColor=#000") assert.Contains(t, recorder.Body.String(), "must be between 2 and 16") } func TestSetupSettingsClearDb(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateTeam(new(model.Team)) - db.CreateMatch(&model.Match{Type: "qualification"}) - db.CreateMatchResult(new(model.MatchResult)) - db.CreateRanking(new(game.Ranking)) - db.CreateAllianceTeam(new(model.AllianceTeam)) - recorder := postHttpResponse("/setup/db/clear", "") + web.arena.Database.CreateTeam(new(model.Team)) + web.arena.Database.CreateMatch(&model.Match{Type: "qualification"}) + web.arena.Database.CreateMatchResult(new(model.MatchResult)) + web.arena.Database.CreateRanking(new(game.Ranking)) + web.arena.Database.CreateAllianceTeam(new(model.AllianceTeam)) + recorder := web.postHttpResponse("/setup/db/clear", "") assert.Equal(t, 302, recorder.Code) - teams, _ := db.GetAllTeams() + teams, _ := web.arena.Database.GetAllTeams() assert.NotEmpty(t, teams) - matches, _ := db.GetMatchesByType("qualification") + matches, _ := web.arena.Database.GetMatchesByType("qualification") assert.Empty(t, matches) - rankings, _ := db.GetAllRankings() + rankings, _ := web.arena.Database.GetAllRankings() assert.Empty(t, rankings) - tournament.CalculateRankings(db) + tournament.CalculateRankings(web.arena.Database) assert.Empty(t, rankings) - alliances, _ := db.GetAllAlliances() + alliances, _ := web.arena.Database.GetAllAlliances() assert.Empty(t, alliances) } func TestSetupSettingsBackupRestoreDb(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Modify a parameter so that we know when the database has been restored. - eventSettings.Name = "Chezy Champs" - db.SaveEventSettings(eventSettings) + web.arena.EventSettings.Name = "Chezy Champs" + web.arena.Database.SaveEventSettings(web.arena.EventSettings) // Back up the database. - recorder := getHttpResponse("/setup/db/save") + recorder := web.getHttpResponse("/setup/db/save") assert.Equal(t, 200, recorder.Code) assert.Equal(t, "application/octet-stream", recorder.HeaderMap["Content-Type"][0]) backupBody := recorder.Body // Wipe the database to reset the defaults. - setupTest(t) - assert.NotEqual(t, "Chezy Champs", eventSettings.Name) + web = setupTestWeb(t) + assert.NotEqual(t, "Chezy Champs", web.arena.EventSettings.Name) // Check restoring with a missing file. - recorder = postHttpResponse("/setup/db/restore", "") + recorder = web.postHttpResponse("/setup/db/restore", "") assert.Contains(t, recorder.Body.String(), "No database backup file was specified") - assert.NotEqual(t, "Chezy Champs", eventSettings.Name) + assert.NotEqual(t, "Chezy Champs", web.arena.EventSettings.Name) // Check restoring with a corrupt file. - recorder = postFileHttpResponse("/setup/db/restore", "databaseFile", bytes.NewBufferString("invalid")) + recorder = web.postFileHttpResponse("/setup/db/restore", "databaseFile", + bytes.NewBufferString("invalid")) assert.Contains(t, recorder.Body.String(), "Could not read uploaded database backup file") - assert.NotEqual(t, "Chezy Champs", eventSettings.Name) + assert.NotEqual(t, "Chezy Champs", web.arena.EventSettings.Name) // Check restoring with the backup retrieved before. - recorder = postFileHttpResponse("/setup/db/restore", "databaseFile", backupBody) + recorder = web.postFileHttpResponse("/setup/db/restore", "databaseFile", backupBody) fmt.Println(recorder.Body.String()) - assert.Equal(t, "Chezy Champs", eventSettings.Name) + assert.Equal(t, "Chezy Champs", web.arena.EventSettings.Name) } -func postFileHttpResponse(path string, paramName string, file *bytes.Buffer) *httptest.ResponseRecorder { +func (web *Web) postFileHttpResponse(path string, paramName string, file *bytes.Buffer) *httptest.ResponseRecorder { body := new(bytes.Buffer) writer := multipart.NewWriter(body) part, _ := writer.CreateFormFile(paramName, "file.ext") @@ -121,6 +122,6 @@ func postFileHttpResponse(path string, paramName string, file *bytes.Buffer) *ht recorder := httptest.NewRecorder() req, _ := http.NewRequest("POST", path, body) req.Header.Set("Content-Type", writer.FormDataContentType()) - newHandler().ServeHTTP(recorder, req) + web.newHandler().ServeHTTP(recorder, req) return recorder } diff --git a/setup_sponsor_slides.go b/setup_sponsor_slides.go index 8f7fec8..4810baf 100644 --- a/setup_sponsor_slides.go +++ b/setup_sponsor_slides.go @@ -13,8 +13,8 @@ import ( ) // Shows the sponsor slides configuration page. -func SponsorSlidesGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) sponsorSlidesGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -23,7 +23,7 @@ func SponsorSlidesGetHandler(w http.ResponseWriter, r *http.Request) { handleWebErr(w, err) return } - sponsorSlides, err := db.GetAllSponsorSlides() + sponsorSlides, err := web.arena.Database.GetAllSponsorSlides() if err != nil { handleWebErr(w, err) return @@ -31,7 +31,7 @@ func SponsorSlidesGetHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings SponsorSlides []model.SponsorSlide - }{eventSettings, sponsorSlides} + }{web.arena.EventSettings, sponsorSlides} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -40,19 +40,19 @@ func SponsorSlidesGetHandler(w http.ResponseWriter, r *http.Request) { } // Saves the new or modified sponsor slides to the database. -func SponsorSlidesPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) sponsorSlidesPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } sponsorSlideId, _ := strconv.Atoi(r.PostFormValue("id")) - sponsorSlide, err := db.GetSponsorSlideById(sponsorSlideId) + sponsorSlide, err := web.arena.Database.GetSponsorSlideById(sponsorSlideId) if err != nil { handleWebErr(w, err) return } if r.PostFormValue("action") == "delete" { - err := db.DeleteSponsorSlide(sponsorSlide) + err := web.arena.Database.DeleteSponsorSlide(sponsorSlide) if err != nil { handleWebErr(w, err) return @@ -63,14 +63,14 @@ func SponsorSlidesPostHandler(w http.ResponseWriter, r *http.Request) { sponsorSlide = &model.SponsorSlide{Subtitle: r.PostFormValue("subtitle"), Line1: r.PostFormValue("line1"), Line2: r.PostFormValue("line2"), Image: r.PostFormValue("image"), DisplayTimeSec: displayTimeSec} - err = db.CreateSponsorSlide(sponsorSlide) + err = web.arena.Database.CreateSponsorSlide(sponsorSlide) } else { sponsorSlide.Subtitle = r.PostFormValue("subtitle") sponsorSlide.Line1 = r.PostFormValue("line1") sponsorSlide.Line2 = r.PostFormValue("line2") sponsorSlide.Image = r.PostFormValue("image") sponsorSlide.DisplayTimeSec = displayTimeSec - err = db.SaveSponsorSlide(sponsorSlide) + err = web.arena.Database.SaveSponsorSlide(sponsorSlide) } if err != nil { handleWebErr(w, err) diff --git a/setup_sponsor_slides_test.go b/setup_sponsor_slides_test.go index e79bcb5..db76899 100644 --- a/setup_sponsor_slides_test.go +++ b/setup_sponsor_slides_test.go @@ -10,37 +10,37 @@ import ( ) func TestSetupSponsorSlides(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateSponsorSlide(&model.SponsorSlide{0, "Subtitle", "Sponsor Line 1", "Sponsor Line 2", "", 10}) - db.CreateSponsorSlide(&model.SponsorSlide{0, "Subtitle", "", "", "Image.gif", 10}) + web.arena.Database.CreateSponsorSlide(&model.SponsorSlide{0, "Subtitle", "Sponsor Line 1", "Sponsor Line 2", "", 10}) + web.arena.Database.CreateSponsorSlide(&model.SponsorSlide{0, "Subtitle", "", "", "Image.gif", 10}) - recorder := getHttpResponse("/setup/sponsor_slides") + recorder := web.getHttpResponse("/setup/sponsor_slides") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Sponsor Line 1") assert.Contains(t, recorder.Body.String(), "Image.gif") - recorder = postHttpResponse("/setup/sponsor_slides", "action=delete&id=1") + recorder = web.postHttpResponse("/setup/sponsor_slides", "action=delete&id=1") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/sponsor_slides") + recorder = web.getHttpResponse("/setup/sponsor_slides") assert.Equal(t, 200, recorder.Code) assert.NotContains(t, recorder.Body.String(), "Sponsor Line 1") assert.Contains(t, recorder.Body.String(), "Image.gif") - recorder = postHttpResponse("/setup/sponsor_slides", "action=save&line2=Sponsor Line 2 revised") + recorder = web.postHttpResponse("/setup/sponsor_slides", "action=save&line2=Sponsor Line 2 revised") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/sponsor_slides") + recorder = web.getHttpResponse("/setup/sponsor_slides") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Sponsor Line 2 revised") - sponsorSlide, _ := db.GetSponsorSlideById(3) + sponsorSlide, _ := web.arena.Database.GetSponsorSlideById(3) assert.NotNil(t, sponsorSlide) - recorder = postHttpResponse("/setup/sponsor_slides", "action=save&image=Image2.gif&id=2") + recorder = web.postHttpResponse("/setup/sponsor_slides", "action=save&image=Image2.gif&id=2") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/sponsor_slides") + recorder = web.getHttpResponse("/setup/sponsor_slides") assert.Equal(t, 200, recorder.Code) assert.NotContains(t, recorder.Body.String(), "Image.gif") assert.Contains(t, recorder.Body.String(), "Image2.gif") - sponsorSlide, _ = db.GetSponsorSlideById(3) + sponsorSlide, _ = web.arena.Database.GetSponsorSlideById(3) assert.NotNil(t, sponsorSlide) } diff --git a/setup_teams.go b/setup_teams.go index b6884be..89b7fdc 100644 --- a/setup_teams.go +++ b/setup_teams.go @@ -21,22 +21,22 @@ import ( const wpaKeyLength = 8 // Shows the team list. -func TeamsGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamsGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - renderTeams(w, r, false) + web.renderTeams(w, r, false) } // Adds teams to the team list. -func TeamsPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamsPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyTeamList() { - renderTeams(w, r, true) + if !web.canModifyTeamList() { + web.renderTeams(w, r, true) return } @@ -49,12 +49,12 @@ func TeamsPostHandler(w http.ResponseWriter, r *http.Request) { } for _, teamNumber := range teamNumbers { - team, err := getOfficialTeamInfo(teamNumber) + team, err := web.getOfficialTeamInfo(teamNumber) if err != nil { handleWebErr(w, err) return } - err = db.CreateTeam(team) + err = web.arena.Database.CreateTeam(team) if err != nil { handleWebErr(w, err) return @@ -64,17 +64,17 @@ func TeamsPostHandler(w http.ResponseWriter, r *http.Request) { } // Clears the team list. -func TeamsClearHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamsClearHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyTeamList() { - renderTeams(w, r, true) + if !web.canModifyTeamList() { + web.renderTeams(w, r, true) return } - err := db.TruncateTeams() + err := web.arena.Database.TruncateTeams() if err != nil { handleWebErr(w, err) return @@ -83,14 +83,14 @@ func TeamsClearHandler(w http.ResponseWriter, r *http.Request) { } // Shows the page to edit a team's fields. -func TeamEditGetHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamEditGetHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } vars := mux.Vars(r) teamId, _ := strconv.Atoi(vars["id"]) - team, err := db.GetTeamById(teamId) + team, err := web.arena.Database.GetTeamById(teamId) if err != nil { handleWebErr(w, err) return @@ -108,7 +108,7 @@ func TeamEditGetHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings *model.Team - }{eventSettings, team} + }{web.arena.EventSettings, team} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -117,14 +117,14 @@ func TeamEditGetHandler(w http.ResponseWriter, r *http.Request) { } // Updates a team's fields. -func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamEditPostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } vars := mux.Vars(r) teamId, _ := strconv.Atoi(vars["id"]) - team, err := db.GetTeamById(teamId) + team, err := web.arena.Database.GetTeamById(teamId) if err != nil { handleWebErr(w, err) return @@ -142,14 +142,14 @@ func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) { team.RookieYear, _ = strconv.Atoi(r.PostFormValue("rookieYear")) team.RobotName = r.PostFormValue("robotName") team.Accomplishments = r.PostFormValue("accomplishments") - if eventSettings.NetworkSecurityEnabled { + if web.arena.EventSettings.NetworkSecurityEnabled { team.WpaKey = r.PostFormValue("wpaKey") if len(team.WpaKey) < 8 || len(team.WpaKey) > 63 { handleWebErr(w, fmt.Errorf("WPA key must be between 8 and 63 characters.")) return } } - err = db.SaveTeam(team) + err = web.arena.Database.SaveTeam(team) if err != nil { handleWebErr(w, err) return @@ -158,19 +158,19 @@ func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) { } // Removes a team from the team list. -func TeamDeletePostHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamDeletePostHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - if !canModifyTeamList() { - renderTeams(w, r, true) + if !web.canModifyTeamList() { + web.renderTeams(w, r, true) return } vars := mux.Vars(r) teamId, _ := strconv.Atoi(vars["id"]) - team, err := db.GetTeamById(teamId) + team, err := web.arena.Database.GetTeamById(teamId) if err != nil { handleWebErr(w, err) return @@ -179,7 +179,7 @@ func TeamDeletePostHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Error: No such team: %d", teamId), 400) return } - err = db.DeleteTeam(team) + err = web.arena.Database.DeleteTeam(team) if err != nil { handleWebErr(w, err) return @@ -188,12 +188,12 @@ func TeamDeletePostHandler(w http.ResponseWriter, r *http.Request) { } // Publishes the team list to the web. -func TeamsPublishHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamsPublishHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } - err := tbaClient.PublishTeams(db) + err := web.arena.TbaClient.PublishTeams(web.arena.Database) if err != nil { http.Error(w, "Failed to publish teams: "+err.Error(), 500) return @@ -202,8 +202,8 @@ func TeamsPublishHandler(w http.ResponseWriter, r *http.Request) { } // Generates random WPA keys and saves them to the team models. -func TeamsGenerateWpaKeysHandler(w http.ResponseWriter, r *http.Request) { - if !UserIsAdmin(w, r) { +func (web *Web) teamsGenerateWpaKeysHandler(w http.ResponseWriter, r *http.Request) { + if !web.userIsAdmin(w, r) { return } @@ -212,7 +212,7 @@ func TeamsGenerateWpaKeysHandler(w http.ResponseWriter, r *http.Request) { generateAllKeys = all[0] == "true" } - teams, err := db.GetAllTeams() + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -220,15 +220,15 @@ func TeamsGenerateWpaKeysHandler(w http.ResponseWriter, r *http.Request) { for _, team := range teams { if len(team.WpaKey) == 0 || generateAllKeys { team.WpaKey = uniuri.NewLen(wpaKeyLength) - db.SaveTeam(&team) + web.arena.Database.SaveTeam(&team) } } http.Redirect(w, r, "/setup/teams", 302) } -func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) { - teams, err := db.GetAllTeams() +func (web *Web) renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) { + teams, err := web.arena.Database.GetAllTeams() if err != nil { handleWebErr(w, err) return @@ -243,7 +243,7 @@ func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) *model.EventSettings Teams []model.Team ShowErrorMessage bool - }{eventSettings, teams, showErrorMessage} + }{web.arena.EventSettings, teams, showErrorMessage} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -252,8 +252,8 @@ func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) } // Returns true if it is safe to change the team list (i.e. no matches/results exist yet). -func canModifyTeamList() bool { - matches, err := db.GetMatchesByType("qualification") +func (web *Web) canModifyTeamList() bool { + matches, err := web.arena.Database.GetMatchesByType("qualification") if err != nil || len(matches) > 0 { return false } @@ -261,13 +261,13 @@ func canModifyTeamList() bool { } // Returns the data for the given team number. -func getOfficialTeamInfo(teamId int) (*model.Team, error) { +func (web *Web) getOfficialTeamInfo(teamId int) (*model.Team, error) { // Create the team variable that stores the result var team model.Team // If team info download is enabled, download the current teams data (caching isn't easy with the new paging system in the api) - if eventSettings.TBADownloadEnabled { - tbaTeam, err := tbaClient.GetTeam(teamId) + if web.arena.EventSettings.TBADownloadEnabled { + tbaTeam, err := web.arena.TbaClient.GetTeam(teamId) if err != nil { return nil, err } @@ -276,12 +276,12 @@ func getOfficialTeamInfo(teamId int) (*model.Team, error) { if tbaTeam.TeamNumber == 0 { team = model.Team{Id: teamId} } else { - robotName, err := tbaClient.GetRobotName(teamId, time.Now().Year()) + robotName, err := web.arena.TbaClient.GetRobotName(teamId, time.Now().Year()) if err != nil { return nil, err } - recentAwards, err := tbaClient.GetTeamAwards(teamId) + recentAwards, err := web.arena.TbaClient.GetTeamAwards(teamId) if err != nil { return nil, err } diff --git a/setup_teams_test.go b/setup_teams_test.go index 79e106e..2855f64 100644 --- a/setup_teams_test.go +++ b/setup_teams_test.go @@ -14,10 +14,10 @@ import ( ) func TestSetupTeams(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) // Check that there are no teams to start. - recorder := getHttpResponse("/setup/teams") + recorder := web.getHttpResponse("/setup/teams") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "0 teams") @@ -81,131 +81,131 @@ func TestSetupTeams(t *testing.T) { } })) defer tbaServer.Close() - tbaClient.BaseUrl = tbaServer.URL + web.arena.TbaClient.BaseUrl = tbaServer.URL // Add some teams. - recorder = postHttpResponse("/setup/teams", "teamNumbers=254\r\nnotateam\r\n1114\r\n") + recorder = web.postHttpResponse("/setup/teams", "teamNumbers=254\r\nnotateam\r\n1114\r\n") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.Contains(t, recorder.Body.String(), "2 teams") assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs") assert.Contains(t, recorder.Body.String(), "1114") // Add another team. - recorder = postHttpResponse("/setup/teams", "teamNumbers=33") + recorder = web.postHttpResponse("/setup/teams", "teamNumbers=33") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.Contains(t, recorder.Body.String(), "3 teams") assert.Contains(t, recorder.Body.String(), "33") // Edit a team. - recorder = getHttpResponse("/setup/teams/254/edit") + recorder = web.getHttpResponse("/setup/teams/254/edit") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs") - recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs") + recorder = web.postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs") // Delete a team. - recorder = postHttpResponse("/setup/teams/1114/delete", "") + recorder = web.postHttpResponse("/setup/teams/1114/delete", "") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.Contains(t, recorder.Body.String(), "2 teams") // Test clearing all teams. - recorder = postHttpResponse("/setup/teams/clear", "") + recorder = web.postHttpResponse("/setup/teams/clear", "") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.Contains(t, recorder.Body.String(), "0 teams") } func TestSetupTeamsDisallowModification(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - db.CreateTeam(&model.Team{Id: 254, Nickname: "The Cheesy Poofs"}) - db.CreateMatch(&model.Match{Type: "qualification"}) + web.arena.Database.CreateTeam(&model.Team{Id: 254, Nickname: "The Cheesy Poofs"}) + web.arena.Database.CreateMatch(&model.Match{Type: "qualification"}) // Disallow adding teams. - recorder := postHttpResponse("/setup/teams", "teamNumbers=33") + recorder := web.postHttpResponse("/setup/teams", "teamNumbers=33") assert.Contains(t, recorder.Body.String(), "can't modify") assert.Contains(t, recorder.Body.String(), "1 teams") assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs") // Disallow deleting team. - recorder = postHttpResponse("/setup/teams/254/delete", "") + recorder = web.postHttpResponse("/setup/teams/254/delete", "") assert.Contains(t, recorder.Body.String(), "can't modify") assert.Contains(t, recorder.Body.String(), "1 teams") assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs") // Disallow clearing all teams. - recorder = postHttpResponse("/setup/teams/clear", "") + recorder = web.postHttpResponse("/setup/teams/clear", "") assert.Contains(t, recorder.Body.String(), "can't modify") assert.Contains(t, recorder.Body.String(), "1 teams") assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs") // Allow editing a team. - recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs") + recorder = web.postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs") assert.Equal(t, 302, recorder.Code) - recorder = getHttpResponse("/setup/teams") + recorder = web.getHttpResponse("/setup/teams") assert.NotContains(t, recorder.Body.String(), "can't modify") assert.Contains(t, recorder.Body.String(), "1 teams") assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs") } func TestSetupTeamsBadReqest(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/setup/teams/254/edit") + recorder := web.getHttpResponse("/setup/teams/254/edit") assert.Equal(t, 400, recorder.Code) assert.Contains(t, recorder.Body.String(), "No such team") - recorder = postHttpResponse("/setup/teams/254/edit", "") + recorder = web.postHttpResponse("/setup/teams/254/edit", "") assert.Equal(t, 400, recorder.Code) assert.Contains(t, recorder.Body.String(), "No such team") - recorder = postHttpResponse("/setup/teams/254/delete", "") + recorder = web.postHttpResponse("/setup/teams/254/delete", "") assert.Equal(t, 400, recorder.Code) assert.Contains(t, recorder.Body.String(), "No such team") } func TestSetupTeamsWpaKeys(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - eventSettings.NetworkSecurityEnabled = true + web.arena.EventSettings.NetworkSecurityEnabled = true team1 := &model.Team{Id: 254, WpaKey: "aaaaaaaa"} team2 := &model.Team{Id: 1114} - db.CreateTeam(team1) - db.CreateTeam(team2) + web.arena.Database.CreateTeam(team1) + web.arena.Database.CreateTeam(team2) - recorder := getHttpResponse("/setup/teams/generate_wpa_keys?all=false") + recorder := web.getHttpResponse("/setup/teams/generate_wpa_keys?all=false") assert.Equal(t, 302, recorder.Code) - team1, _ = db.GetTeamById(254) - team2, _ = db.GetTeamById(1114) + team1, _ = web.arena.Database.GetTeamById(254) + team2, _ = web.arena.Database.GetTeamById(1114) assert.Equal(t, "aaaaaaaa", team1.WpaKey) assert.Equal(t, 8, len(team2.WpaKey)) - recorder = getHttpResponse("/setup/teams/generate_wpa_keys?all=true") + recorder = web.getHttpResponse("/setup/teams/generate_wpa_keys?all=true") assert.Equal(t, 302, recorder.Code) - team1, _ = db.GetTeamById(254) - team3, _ := db.GetTeamById(1114) + team1, _ = web.arena.Database.GetTeamById(254) + team3, _ := web.arena.Database.GetTeamById(1114) assert.NotEqual(t, "aaaaaaaa", team1.WpaKey) assert.Equal(t, 8, len(team1.WpaKey)) assert.NotEqual(t, team2.WpaKey, team3.WpaKey) assert.Equal(t, 8, len(team3.WpaKey)) // Disallow invalid manual WPA keys. - recorder = postHttpResponse("/setup/teams/254/edit", "wpa_key=1234567") + recorder = web.postHttpResponse("/setup/teams/254/edit", "wpa_key=1234567") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "WPA key must be between 8 and 63 characters") } func TestSetupTeamsPublish(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - tbaClient.BaseUrl = "fakeurl" - eventSettings.TbaPublishingEnabled = true + web.arena.TbaClient.BaseUrl = "fakeurl" + web.arena.EventSettings.TbaPublishingEnabled = true - recorder := postHttpResponse("/setup/teams/publish", "") + recorder := web.postHttpResponse("/setup/teams/publish", "") assert.Equal(t, 500, recorder.Code) assert.Contains(t, recorder.Body.String(), "Failed to publish teams") } diff --git a/test_helpers.go b/test_helpers.go index 747e0e5..540c8f2 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -6,18 +6,11 @@ package main import ( - "github.com/Team254/cheesy-arena/model" - "github.com/Team254/cheesy-arena/partner" - "github.com/stretchr/testify/assert" + "github.com/Team254/cheesy-arena/field" "testing" ) -func setupTest(t *testing.T) { - db = model.SetupTestDb(t, "main", ".") - var err error - eventSettings, err = db.GetEventSettings() - assert.Nil(t, err) - tbaClient = partner.NewTbaClient(eventSettings.TbaEventCode, eventSettings.TbaSecretId, eventSettings.TbaSecret) - stemTvClient = partner.NewStemTvClient(eventSettings.StemTvEventCode) - mainArena.Setup() +func setupTestWeb(t *testing.T) *Web { + arena := field.SetupTestArena(t, "web") + return NewWeb(arena) } diff --git a/tournament/test_helpers.go b/tournament/test_helpers.go index 157f6e5..dcbd37e 100644 --- a/tournament/test_helpers.go +++ b/tournament/test_helpers.go @@ -19,5 +19,5 @@ func CreateTestAlliances(database *model.Database, allianceCount int) { } func setupTestDb(t *testing.T) *model.Database { - return model.SetupTestDb(t, "tournament", "..") + return model.SetupTestDb(t, "tournament") } diff --git a/web.go b/web.go index a54b36d..2d9f28d 100644 --- a/web.go +++ b/web.go @@ -8,92 +8,55 @@ package main import ( "bitbucket.org/rj/httpauth-go" "fmt" + "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/model" "github.com/gorilla/mux" - "github.com/gorilla/websocket" "log" "net/http" - "sync" "text/template" ) -const httpPort = 8080 -const adminUser = "admin" -const readerUser = "reader" +const ( + adminUser = "admin" + readerUser = "reader" +) -var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014} -var adminAuth = httpauth.NewBasic("Cheesy Arena", checkAdminPassword, nil) -var readerAuth = httpauth.NewBasic("Cheesy Arena", checkReaderPassword, nil) +type Web struct { + arena *field.Arena + adminAuth *httpauth.Basic + readerAuth *httpauth.Basic + templateHelpers template.FuncMap +} -// Helper functions that can be used inside templates. -var templateHelpers = template.FuncMap{ - // Allows sub-templates to be invoked with multiple arguments. - "dict": func(values ...interface{}) (map[string]interface{}, error) { - if len(values)%2 != 0 { - return nil, fmt.Errorf("Invalid dict call.") - } - dict := make(map[string]interface{}, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, fmt.Errorf("Dict keys must be strings.") +func NewWeb(arena *field.Arena) *Web { + web := &Web{arena: arena} + web.adminAuth = httpauth.NewBasic("Cheesy Arena", web.checkAdminPassword, nil) + web.readerAuth = httpauth.NewBasic("Cheesy Arena", web.checkReaderPassword, nil) + + // Helper functions that can be used inside templates. + web.templateHelpers = template.FuncMap{ + // Allows sub-templates to be invoked with multiple arguments. + "dict": func(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, fmt.Errorf("Invalid dict call.") } - dict[key] = values[i+1] - } - return dict, nil - }, -} - -// Wraps the Gorilla Websocket module so that we can define additional functions on it. -type Websocket struct { - conn *websocket.Conn - writeMutex *sync.Mutex -} - -type WebsocketMessage struct { - Type string `json:"type"` - Data interface{} `json:"data"` -} - -// Upgrades the given HTTP request to a websocket connection. -func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) { - conn, err := websocketUpgrader.Upgrade(w, r, nil) - if err != nil { - return nil, err + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, fmt.Errorf("Dict keys must be strings.") + } + dict[key] = values[i+1] + } + return dict, nil + }, } - return &Websocket{conn, new(sync.Mutex)}, nil -} -func (websocket *Websocket) Close() { - websocket.conn.Close() -} - -func (websocket *Websocket) Read() (string, interface{}, error) { - var message WebsocketMessage - err := websocket.conn.ReadJSON(&message) - return message.Type, message.Data, err -} - -func (websocket *Websocket) Write(messageType string, data interface{}) error { - websocket.writeMutex.Lock() - defer websocket.writeMutex.Unlock() - return websocket.conn.WriteJSON(WebsocketMessage{messageType, data}) -} - -func (websocket *Websocket) WriteError(errorMessage string) error { - websocket.writeMutex.Lock() - defer websocket.writeMutex.Unlock() - return websocket.conn.WriteJSON(WebsocketMessage{"error", errorMessage}) -} - -func (websocket *Websocket) ShowDialog(message string) error { - websocket.writeMutex.Lock() - defer websocket.writeMutex.Unlock() - return websocket.conn.WriteJSON(WebsocketMessage{"dialog", message}) + return web } // Serves the root page of Cheesy Arena. -func IndexHandler(w http.ResponseWriter, r *http.Request) { +func (web *Web) indexHandler(w http.ResponseWriter, r *http.Request) { template, err := template.ParseFiles("templates/index.html", "templates/base.html") if err != nil { handleWebErr(w, err) @@ -101,7 +64,7 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { } data := struct { *model.EventSettings - }{eventSettings} + }{web.arena.EventSettings} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -110,119 +73,118 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { } // Starts the webserver and blocks, waiting on requests. Does not return until the application exits. -func ServeWebInterface() { +func (web *Web) ServeWebInterface(port int) { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) - http.Handle("/", newHandler()) - log.Printf("Serving HTTP requests on port %d", httpPort) + http.Handle("/", web.newHandler()) + log.Printf("Serving HTTP requests on port %d", port) // Start Server - http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil) + http.ListenAndServe(fmt.Sprintf(":%d", port), nil) } // Returns true if the given user is authorized for admin operations. Used for HTTP Basic Auth. -func UserIsAdmin(w http.ResponseWriter, r *http.Request) bool { - if eventSettings.AdminPassword == "" { +func (web *Web) userIsAdmin(w http.ResponseWriter, r *http.Request) bool { + if web.arena.EventSettings.AdminPassword == "" { // Disable auth if there is no password configured. return true } - if adminAuth.Authorize(r) == "" { - adminAuth.NotifyAuthRequired(w, r) + if web.adminAuth.Authorize(r) == "" { + web.adminAuth.NotifyAuthRequired(w, r) return false } return true } // Returns true if the given user is authorized for read-only operations. Used for HTTP Basic Auth. -func UserIsReader(w http.ResponseWriter, r *http.Request) bool { - if eventSettings.ReaderPassword == "" { +func (web *Web) userIsReader(w http.ResponseWriter, r *http.Request) bool { + if web.arena.EventSettings.ReaderPassword == "" { // Disable auth if there is no password configured. return true } - if readerAuth.Authorize(r) == "" { - readerAuth.NotifyAuthRequired(w, r) + if web.readerAuth.Authorize(r) == "" { + web.readerAuth.NotifyAuthRequired(w, r) return false } return true } -func checkAdminPassword(user, password string) bool { - return user == adminUser && password == eventSettings.AdminPassword +func (web *Web) checkAdminPassword(user, password string) bool { + return user == adminUser && password == web.arena.EventSettings.AdminPassword } -func checkReaderPassword(user, password string) bool { +func (web *Web) checkReaderPassword(user, password string) bool { if user == readerUser { - return password == eventSettings.ReaderPassword + return password == web.arena.EventSettings.ReaderPassword } // The admin role also has read permissions. - return checkAdminPassword(user, password) + return web.checkAdminPassword(user, password) } // Sets up the mapping between URLs and handlers. -func newHandler() http.Handler { +func (web *Web) newHandler() http.Handler { router := mux.NewRouter() - router.HandleFunc("/setup/settings", SettingsGetHandler).Methods("GET") - router.HandleFunc("/setup/settings", SettingsPostHandler).Methods("POST") - router.HandleFunc("/setup/db/save", SaveDbHandler).Methods("GET") - router.HandleFunc("/setup/db/restore", RestoreDbHandler).Methods("POST") - router.HandleFunc("/setup/db/clear", ClearDbHandler).Methods("POST") - router.HandleFunc("/setup/teams", TeamsGetHandler).Methods("GET") - router.HandleFunc("/setup/teams", TeamsPostHandler).Methods("POST") - router.HandleFunc("/setup/teams/clear", TeamsClearHandler).Methods("POST") - router.HandleFunc("/setup/teams/{id}/edit", TeamEditGetHandler).Methods("GET") - router.HandleFunc("/setup/teams/{id}/edit", TeamEditPostHandler).Methods("POST") - router.HandleFunc("/setup/teams/{id}/delete", TeamDeletePostHandler).Methods("POST") - router.HandleFunc("/setup/teams/publish", TeamsPublishHandler).Methods("POST") - router.HandleFunc("/setup/teams/generate_wpa_keys", TeamsGenerateWpaKeysHandler).Methods("GET") - router.HandleFunc("/setup/schedule", ScheduleGetHandler).Methods("GET") - router.HandleFunc("/setup/schedule/generate", ScheduleGeneratePostHandler).Methods("POST") - router.HandleFunc("/setup/schedule/republish", ScheduleRepublishPostHandler).Methods("POST") - router.HandleFunc("/setup/schedule/save", ScheduleSavePostHandler).Methods("POST") - router.HandleFunc("/setup/alliance_selection", AllianceSelectionGetHandler).Methods("GET") - router.HandleFunc("/setup/alliance_selection", AllianceSelectionPostHandler).Methods("POST") - router.HandleFunc("/setup/alliance_selection/start", AllianceSelectionStartHandler).Methods("POST") - router.HandleFunc("/setup/alliance_selection/reset", AllianceSelectionResetHandler).Methods("POST") - router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST") - router.HandleFunc("/setup/field", FieldGetHandler).Methods("GET") - router.HandleFunc("/setup/field", FieldPostHandler).Methods("POST") - router.HandleFunc("/setup/field/reload_displays", FieldReloadDisplaysHandler).Methods("GET") - router.HandleFunc("/setup/field/lights", FieldLightsPostHandler).Methods("POST") - router.HandleFunc("/setup/lower_thirds", LowerThirdsGetHandler).Methods("GET") - router.HandleFunc("/setup/lower_thirds/websocket", LowerThirdsWebsocketHandler).Methods("GET") - router.HandleFunc("/setup/sponsor_slides", SponsorSlidesGetHandler).Methods("GET") - router.HandleFunc("/setup/sponsor_slides", SponsorSlidesPostHandler).Methods("POST") - router.HandleFunc("/api/sponsor_slides", SponsorSlidesApiHandler).Methods("GET") - router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET") - router.HandleFunc("/match_play/{matchId}/load", MatchPlayLoadHandler).Methods("GET") - router.HandleFunc("/match_play/{matchId}/show_result", MatchPlayShowResultHandler).Methods("GET") - router.HandleFunc("/match_play/websocket", MatchPlayWebsocketHandler).Methods("GET") - router.HandleFunc("/match_review", MatchReviewHandler).Methods("GET") - router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditGetHandler).Methods("GET") - router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditPostHandler).Methods("POST") - router.HandleFunc("/reports/csv/rankings", RankingsCsvReportHandler).Methods("GET") - router.HandleFunc("/reports/pdf/rankings", RankingsPdfReportHandler).Methods("GET") - router.HandleFunc("/reports/csv/schedule/{type}", ScheduleCsvReportHandler).Methods("GET") - router.HandleFunc("/reports/pdf/schedule/{type}", SchedulePdfReportHandler).Methods("GET") - router.HandleFunc("/reports/csv/teams", TeamsCsvReportHandler).Methods("GET") - router.HandleFunc("/reports/pdf/teams", TeamsPdfReportHandler).Methods("GET") - router.HandleFunc("/reports/csv/wpa_keys", WpaKeysCsvReportHandler).Methods("GET") - router.HandleFunc("/displays/audience", AudienceDisplayHandler).Methods("GET") - router.HandleFunc("/displays/audience/websocket", AudienceDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/pit", PitDisplayHandler).Methods("GET") - router.HandleFunc("/displays/pit/websocket", PitDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/announcer", AnnouncerDisplayHandler).Methods("GET") - router.HandleFunc("/displays/announcer/websocket", AnnouncerDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/scoring/{alliance}", ScoringDisplayHandler).Methods("GET") - router.HandleFunc("/displays/scoring/{alliance}/websocket", ScoringDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/referee", RefereeDisplayHandler).Methods("GET") - router.HandleFunc("/displays/referee/websocket", RefereeDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/alliance_station", AllianceStationDisplayHandler).Methods("GET") - router.HandleFunc("/displays/alliance_station/websocket", AllianceStationDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/displays/fta", FtaDisplayHandler).Methods("GET") - router.HandleFunc("/displays/fta/websocket", FtaDisplayWebsocketHandler).Methods("GET") - router.HandleFunc("/api/matches/{type}", MatchesApiHandler).Methods("GET") - router.HandleFunc("/api/rankings", RankingsApiHandler).Methods("GET") - router.HandleFunc("/", IndexHandler).Methods("GET") + router.HandleFunc("/setup/settings", web.settingsGetHandler).Methods("GET") + router.HandleFunc("/setup/settings", web.settingsPostHandler).Methods("POST") + router.HandleFunc("/setup/db/save", web.saveDbHandler).Methods("GET") + router.HandleFunc("/setup/db/restore", web.restoreDbHandler).Methods("POST") + router.HandleFunc("/setup/db/clear", web.clearDbHandler).Methods("POST") + router.HandleFunc("/setup/teams", web.teamsGetHandler).Methods("GET") + router.HandleFunc("/setup/teams", web.teamsPostHandler).Methods("POST") + router.HandleFunc("/setup/teams/clear", web.teamsClearHandler).Methods("POST") + router.HandleFunc("/setup/teams/{id}/edit", web.teamEditGetHandler).Methods("GET") + router.HandleFunc("/setup/teams/{id}/edit", web.teamEditPostHandler).Methods("POST") + router.HandleFunc("/setup/teams/{id}/delete", web.teamDeletePostHandler).Methods("POST") + router.HandleFunc("/setup/teams/publish", web.teamsPublishHandler).Methods("POST") + router.HandleFunc("/setup/teams/generate_wpa_keys", web.teamsGenerateWpaKeysHandler).Methods("GET") + router.HandleFunc("/setup/schedule", web.scheduleGetHandler).Methods("GET") + router.HandleFunc("/setup/schedule/generate", web.scheduleGeneratePostHandler).Methods("POST") + router.HandleFunc("/setup/schedule/republish", web.scheduleRepublishPostHandler).Methods("POST") + router.HandleFunc("/setup/schedule/save", web.scheduleSavePostHandler).Methods("POST") + router.HandleFunc("/setup/alliance_selection", web.allianceSelectionGetHandler).Methods("GET") + router.HandleFunc("/setup/alliance_selection", web.allianceSelectionPostHandler).Methods("POST") + router.HandleFunc("/setup/alliance_selection/start", web.allianceSelectionStartHandler).Methods("POST") + router.HandleFunc("/setup/alliance_selection/reset", web.allianceSelectionResetHandler).Methods("POST") + router.HandleFunc("/setup/alliance_selection/finalize", web.allianceSelectionFinalizeHandler).Methods("POST") + router.HandleFunc("/setup/field", web.fieldGetHandler).Methods("GET") + router.HandleFunc("/setup/field", web.fieldPostHandler).Methods("POST") + router.HandleFunc("/setup/field/reload_displays", web.fieldReloadDisplaysHandler).Methods("GET") + router.HandleFunc("/setup/lower_thirds", web.lowerThirdsGetHandler).Methods("GET") + router.HandleFunc("/setup/lower_thirds/websocket", web.lowerThirdsWebsocketHandler).Methods("GET") + router.HandleFunc("/setup/sponsor_slides", web.sponsorSlidesGetHandler).Methods("GET") + router.HandleFunc("/setup/sponsor_slides", web.sponsorSlidesPostHandler).Methods("POST") + router.HandleFunc("/api/matches/{type}", web.matchesApiHandler).Methods("GET") + router.HandleFunc("/api/rankings", web.rankingsApiHandler).Methods("GET") + router.HandleFunc("/api/sponsor_slides", web.sponsorSlidesApiHandler).Methods("GET") + router.HandleFunc("/match_play", web.matchPlayHandler).Methods("GET") + router.HandleFunc("/match_play/{matchId}/load", web.matchPlayLoadHandler).Methods("GET") + router.HandleFunc("/match_play/{matchId}/show_result", web.matchPlayShowResultHandler).Methods("GET") + router.HandleFunc("/match_play/websocket", web.matchPlayWebsocketHandler).Methods("GET") + router.HandleFunc("/match_review", web.matchReviewHandler).Methods("GET") + router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditGetHandler).Methods("GET") + router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditPostHandler).Methods("POST") + router.HandleFunc("/reports/csv/rankings", web.rankingsCsvReportHandler).Methods("GET") + router.HandleFunc("/reports/pdf/rankings", web.rankingsPdfReportHandler).Methods("GET") + router.HandleFunc("/reports/csv/schedule/{type}", web.scheduleCsvReportHandler).Methods("GET") + router.HandleFunc("/reports/pdf/schedule/{type}", web.schedulePdfReportHandler).Methods("GET") + router.HandleFunc("/reports/csv/teams", web.teamsCsvReportHandler).Methods("GET") + router.HandleFunc("/reports/pdf/teams", web.teamsPdfReportHandler).Methods("GET") + router.HandleFunc("/reports/csv/wpa_keys", web.wpaKeysCsvReportHandler).Methods("GET") + router.HandleFunc("/displays/audience", web.audienceDisplayHandler).Methods("GET") + router.HandleFunc("/displays/audience/websocket", web.audienceDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/pit", web.pitDisplayHandler).Methods("GET") + router.HandleFunc("/displays/pit/websocket", web.pitDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/announcer", web.announcerDisplayHandler).Methods("GET") + router.HandleFunc("/displays/announcer/websocket", web.announcerDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/scoring/{alliance}", web.scoringDisplayHandler).Methods("GET") + router.HandleFunc("/displays/scoring/{alliance}/websocket", web.scoringDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/referee", web.refereeDisplayHandler).Methods("GET") + router.HandleFunc("/displays/referee/websocket", web.refereeDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/alliance_station", web.allianceStationDisplayHandler).Methods("GET") + router.HandleFunc("/displays/alliance_station/websocket", web.allianceStationDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/fta", web.ftaDisplayHandler).Methods("GET") + router.HandleFunc("/displays/fta/websocket", web.ftaDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/", web.indexHandler).Methods("GET") return router } diff --git a/web_test.go b/web_test.go index 7f89482..13ab091 100644 --- a/web_test.go +++ b/web_test.go @@ -12,31 +12,31 @@ import ( ) func TestIndex(t *testing.T) { - setupTest(t) + web := setupTestWeb(t) - recorder := getHttpResponse("/") + recorder := web.getHttpResponse("/") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "Home - Untitled Event - Cheesy Arena") } -func getHttpResponse(path string) *httptest.ResponseRecorder { +func (web *Web) getHttpResponse(path string) *httptest.ResponseRecorder { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", path, nil) - newHandler().ServeHTTP(recorder, req) + web.newHandler().ServeHTTP(recorder, req) return recorder } -func postHttpResponse(path string, body string) *httptest.ResponseRecorder { +func (web *Web) postHttpResponse(path string, body string) *httptest.ResponseRecorder { recorder := httptest.NewRecorder() req, _ := http.NewRequest("POST", path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") - newHandler().ServeHTTP(recorder, req) + web.newHandler().ServeHTTP(recorder, req) return recorder } // Starts a real local HTTP server that can be used by more sophisticated tests. -func startTestServer() (*httptest.Server, string) { - server := httptest.NewServer(newHandler()) +func (web *Web) startTestServer() (*httptest.Server, string) { + server := httptest.NewServer(web.newHandler()) return server, "ws" + server.URL[len("http"):] } diff --git a/websocket.go b/websocket.go new file mode 100644 index 0000000..ef9425f --- /dev/null +++ b/websocket.go @@ -0,0 +1,62 @@ +// Copyright 2014 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Functions for the server side of handling websockets. + +package main + +import ( + "github.com/gorilla/websocket" + "net/http" + "sync" +) + +// Wraps the Gorilla Websocket module so that we can define additional functions on it. +type Websocket struct { + conn *websocket.Conn + writeMutex *sync.Mutex +} + +type WebsocketMessage struct { + Type string `json:"type"` + Data interface{} `json:"data"` +} + +var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014} + +// Upgrades the given HTTP request to a websocket connection. +func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) { + conn, err := websocketUpgrader.Upgrade(w, r, nil) + if err != nil { + return nil, err + } + return &Websocket{conn, new(sync.Mutex)}, nil +} + +func (websocket *Websocket) Close() { + websocket.conn.Close() +} + +func (websocket *Websocket) Read() (string, interface{}, error) { + var message WebsocketMessage + err := websocket.conn.ReadJSON(&message) + return message.Type, message.Data, err +} + +func (websocket *Websocket) Write(messageType string, data interface{}) error { + websocket.writeMutex.Lock() + defer websocket.writeMutex.Unlock() + return websocket.conn.WriteJSON(WebsocketMessage{messageType, data}) +} + +func (websocket *Websocket) WriteError(errorMessage string) error { + websocket.writeMutex.Lock() + defer websocket.writeMutex.Unlock() + return websocket.conn.WriteJSON(WebsocketMessage{"error", errorMessage}) +} + +func (websocket *Websocket) ShowDialog(message string) error { + websocket.writeMutex.Lock() + defer websocket.writeMutex.Unlock() + return websocket.conn.WriteJSON(WebsocketMessage{"dialog", message}) +}