From 14c981598078fad29e89749d8c02ff7c7b87c761 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 28 Mar 2020 18:32:46 -0700 Subject: [PATCH] Calculate event lateness periodically to improve accuracy. --- field/arena.go | 80 +++++++++++++++++++++++++++ field/arena_notifiers.go | 6 ++ field/arena_test.go | 97 +++++++++++++++++++++++++++++++++ static/css/pit_display.css | 8 +++ static/css/queueing_display.css | 2 +- static/js/match_play.js | 8 ++- static/js/pit_display.js | 9 ++- static/js/queueing_display.js | 8 ++- templates/match_play.html | 3 + templates/pit_display.html | 1 + templates/queueing_display.html | 2 +- web/match_play.go | 2 +- web/match_play_test.go | 2 + web/pit_display.go | 3 +- web/pit_display_test.go | 1 + web/queueing_display.go | 44 ++------------- web/queueing_display_test.go | 57 +------------------ 17 files changed, 231 insertions(+), 102 deletions(-) diff --git a/field/arena.go b/field/arena.go index 2f1145c..5934908 100644 --- a/field/arena.go +++ b/field/arena.go @@ -13,15 +13,19 @@ import ( "github.com/Team254/cheesy-arena/partner" "github.com/Team254/cheesy-arena/plc" "log" + "math" "time" ) const ( arenaLoopPeriodMs = 10 dsPacketPeriodMs = 250 + periodicTaskPeriodSec = 30 matchEndScoreDwellSec = 3 postTimeoutSec = 4 preLoadNextMatchDelaySec = 5 + earlyLateThresholdMin = 2.5 + MaxMatchGapMin = 20 ) // Progression of match states. @@ -59,6 +63,8 @@ type Arena struct { RedRealtimeScore *RealtimeScore BlueRealtimeScore *RealtimeScore lastDsPacketTime time.Time + lastPeriodicTaskTime time.Time + EventStatusMessage string FieldVolunteers bool FieldReset bool AudienceDisplayMode string @@ -512,6 +518,10 @@ func (arena *Arena) Run() { for { arena.Update() + if time.Since(arena.lastPeriodicTaskTime).Seconds() >= periodicTaskPeriodSec { + arena.lastPeriodicTaskTime = time.Now() + go arena.runPeriodicTasks() + } time.Sleep(time.Millisecond * arenaLoopPeriodMs) } } @@ -854,3 +864,73 @@ func (arena *Arena) alliancePostMatchScoreReady(alliance string) bool { numPanels := arena.ScoringPanelRegistry.GetNumPanels(alliance) return numPanels > 0 && arena.ScoringPanelRegistry.GetNumScoreCommitted(alliance) >= numPanels } + +// Performs any actions that need to run at the interval specified by periodicTaskPeriodSec. +func (arena *Arena) runPeriodicTasks() { + // Check how early or late the event is running and publish an update to the displays that show it. + newEventStatusMessage := arena.getEventStatusMessage() + if newEventStatusMessage != arena.EventStatusMessage { + arena.EventStatusMessage = newEventStatusMessage + arena.EventStatusNotifier.Notify() + } +} + +// Updates the string that indicates how early or late the event is running. +func (arena *Arena) getEventStatusMessage() string { + currentMatch := arena.CurrentMatch + if currentMatch.Type != "practice" && currentMatch.Type != "qualification" { + // Only practice and qualification matches have a strict schedule. + return "" + } + if currentMatch.Status == "complete" { + // This is a replay or otherwise unpredictable situation. + return "" + } + + var minutesLate float64 + if arena.MatchState > PreMatch && arena.MatchState < PostMatch { + // The match is in progress; simply calculate lateness from its start time. + minutesLate = currentMatch.StartedAt.Sub(currentMatch.Time).Minutes() + } else { + // We need to check the adjacent matches to accurately determine lateness. + matches, _ := arena.Database.GetMatchesByType(currentMatch.Type) + + previousMatchIndex := -1 + nextMatchIndex := len(matches) + for i, match := range matches { + if match.Id == currentMatch.Id { + previousMatchIndex = i - 1 + nextMatchIndex = i + 1 + break + } + } + + if arena.MatchState == PreMatch { + currentMinutesLate := time.Now().Sub(currentMatch.Time).Minutes() + if previousMatchIndex >= 0 && + currentMatch.Time.Sub(matches[previousMatchIndex].Time).Minutes() <= MaxMatchGapMin { + previousMatch := matches[previousMatchIndex] + previousMinutesLate := previousMatch.StartedAt.Sub(previousMatch.Time).Minutes() + minutesLate = math.Max(previousMinutesLate, currentMinutesLate) + } else { + minutesLate = math.Max(currentMinutesLate, 0) + } + } else if arena.MatchState == PostMatch { + currentMinutesLate := currentMatch.StartedAt.Sub(currentMatch.Time).Minutes() + if nextMatchIndex < len(matches) { + nextMatch := matches[nextMatchIndex] + nextMinutesLate := time.Now().Sub(nextMatch.Time).Minutes() + minutesLate = math.Max(currentMinutesLate, nextMinutesLate) + } else { + minutesLate = currentMinutesLate + } + } + } + + if minutesLate > earlyLateThresholdMin { + return fmt.Sprintf("Event is running %d minutes late", int(minutesLate)) + } else if minutesLate < -earlyLateThresholdMin { + return fmt.Sprintf("Event is running %d minutes early", int(-minutesLate)) + } + return "Event is running on schedule" +} diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 3cd8403..fc60cfe 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -20,6 +20,7 @@ type ArenaNotifiers struct { ArenaStatusNotifier *websocket.Notifier AudienceDisplayModeNotifier *websocket.Notifier DisplayConfigurationNotifier *websocket.Notifier + EventStatusNotifier *websocket.Notifier LowerThirdNotifier *websocket.Notifier MatchLoadNotifier *websocket.Notifier MatchTimeNotifier *websocket.Notifier @@ -57,6 +58,7 @@ func (arena *Arena) configureNotifiers() { arena.generateAudienceDisplayModeMessage) arena.DisplayConfigurationNotifier = websocket.NewNotifier("displayConfiguration", arena.generateDisplayConfigurationMessage) + arena.EventStatusNotifier = websocket.NewNotifier("eventStatus", arena.generateEventStatusMessage) arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", arena.generateLowerThirdMessage) arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.generateMatchLoadMessage) arena.MatchTimeNotifier = websocket.NewNotifier("matchTime", arena.generateMatchTimeMessage) @@ -117,6 +119,10 @@ func (arena *Arena) generateDisplayConfigurationMessage() interface{} { return &DisplayConfigurationMessage{displaysCopy, displayUrls} } +func (arena *Arena) generateEventStatusMessage() interface{} { + return arena.EventStatusMessage +} + func (arena *Arena) generateLowerThirdMessage() interface{} { return &struct { LowerThird *model.LowerThird diff --git a/field/arena_test.go b/field/arena_test.go index b80a7f1..ae75861 100644 --- a/field/arena_test.go +++ b/field/arena_test.go @@ -644,3 +644,100 @@ func TestSaveTeamHasConnected(t *testing.T) { assert.Equal(t, "San Jose", teams[5].City) } } + +func TestEventStatusMessage(t *testing.T) { + arena := setupTestArena(t) + + arena.LoadTestMatch() + assert.Equal(t, "", arena.getEventStatusMessage()) + + arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "1"}) + arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "2"}) + matches, _ := arena.Database.GetMatchesByType("qualification") + assert.Equal(t, 2, len(matches)) + + setMatch(arena.Database, &matches[0], time.Now().Add(300*time.Second), time.Time{}, false) + arena.CurrentMatch = &matches[0] + arena.MatchState = PreMatch + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(60*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(-60*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(-120*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(-180*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(181*time.Second), time.Now(), false) + arena.MatchState = AutoPeriod + assert.Equal(t, "Event is running 3 minutes early", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), false) + setMatch(arena.Database, &matches[1], time.Now().Add(481*time.Second), time.Time{}, false) + arena.MatchState = PostMatch + assert.Equal(t, "Event is running 5 minutes early", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(181*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running 3 minutes early", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(-60*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), true) + assert.Equal(t, "", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(900*time.Second), time.Time{}, false) + arena.CurrentMatch = &matches[1] + arena.MatchState = PreMatch + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(899*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running 5 minutes early", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(60*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(-120*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false) + assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Now().Add(-541*time.Second), false) + arena.MatchState = TeleopPeriod + assert.Equal(t, "Event is running 6 minutes early", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), false) + arena.MatchState = PostMatch + assert.Equal(t, "Event is running 8 minutes late", arena.getEventStatusMessage()) + + setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), true) + assert.Equal(t, "", arena.getEventStatusMessage()) + + // Check other match types. + arena.MatchState = PreMatch + arena.CurrentMatch = &model.Match{Type: "practice", Time: time.Now().Add(-181 * time.Second)} + assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage()) + + arena.CurrentMatch = &model.Match{Type: "elimination", Time: time.Now().Add(-181 * time.Second)} + assert.Equal(t, "", arena.getEventStatusMessage()) +} + +func setMatch(database *model.Database, match *model.Match, matchTime time.Time, startedAt time.Time, isComplete bool) { + match.Time = matchTime + match.StartedAt = startedAt + if isComplete { + match.Status = "complete" + } else { + match.Status = "" + } + _ = database.SaveMatch(match) +} diff --git a/static/css/pit_display.css b/static/css/pit_display.css index 444f742..1ea5e02 100644 --- a/static/css/pit_display.css +++ b/static/css/pit_display.css @@ -75,3 +75,11 @@ body { padding-left: 0; padding-right: 0; } +#eventStatusMessage { + margin-top: 10px; + font-size: 25px; + font-family: "FuturaLTBold"; + color: #fff; + text-align: center; + text-transform: uppercase; +} diff --git a/static/css/queueing_display.css b/static/css/queueing_display.css index 6a4cfa1..eb03139 100644 --- a/static/css/queueing_display.css +++ b/static/css/queueing_display.css @@ -54,7 +54,7 @@ h1 { .avatars { line-height: 51px; } -#footer { +#eventStatusMessage { font-size: 25px; font-family: "FuturaLTBold"; color: #fff; diff --git a/static/js/match_play.js b/static/js/match_play.js index 0252808..38d487c 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -225,6 +225,11 @@ var handleAllianceStationDisplayMode = function(data) { $("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true); }; +// Handles a websocket message to update the event status message. +var handleEventStatus = function(data) { + $("#eventStatusMessage").text(data); +}; + $(function() { // Activate tooltips above the status headers. $("[data-toggle=tooltip]").tooltip({"placement": "top"}); @@ -234,9 +239,10 @@ $(function() { allianceStationDisplayMode: function(event) { handleAllianceStationDisplayMode(event.data); }, arenaStatus: function(event) { handleArenaStatus(event.data); }, audienceDisplayMode: function(event) { handleAudienceDisplayMode(event.data); }, + eventStatus: function(event) { handleEventStatus(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, matchTiming: function(event) { handleMatchTiming(event.data); }, realtimeScore: function(event) { handleRealtimeScore(event.data); }, - scoringStatus: function(event) { handleScoringStatus(event.data); } + scoringStatus: function(event) { handleScoringStatus(event.data); }, }); }); diff --git a/static/js/pit_display.js b/static/js/pit_display.js index 25bd416..49398b5 100644 --- a/static/js/pit_display.js +++ b/static/js/pit_display.js @@ -79,13 +79,20 @@ var setHighestPlayedMatch = function(highestPlayedMatch) { } }; +// Handles a websocket message to update the event status message. +var handleEventStatus = function(data) { + $("#eventStatusMessage").text(data); +}; + $(function() { // Read the configuration for this display from the URL query string. var urlParams = new URLSearchParams(window.location.search); scrollMsPerRow = urlParams.get("scrollMsPerRow"); // Set up the websocket back to the server. Used only for remote forcing of reloads. - websocket = new CheesyWebsocket("/displays/pit/websocket", {}); + websocket = new CheesyWebsocket("/displays/pit/websocket", { + eventStatus: function(event) { handleEventStatus(event.data); }, + }); updateStaticRankings(); }); diff --git a/static/js/queueing_display.js b/static/js/queueing_display.js index 698c537..2bb95fb 100644 --- a/static/js/queueing_display.js +++ b/static/js/queueing_display.js @@ -28,11 +28,17 @@ var handleMatchTime = function(data) { }); }; +// Handles a websocket message to update the event status message. +var handleEventStatus = function(data) { + $("#eventStatusMessage").text(data); +}; + $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/queueing/websocket", { + eventStatus: function(event) { handleEventStatus(event.data); }, matchLoad: function(event) { handleMatchLoad(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, - matchTiming: function(event) { handleMatchTiming(event.data); } + matchTiming: function(event) { handleMatchTiming(event.data); }, }); }); diff --git a/templates/match_play.html b/templates/match_play.html index b2bf9b7..3760c53 100644 --- a/templates/match_play.html +++ b/templates/match_play.html @@ -216,6 +216,9 @@ +
+
+
diff --git a/templates/pit_display.html b/templates/pit_display.html index daa110f..c30f991 100644 --- a/templates/pit_display.html +++ b/templates/pit_display.html @@ -46,6 +46,7 @@ +
diff --git a/web/match_play.go b/web/match_play.go index 93d5b96..cf59fb3 100644 --- a/web/match_play.go +++ b/web/match_play.go @@ -177,7 +177,7 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request // Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine. go ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.ArenaStatusNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, web.arena.ScoringStatusNotifier, web.arena.AudienceDisplayModeNotifier, - web.arena.AllianceStationDisplayModeNotifier) + web.arena.AllianceStationDisplayModeNotifier, web.arena.EventStatusNotifier) // Loop, waiting for commands and responding to them, until the client closes the connection. for { diff --git a/web/match_play_test.go b/web/match_play_test.go index 7354552..f608abc 100644 --- a/web/match_play_test.go +++ b/web/match_play_test.go @@ -258,6 +258,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { readWebsocketType(t, ws, "scoringStatus") readWebsocketType(t, ws, "audienceDisplayMode") readWebsocketType(t, ws, "allianceStationDisplayMode") + readWebsocketType(t, ws, "eventStatus") // Test that a server-side error is communicated to the client. ws.Write("nonexistenttype", nil) @@ -348,6 +349,7 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) { readWebsocketType(t, ws, "scoringStatus") readWebsocketType(t, ws, "audienceDisplayMode") readWebsocketType(t, ws, "allianceStationDisplayMode") + readWebsocketType(t, ws, "eventStatus") web.arena.AllianceStations["R1"].Bypass = true web.arena.AllianceStations["R2"].Bypass = true diff --git a/web/pit_display.go b/web/pit_display.go index c1b3df8..9f81229 100644 --- a/web/pit_display.go +++ b/web/pit_display.go @@ -49,5 +49,6 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques defer ws.Close() // Subscribe the websocket to the notifiers whose messages will be passed on to the client. - ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) + ws.HandleNotifiers(web.arena.EventStatusNotifier, web.arena.DisplayConfigurationNotifier, + web.arena.ReloadDisplaysNotifier) } diff --git a/web/pit_display_test.go b/web/pit_display_test.go index 1099431..bfb2d06 100644 --- a/web/pit_display_test.go +++ b/web/pit_display_test.go @@ -29,6 +29,7 @@ func TestPitDisplayWebsocket(t *testing.T) { ws := websocket.NewTestWebsocket(conn) // Should get a few status updates right after connection. + readWebsocketType(t, ws, "eventStatus") readWebsocketType(t, ws, "displayConfiguration") // Check forced reloading as that is the only purpose the pit websocket serves. diff --git a/web/queueing_display.go b/web/queueing_display.go index 66c8eb7..71f00ec 100644 --- a/web/queueing_display.go +++ b/web/queueing_display.go @@ -6,7 +6,7 @@ package web import ( - "fmt" + "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/websocket" "net/http" @@ -14,9 +14,7 @@ import ( ) const ( - earlyLateThresholdMin = 2 - maxGapMin = 20 - numMatchesToShow = 5 + numMatchesToShow = 5 ) // Renders the queueing display that shows upcoming matches and timing information. @@ -41,7 +39,7 @@ func (web *Web) queueingDisplayHandler(w http.ResponseWriter, r *http.Request) { } // Don't include any more matches if there is a significant gap before the next one. - if i+1 < len(matches) && matches[i+1].Time.Sub(match.Time) > maxGapMin*time.Minute { + if i+1 < len(matches) && matches[i+1].Time.Sub(match.Time) > field.MaxMatchGapMin*time.Minute { break } } @@ -56,9 +54,7 @@ func (web *Web) queueingDisplayHandler(w http.ResponseWriter, r *http.Request) { *model.EventSettings MatchTypePrefix string Matches []model.Match - StatusMessage string - }{web.arena.EventSettings, web.arena.CurrentMatch.TypePrefix(), upcomingMatches, - generateEventStatusMessage(web.arena.CurrentMatch.Type, matches)} + }{web.arena.EventSettings, web.arena.CurrentMatch.TypePrefix(), upcomingMatches} err = template.ExecuteTemplate(w, "queueing_display.html", data) if err != nil { handleWebErr(w, err) @@ -84,35 +80,5 @@ func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r *http.R // Subscribe the websocket to the notifiers whose messages will be passed on to the client. ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, - web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) -} - -// Returns a message indicating how early or late the event is running. -func generateEventStatusMessage(matchType string, matches []model.Match) string { - if matchType != "practice" && matchType != "qualification" { - // Only practice and qualification matches have a strict schedule. - return "" - } - if len(matches) == 0 || matches[len(matches)-1].Status == "complete" { - // All matches of the current type are complete. - return "" - } - - for i := len(matches) - 1; i >= 0; i-- { - match := matches[i] - if match.Status == "complete" { - if i+1 < len(matches) && matches[i+1].Time.Sub(match.Time) > maxGapMin*time.Minute { - break - } else { - minutesLate := match.StartedAt.Sub(match.Time).Minutes() - if minutesLate > earlyLateThresholdMin { - return fmt.Sprintf("Event is running %d minutes late", int(minutesLate)) - } else if minutesLate < -earlyLateThresholdMin { - return fmt.Sprintf("Event is running %d minutes early", int(-minutesLate)) - } - } - } - } - - return "Event is running on schedule" + web.arena.EventStatusNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) } diff --git a/web/queueing_display_test.go b/web/queueing_display_test.go index 70c3d84..46286b0 100644 --- a/web/queueing_display_test.go +++ b/web/queueing_display_test.go @@ -4,12 +4,10 @@ package web import ( - "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/websocket" gorillawebsocket "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "testing" - "time" ) func TestQueueingDisplay(t *testing.T) { @@ -34,59 +32,6 @@ func TestQueueingDisplayWebsocket(t *testing.T) { readWebsocketType(t, ws, "matchTiming") readWebsocketType(t, ws, "matchLoad") readWebsocketType(t, ws, "matchTime") + readWebsocketType(t, ws, "eventStatus") readWebsocketType(t, ws, "displayConfiguration") } - -func TestQueueingStatusMessage(t *testing.T) { - assert.Equal(t, "", generateEventStatusMessage("practice", []model.Match{})) - - matches := make([]model.Match, 3) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("practice", matches)) - - // Check within threshold considered to be on time. - setMatchLateness(&matches[1], 0) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) - setMatchLateness(&matches[1], 60) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("practice", matches)) - setMatchLateness(&matches[1], -60) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) - setMatchLateness(&matches[1], 90) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) - setMatchLateness(&matches[1], -90) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) - setMatchLateness(&matches[1], 110) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("practice", matches)) - setMatchLateness(&matches[1], -110) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) - - // Check lateness. - setMatchLateness(&matches[1], 130) - assert.Equal(t, "Event is running 2 minutes late", generateEventStatusMessage("practice", matches)) - setMatchLateness(&matches[1], 3601) - assert.Equal(t, "Event is running 60 minutes late", generateEventStatusMessage("qualification", matches)) - - // Check earliness. - setMatchLateness(&matches[1], -130) - assert.Equal(t, "Event is running 2 minutes early", generateEventStatusMessage("qualification", matches)) - setMatchLateness(&matches[1], -3601) - assert.Equal(t, "Event is running 60 minutes early", generateEventStatusMessage("practice", matches)) - - // Check other match types. - assert.Equal(t, "", generateEventStatusMessage("test", matches)) - assert.Equal(t, "", generateEventStatusMessage("elimination", matches)) - - // Check that later matches supersede earlier ones. - matches = append(matches, model.Match{}) - setMatchLateness(&matches[2], 180) - assert.Equal(t, "Event is running 3 minutes late", generateEventStatusMessage("qualification", matches)) - - // Check that a lateness before a large gap is ignored. - matches[3].Time = time.Now().Add(time.Minute * 25) - assert.Equal(t, "Event is running on schedule", generateEventStatusMessage("qualification", matches)) -} - -func setMatchLateness(match *model.Match, secondsLate int) { - match.Time = time.Now() - match.StartedAt = time.Now().Add(time.Second * time.Duration(secondsLate)) - match.Status = "complete" -}