From d36c0da5bfec0c35ff18f8690e6637b0d9b90945 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 30 Sep 2021 22:50:32 +0000 Subject: [PATCH 01/98] Minor version release --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1b588..8d8f41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [v4.2.1] - unreleased +## [v4.2.2] - 2021-09-30 +### Added +- `debug.notes` front matter field + +## [v4.2.1] - 2021-04-13 ### Fixed - Transpiled KSAs no longer dropped From dd8ca811862013b1975e86dedbdf1e6166b44162 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 13 Oct 2021 22:43:51 +0000 Subject: [PATCH 02/98] Test case for #144 --- CHANGELOG.md | 4 ++++ cmd/mothd/state_test.go | 2 +- cmd/mothd/theme_test.go | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8f41b..72bba3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] +### Fixed +- + ## [v4.2.2] - 2021-09-30 ### Added - `debug.notes` front matter field diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 5b890a7..851e222 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -57,7 +57,7 @@ func TestState(t *testing.T) { teamName := "My Team" if err := s.SetTeamName(teamID, teamName); err != nil { - t.Errorf("Setting team name: %w", err) + t.Errorf("Setting team name: %v", err) } if err := s.SetTeamName(teamID, "wat"); err == nil { t.Errorf("Registering team a second time didn't fail") diff --git a/cmd/mothd/theme_test.go b/cmd/mothd/theme_test.go index c042264..3025daa 100644 --- a/cmd/mothd/theme_test.go +++ b/cmd/mothd/theme_test.go @@ -32,6 +32,11 @@ func TestTheme(t *testing.T) { t.Error("Timestamp compared wrong") } + if f, _, err := s.Open("/foo/bar/index.html"); err == nil { + f.Close() + t.Error("Path is ignored") + } + if f, _, err := s.Open("nofile"); err == nil { f.Close() t.Error("Opening non-existent file didn't return an error") From f68201ab53156a40b167ce090c02db29b2f133e1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 13 Oct 2021 18:25:27 -0600 Subject: [PATCH 03/98] Check for new mothballs with the same name Also updated some error messages to pass newer linter. Fixes #172 --- cmd/mothd/httpd.go | 4 ++-- cmd/mothd/httpd_test.go | 16 +++++++--------- cmd/mothd/mothballs.go | 22 +++++++++++++++++----- cmd/mothd/mothballs_test.go | 27 ++++++++++++++++++++++++--- cmd/mothd/providercommand.go | 2 +- cmd/mothd/server.go | 12 ++++++------ cmd/mothd/state.go | 19 ++++++------------- 7 files changed, 63 insertions(+), 39 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 87f8acf..54ca156 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -117,11 +117,11 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite } if err := mh.Register(teamName); err == ErrAlreadyRegistered { - jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered") + jsend.Sendf(w, jsend.Success, "already registered", "team ID has already been registered") } else if err != nil { jsend.Sendf(w, jsend.Fail, "not registered", err.Error()) } else { - jsend.Sendf(w, jsend.Success, "registered", "Team ID registered") + jsend.Sendf(w, jsend.Success, "registered", "team ID registered") } } diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index f8bbb5b..6c623c3 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -18,10 +18,8 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest vals := url.Values{} vals.Set("pid", TestParticipantID) vals.Set("id", TestTeamID) - if args != nil { - for k, v := range args { - vals.Set(k, v) - } + for k, v := range args { + vals.Set(k, v) } recorder := httptest.NewRecorder() @@ -56,19 +54,19 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"status":"fail","data":{"short":"not registered","description":"Team ID not found in list of valid Team IDs"}}` { + } else if r.Body.String() != `{"status":"fail","data":{"short":"not registered","description":"team ID not found in list of valid team IDs"}}` { t.Error("Register bad team ID failed") } if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"Team ID registered"}}` { + } else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"team ID registered"}}` { t.Error("Register failed") } if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` { + } else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"team ID has already been registered"}}` { t.Error("Register failed", r.Body.String()) } @@ -102,7 +100,7 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Incorrect answer"}}` { + } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"incorrect answer"}}` { t.Error("Unexpected body", r.Body.String()) } @@ -131,7 +129,7 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Error awarding points: Points already awarded to this team in this category"}}` { + } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` { t.Error("Unexpected body", r.Body.String()) } } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 8d44a0e..67b7679 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -19,6 +19,7 @@ import ( type zipCategory struct { afero.Fs io.Closer + mtime time.Time } // Mothballs provides a collection of active mothball files (puzzle categories) @@ -48,7 +49,7 @@ func (m *Mothballs) getCat(cat string) (zipCategory, bool) { func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) { zc, ok := m.getCat(cat) if !ok { - return nil, time.Time{}, fmt.Errorf("No such category: %s", cat) + return nil, time.Time{}, fmt.Errorf("no such category: %s", cat) } f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename)) @@ -91,12 +92,12 @@ func (m *Mothballs) Inventory() []Category { func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) { zfs, ok := m.getCat(cat) if !ok { - return false, fmt.Errorf("No such category: %s", cat) + return false, fmt.Errorf("no such category: %s", cat) } af, err := zfs.Open("answers.txt") if err != nil { - return false, fmt.Errorf("No answers.txt file") + return false, fmt.Errorf("no answers.txt file") } defer af.Close() @@ -132,7 +133,18 @@ func (m *Mothballs) refresh() { categoryName := strings.TrimSuffix(filename, ".mb") found[categoryName] = true - if _, ok := m.categories[categoryName]; !ok { + reopen := false + if existingMothball, ok := m.categories[categoryName]; !ok { + reopen = true + } else if si, err := m.Fs.Stat(filename); err != nil { + log.Println(err) + } else if si.ModTime().After(existingMothball.mtime) { + existingMothball.Close() + delete(m.categories, categoryName) + reopen = true + } + + if reopen { f, err := m.Fs.Open(filename) if err != nil { log.Println(err) @@ -174,7 +186,7 @@ func (m *Mothballs) refresh() { // Mothball just returns an error func (m *Mothballs) Mothball(cat string, w io.Writer) error { - return fmt.Errorf("Refusing to repackage a compiled mothball") + return fmt.Errorf("refusing to repackage a compiled mothball") } // Maintain performs housekeeping for Mothballs. diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index 9b75838..61cfff5 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -3,6 +3,8 @@ package main import ( "archive/zip" "fmt" + "io/ioutil" + "log" "testing" "github.com/spf13/afero" @@ -14,14 +16,13 @@ var testFiles = []struct { {"puzzles.txt", "1\n3\n2\n"}, {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"}, {"1/puzzle.json", `{"name": "moo"}`}, - {"1/moo.txt", `moo`}, {"2/puzzle.json", `{}`}, {"2/moo.txt", `moo`}, {"3/puzzle.json", `{}`}, {"3/moo.txt", `moo`}, } -func (m *Mothballs) createMothball(cat string) { +func (m *Mothballs) createMothballWithMoo1(cat string, moo1 string) { f, _ := m.Create(fmt.Sprintf("%s.mb", cat)) defer f.Close() @@ -32,6 +33,13 @@ func (m *Mothballs) createMothball(cat string) { of, _ := w.Create(file.Name) of.Write([]byte(file.Body)) } + + of, _ := w.Create("1/moo.txt") + of.Write([]byte(moo1)) +} + +func (m *Mothballs) createMothball(cat string) { + m.createMothballWithMoo1(cat, "moo") } func NewTestMothballs() *Mothballs { @@ -92,10 +100,23 @@ func TestMothballs(t *testing.T) { } if ok, err := m.CheckAnswer("nealegory", 1, "moo"); ok { t.Error("Checking answer in non-existent category should fail") - } else if err.Error() != "No such category: nealegory" { + } else if err.Error() != "no such category: nealegory" { t.Error("Wrong error message") } + goofyText := "bozonics" + log.Print("Hey Bozo") + //time.Sleep(1 * time.Second) // I don't love this, but we need the mtime to increase, and it's only accurate to 1s + m.createMothballWithMoo1("pategory", goofyText) + m.refresh() + if f, _, err := m.Open("pategory", 1, "moo.txt"); err != nil { + t.Error("pategory/1/moo.txt", err) + } else if contents, err := ioutil.ReadAll(f); err != nil { + t.Error("read all pategory/1/moo.txt", err) + } else if string(contents) != goofyText { + t.Error("read all replacement pategory/1/moo.txt contents wrong, got", string(contents)) + } + m.createMothball("test2") m.Fs.Remove("pategory.mb") m.refresh() diff --git a/cmd/mothd/providercommand.go b/cmd/mothd/providercommand.go index ef0ca0b..1bdd693 100644 --- a/cmd/mothd/providercommand.go +++ b/cmd/mothd/providercommand.go @@ -125,7 +125,7 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo // Mothball just returns an error func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) { - return nil, fmt.Errorf("Can't package a command-generated category") + return nil, fmt.Errorf("can't package a command-generated category") } // Maintain does nothing: a command puzzle ProviderCommand has no housekeeping diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index f9d6775..608e6d6 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -115,7 +115,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) ( } } if !found { - return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked") + return nil, time.Time{}, fmt.Errorf("puzzle does not exist or is locked") } // Try every provider until someone doesn't return an error @@ -146,16 +146,16 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) } if !correct { mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points) - return fmt.Errorf("Incorrect answer") + return fmt.Errorf("incorrect answer") } mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points) if _, err := mh.State.TeamName(mh.teamID); err != nil { - return fmt.Errorf("Invalid team ID") + return fmt.Errorf("invalid team ID") } if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil { - return fmt.Errorf("Error awarding points: %s", err) + return fmt.Errorf("error awarding points: %s", err) } return nil @@ -170,7 +170,7 @@ func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time, func (mh *MothRequestHandler) Register(teamName string) error { // BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success if teamName == "" { - return fmt.Errorf("Empty team name") + return fmt.Errorf("empty team name") } mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0) return mh.State.SetTeamName(mh.teamID, teamName) @@ -254,7 +254,7 @@ func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error { var err error if !mh.Config.Devel { - return fmt.Errorf("Cannot mothball in production mode") + return fmt.Errorf("cannot mothball in production mode") } for _, provider := range mh.PuzzleProviders { if err = provider.Mothball(cat, w); err == nil { diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index a542caa..e354b22 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -27,7 +27,7 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy=" const RFC3339Space = "2006-01-02 15:04:05Z07:00" // ErrAlreadyRegistered means a team cannot be registered because it was registered previously. -var ErrAlreadyRegistered = errors.New("Team ID has already been registered") +var ErrAlreadyRegistered = errors.New("team ID has already been registered") // State defines the current state of a MOTH instance. // We use the filesystem for synchronization between threads. @@ -123,9 +123,9 @@ func (s *State) TeamName(teamID string) (string, error) { teamFs := afero.NewBasePathFs(s.Fs, "teams") teamNameBytes, err := afero.ReadFile(teamFs, teamID) if os.IsNotExist(err) { - return "", fmt.Errorf("Unregistered team ID: %s", teamID) + return "", fmt.Errorf("unregistered team ID: %s", teamID) } else if err != nil { - return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err) + return "", fmt.Errorf("unregistered team ID: %s (%s)", teamID, err) } teamName := strings.TrimSpace(string(teamNameBytes)) @@ -137,7 +137,7 @@ func (s *State) TeamName(teamID string) (string, error) { func (s *State) SetTeamName(teamID, teamName string) error { idsFile, err := s.Open("teamids.txt") if err != nil { - return fmt.Errorf("Team IDs file does not exist") + return fmt.Errorf("team IDs file does not exist") } defer idsFile.Close() found := false @@ -149,7 +149,7 @@ func (s *State) SetTeamName(teamID, teamName string) error { } } if !found { - return fmt.Errorf("Team ID not found in list of valid Team IDs") + return fmt.Errorf("team ID not found in list of valid team IDs") } teamFilename := filepath.Join("teams", teamID) @@ -211,7 +211,7 @@ func (s *State) AwardPoints(teamID, category string, points int) error { for _, e := range s.PointsLog() { if a.Equal(e) { - return fmt.Errorf("Points already awarded to this team in this category") + return fmt.Errorf("points already awarded to this team in this category") } } @@ -363,13 +363,6 @@ func (s *State) maybeInitialize() { } } -func logstr(s string) string { - if s == "" { - return "-" - } - return s -} - // LogEvent writes to the event log func (s *State) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) { s.eventStream <- append( From 46fea903a666b122e2df4af10fa321bf1f4241e8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 13 Oct 2021 18:35:33 -0600 Subject: [PATCH 04/98] Remove debug log message --- cmd/mothd/mothballs_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index 61cfff5..7348bfd 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -105,7 +105,6 @@ func TestMothballs(t *testing.T) { } goofyText := "bozonics" - log.Print("Hey Bozo") //time.Sleep(1 * time.Second) // I don't love this, but we need the mtime to increase, and it's only accurate to 1s m.createMothballWithMoo1("pategory", goofyText) m.refresh() From 454e643886db94c766f86955987ff39904b708cd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 14 Oct 2021 15:57:17 -0600 Subject: [PATCH 05/98] Fix broken build? --- cmd/mothd/mothballs_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index 7348bfd..81e82e2 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -4,7 +4,6 @@ import ( "archive/zip" "fmt" "io/ioutil" - "log" "testing" "github.com/spf13/afero" From 41a0e6dffc1d935e24393264feda5ce499bf003e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 14 Oct 2021 19:01:12 -0600 Subject: [PATCH 06/98] Prepend timestamp to award filenames Fixes #168 --- CHANGELOG.md | 4 +++- cmd/mothd/state.go | 9 +++++++-- cmd/mothd/state_test.go | 24 ++++++++++++++++++++++++ pkg/award/award.go | 14 +++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bba3a..db46649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Fixed -- +- Points awarded while scoring is paused are now correctly sorted (#168) +- Writing a new mothball with the same name is now detected and the new mothball loaded (#172) +- Regression test for issue where URL path leading directories were ignored (#144) ## [v4.2.2] - 2021-09-30 ### Added diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index e354b22..b8cb231 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -202,8 +202,12 @@ func (s *State) Messages() string { // It's just a courtesy to the user. // The update task makes sure we never have duplicate points in the log. func (s *State) AwardPoints(teamID, category string, points int) error { + return s.awardPointsAtTime(time.Now().Unix(), teamID, category, points) +} + +func (s *State) awardPointsAtTime(when int64, teamID string, category string, points int) error { a := award.T{ - When: time.Now().Unix(), + When: when, TeamID: teamID, Category: category, Points: points, @@ -215,7 +219,8 @@ func (s *State) AwardPoints(teamID, category string, points int) error { } } - fn := fmt.Sprintf("%s-%s-%d", teamID, category, points) + //fn := fmt.Sprintf("%s-%s-%d", a.TeamID, a.Category, a.Points) + fn := a.Filename() tmpfn := filepath.Join("points.tmp", fn) newfn := filepath.Join("points.new", fn) diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 851e222..5cb1097 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -122,6 +122,30 @@ func TestState(t *testing.T) { } +// Out of order points insertion, issue #168 +func TestStateOutOfOrderAward(t *testing.T) { + s := NewTestState() + + category := "meow" + points := 100 + + now := time.Now().Unix() + if err := s.awardPointsAtTime(now+20, "AA", category, points); err != nil { + t.Error("Awarding points to team ZZ:", err) + } + if err := s.awardPointsAtTime(now+10, "ZZ", category, points); err != nil { + t.Error("Awarding points to team AA:", err) + } + s.refresh() + pl := s.PointsLog() + if len(pl) != 2 { + t.Error("Wrong length for points log") + } + if pl[0].TeamID != "ZZ" { + t.Error("Out of order points insertion not properly sorted in points log") + } +} + func TestStateEvents(t *testing.T) { s := NewTestState() s.LogEvent("moo", "", "", "", 0) diff --git a/pkg/award/award.go b/pkg/award/award.go index a589866..fda4c05 100644 --- a/pkg/award/award.go +++ b/pkg/award/award.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/json" "fmt" + "net/url" "reflect" "strconv" "strings" @@ -49,7 +50,7 @@ func Parse(s string) (T, error) { if err != nil { return ret, err } else if n != 4 { - return ret, fmt.Errorf("Malformed award string: only parsed %d fields", n) + return ret, fmt.Errorf("malformed award string: only parsed %d fields", n) } return ret, nil @@ -60,6 +61,17 @@ func (a T) String() string { return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points) } +// Filename returns a string version of an award suitable for a filesystem +func (a T) Filename() string { + return fmt.Sprintf( + "%d-%s-%s-%d.award", + a.When, + url.PathEscape(a.TeamID), + url.PathEscape(a.Category), + a.Points, + ) +} + // MarshalJSON returns the award event, encoded as a list. func (a T) MarshalJSON() ([]byte, error) { ao := []interface{}{ From d9299f5e59e948edcaff25035d405c5a75c17e80 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 14 Oct 2021 19:17:38 -0600 Subject: [PATCH 07/98] Fix test failure --- CHANGELOG.md | 4 ++++ cmd/mothd/mothballs_test.go | 29 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db46649..685cba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Writing a new mothball with the same name is now detected and the new mothball loaded (#172) - Regression test for issue where URL path leading directories were ignored (#144) +### Changed +- Many error messages were changed to start with a lower-case letter, + in order to satisfy a new linter check. + ## [v4.2.2] - 2021-09-30 ### Added - `debug.notes` front matter field diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index 81e82e2..61bcd82 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -9,9 +9,11 @@ import ( "github.com/spf13/afero" ) -var testFiles = []struct { +type testFileContents struct { Name, Body string -}{ +} + +var testFiles = []testFileContents{ {"puzzles.txt", "1\n3\n2\n"}, {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"}, {"1/puzzle.json", `{"name": "moo"}`}, @@ -21,7 +23,7 @@ var testFiles = []struct { {"3/moo.txt", `moo`}, } -func (m *Mothballs) createMothballWithMoo1(cat string, moo1 string) { +func (m *Mothballs) createMothballWithFiles(cat string, contents []testFileContents) { f, _ := m.Create(fmt.Sprintf("%s.mb", cat)) defer f.Close() @@ -32,13 +34,19 @@ func (m *Mothballs) createMothballWithMoo1(cat string, moo1 string) { of, _ := w.Create(file.Name) of.Write([]byte(file.Body)) } - - of, _ := w.Create("1/moo.txt") - of.Write([]byte(moo1)) + for _, file := range contents { + of, _ := w.Create(file.Name) + of.Write([]byte(file.Body)) + } } func (m *Mothballs) createMothball(cat string) { - m.createMothballWithMoo1(cat, "moo") + m.createMothballWithFiles( + cat, + []testFileContents{ + {"1/moo.txt", "moo"}, + }, + ) } func NewTestMothballs() *Mothballs { @@ -105,7 +113,12 @@ func TestMothballs(t *testing.T) { goofyText := "bozonics" //time.Sleep(1 * time.Second) // I don't love this, but we need the mtime to increase, and it's only accurate to 1s - m.createMothballWithMoo1("pategory", goofyText) + m.createMothballWithFiles( + "pategory", + []testFileContents{ + {"1/moo.txt", goofyText}, + }, + ) m.refresh() if f, _, err := m.Open("pategory", 1, "moo.txt"); err != nil { t.Error("pategory/1/moo.txt", err) From a585afdd8dd1aff3c6eb8bb9e0f890d5758581d7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 18:22:00 -0600 Subject: [PATCH 08/98] CI/CD --- .gitlab-ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..63d90a6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,21 @@ +stages: + - test + - push + +test: + image: golang:1.17 + only: + refs: + - main + - merge_requests + stage: test + script: + - go test ./... + +push: + stage: push + rules: + - if: $CI_COMMIT_TAG + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + script: + - ./ci.sh push From 5adf32f4560242d19e0479c89b8e91b0a5a9bd28 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 18:30:11 -0600 Subject: [PATCH 09/98] CI/CD --- cmd/mothd/server_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 0d57e94..2434e22 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -168,9 +168,8 @@ func TestProdServer(t *testing.T) { t.Error("Anonymous TeamNames is wrong:", es.TeamNames) } if len(es.PointsLog) != 2 { - t.Error("Points log wrong length") - } - if es.PointsLog[1].TeamID != "0" { + t.Errorf("Points log wrong length: got %d, wanted 2", len(es.PointsLog)) + } else if es.PointsLog[1].TeamID != "0" { t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1]) } } From 4dbeaac0bf7e809d6005ccfbe96dcf5dd0b7afe8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 18:52:05 -0600 Subject: [PATCH 10/98] CI/CD --- .gitlab-ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63d90a6..c453329 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,4 +18,12 @@ push: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - - ./ci.sh push + - > + docker buildx + --file build/package/Containerfile + --tag ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} + build + . + - > + docker push + ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} From ec9b2e2772d6d9279d4fb6b07207c004a247d09c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 18:59:15 -0600 Subject: [PATCH 11/98] CI/CD --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c453329..013a5cd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,17 +3,18 @@ stages: - push test: + stage: test image: golang:1.17 only: refs: - main - merge_requests - stage: test script: - go test ./... push: stage: push + image: docker:20 rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 16a02b93abfa953f79e29173b930ab6bdf28cb97 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 19:03:12 -0600 Subject: [PATCH 12/98] CI/CD --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 013a5cd..1f0e146 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,10 +20,9 @@ push: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - > - docker buildx + docker buildx build --file build/package/Containerfile --tag ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} - build . - > docker push From 0d7a8fc93523be9156b17417cbd50cf4604b3c3c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 19:05:17 -0600 Subject: [PATCH 13/98] CI/CD --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1f0e146..26dc94a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ push: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - > - docker buildx build + docker build --file build/package/Containerfile --tag ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} . From c477c1d2ea741a640777da97a24c66486b50c25a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 19:09:58 -0600 Subject: [PATCH 14/98] CI/CD --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 26dc94a..3c8fa75 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,6 @@ test: push: stage: push - image: docker:20 rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 54442ed1b913b68babaed7b92ab9c2d060e6c69f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 19:21:49 -0600 Subject: [PATCH 15/98] CI/CD --- .gitlab-ci.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c8fa75..15b711c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,15 +14,13 @@ test: push: stage: push + image: + gcr.io/kaniko-project/executor:debug + entrypoint: [""] rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - - > - docker build - --file build/package/Containerfile - --tag ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} - . - - > - docker push - ghcr.io/dirtbags/moth:${CI_COMMIT_REF_SLUG} + - mkdir -p /kaniko/.docker + - echo "$DOCKER_AUTH_CONFIG" > /kaniko/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/build/package/Containerfile --destination ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG From e05bc90b3af41e4351ed3b432bef90ab89911dda Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 18 Oct 2021 19:22:49 -0600 Subject: [PATCH 16/98] CI/CD --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15b711c..f2660a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ test: push: stage: push image: - gcr.io/kaniko-project/executor:debug + name: gcr.io/kaniko-project/executor:debug entrypoint: [""] rules: - if: $CI_COMMIT_TAG From 362ba11cf56f19cd9c2b399f481793d0e618bed3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 17:22:11 -0600 Subject: [PATCH 17/98] CI --- .gitlab-ci.yml | 12 +++++++----- CHANGELOG.md | 1 + build/package/Containerfile | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2660a2..faab991 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,12 +15,14 @@ test: push: stage: push image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] + name: docker:19.03.12 rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - - mkdir -p /kaniko/.docker - - echo "$DOCKER_AUTH_CONFIG" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/build/package/Containerfile --destination ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG + - echo "$DOCKER_AUTH_CONFIG" > !~.docker/config.json + - > + docker build + --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG + --file build/package/Containerfile + . diff --git a/CHANGELOG.md b/CHANGELOG.md index 685cba1..3c87828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Many error messages were changed to start with a lower-case letter, in order to satisfy a new linter check. +- CI/CD moved to our Cyber Fire Gitlab instance ## [v4.2.2] - 2021-09-30 ### Added diff --git a/build/package/Containerfile b/build/package/Containerfile index b13d8f5..238db91 100644 --- a/build/package/Containerfile +++ b/build/package/Containerfile @@ -7,7 +7,7 @@ COPY example-puzzles /target/puzzles/ COPY LICENSE.md /target/ RUN mkdir -p /target/state WORKDIR /src/ -RUN CGO_ENABLED=0 GOOS=linux go install -i -a -ldflags '-extldflags "-static"' ./... +RUN CGO_ENABLED=0 GOOS=linux go install -a -ldflags '-extldflags "-static"' ./... # I can't use /target/bin: doing so would cause the devel server to overwrite Ubuntu's /bin ########## From e64e4dfa67f62acc3735b07978faa15bb505cb8e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 17:22:29 -0600 Subject: [PATCH 18/98] CI --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index faab991..54c29ff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ push: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - - echo "$DOCKER_AUTH_CONFIG" > !~.docker/config.json + - echo "$DOCKER_AUTH_CONFIG" > ~/.docker/config.json - > docker build --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG From 30dcda05e4003b373c12c4fb78dec1e8be5d018d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 17:24:08 -0600 Subject: [PATCH 19/98] CI --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54c29ff..99afcf2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ push: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: + - mkdir ~/.docker - echo "$DOCKER_AUTH_CONFIG" > ~/.docker/config.json - > docker build From 61e09eaa71599c2876ca0501c8f20e10fe44d1bd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 17:33:22 -0600 Subject: [PATCH 20/98] CI --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99afcf2..174fd29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,8 +14,6 @@ test: push: stage: push - image: - name: docker:19.03.12 rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 7a672d2fcfc83cec678f9aa6b32cd04a6bb485e5 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 17:36:35 -0600 Subject: [PATCH 21/98] CI --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 174fd29..556c96c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,3 +25,5 @@ push: --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG --file build/package/Containerfile . + tags: + docker From c743148eebe227e912d2fdbad04fddcf61a3665e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 18:45:43 -0600 Subject: [PATCH 22/98] CI --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 556c96c..174fd29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,5 +25,3 @@ push: --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG --file build/package/Containerfile . - tags: - docker From 79f58ff83c22ff1a92309b414488f32696b06ecc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 18:49:46 -0600 Subject: [PATCH 23/98] try to spiff up the CI build --- .gitlab-ci.yml | 1 + build/ci/ci.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100755 build/ci/ci.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 174fd29..b6d291d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ push: script: - mkdir ~/.docker - echo "$DOCKER_AUTH_CONFIG" > ~/.docker/config.json + - sh build/ci/ci.sh publish - > docker build --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG diff --git a/build/ci/ci.sh b/build/ci/ci.sh new file mode 100755 index 0000000..cf53b8c --- /dev/null +++ b/build/ci/ci.sh @@ -0,0 +1,42 @@ +#! /bin/sh + +set -e + +ACTION=$1 +BASE=$2 +if [ -z "$ACTION" ] || [ -z "$BASE" ]; then + echo "Usage: $0 ACTION BASE" + exit 1 +fi + +log () { + printf "=== %s\n" "$*" 1>&2 +} + +fail () { + printf "\033[31;1m=== FAIL: %s\033[0m\n" "$*" 1>&2 + exit 1 +} + +tags () { + pfx=$1 + for base in ghcr.io/dirtbags/moth dirtbags/moth; do + echo $pfx $base:${CI_COMMIT_REF_SLUG} + echo $pfx $base:${CI_COMMIT_REF_SLUG%.*} + echo $pfx $base:${CI_COMMIT_REF_SLUG%.*.*} + done | uniq +} + +case $ACTION in + publish) + docker build \ + --file build/package/Containerfile \ + $(tags) + docker push $(tags --destination) + ;; +*) + echo "Unknown action: $1" 1>&2 + exit 1 + ;; +esac + From c0092751b5a467ae74eaf65e833eae7e72f43591 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 18:52:19 -0600 Subject: [PATCH 24/98] CI --- build/ci/ci.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index cf53b8c..451106a 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -3,9 +3,8 @@ set -e ACTION=$1 -BASE=$2 -if [ -z "$ACTION" ] || [ -z "$BASE" ]; then - echo "Usage: $0 ACTION BASE" +if [ -z "$ACTION" ]; then + echo "Usage: $0 ACTION" exit 1 fi From 31fd9c2fab7051e19b3f91d09ffcba4ed47e791e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 18:53:18 -0600 Subject: [PATCH 25/98] CI --- build/ci/ci.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 451106a..1d1707c 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -30,7 +30,8 @@ case $ACTION in publish) docker build \ --file build/package/Containerfile \ - $(tags) + $(tags) \ + . docker push $(tags --destination) ;; *) From de1fdc06913f931f7561f640a14f6a636dd9f27a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 19:55:42 -0600 Subject: [PATCH 26/98] CI --- build/ci/ci.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 1d1707c..de2692a 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -17,6 +17,11 @@ fail () { exit 1 } +run () { + printf "\033[32m%\033[0m $*\n" 1>&2 + "$@" +} + tags () { pfx=$1 for base in ghcr.io/dirtbags/moth dirtbags/moth; do @@ -28,11 +33,11 @@ tags () { case $ACTION in publish) - docker build \ + run docker build \ --file build/package/Containerfile \ $(tags) \ . - docker push $(tags --destination) + run docker push $(tags --destination) ;; *) echo "Unknown action: $1" 1>&2 From e2e7d37300540f59baa7683dd5b5f7d28a627b3e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 19:57:35 -0600 Subject: [PATCH 27/98] CI --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index de2692a..ea519b9 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -18,7 +18,7 @@ fail () { } run () { - printf "\033[32m%\033[0m $*\n" 1>&2 + printf "\033[32m%\033[0m %s\n" "$*" 1>&2 "$@" } From b5d4ab5c1594b52576febcb2538dd3473c896831 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:01:27 -0600 Subject: [PATCH 28/98] CI --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index ea519b9..96c4665 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -18,7 +18,7 @@ fail () { } run () { - printf "\033[32m%\033[0m %s\n" "$*" 1>&2 + printf "\033[32m\$\033[0m %s\n" "$*" 1>&2 "$@" } From 49343969367c29134927a4195fa71147171b2306 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:03:54 -0600 Subject: [PATCH 29/98] CI --- build/ci/ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 96c4665..6ca1336 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -35,9 +35,9 @@ case $ACTION in publish) run docker build \ --file build/package/Containerfile \ - $(tags) \ + $(tags --destination) \ . - run docker push $(tags --destination) + run docker push $(tags) ;; *) echo "Unknown action: $1" 1>&2 From 2952c1b21c1b530b8b02313781b0c0ce02c055d4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:05:02 -0600 Subject: [PATCH 30/98] CI --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 6ca1336..34281c4 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -35,7 +35,7 @@ case $ACTION in publish) run docker build \ --file build/package/Containerfile \ - $(tags --destination) \ + $(tags --tag) \ . run docker push $(tags) ;; From 781217d2efd8d5cd49d402eec1aaffd20967afd4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:09:51 -0600 Subject: [PATCH 31/98] CI --- build/ci/ci.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 34281c4..28fb389 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -2,6 +2,8 @@ set -e +images="ghcr.io/dirtbags/moth dirtbags/moth" + ACTION=$1 if [ -z "$ACTION" ]; then echo "Usage: $0 ACTION" @@ -24,7 +26,7 @@ run () { tags () { pfx=$1 - for base in ghcr.io/dirtbags/moth dirtbags/moth; do + for base in $images; do echo $pfx $base:${CI_COMMIT_REF_SLUG} echo $pfx $base:${CI_COMMIT_REF_SLUG%.*} echo $pfx $base:${CI_COMMIT_REF_SLUG%.*.*} @@ -37,7 +39,9 @@ case $ACTION in --file build/package/Containerfile \ $(tags --tag) \ . - run docker push $(tags) + for image in $images; do + run docker image push -a $image + done ;; *) echo "Unknown action: $1" 1>&2 From fd6d3192185290f6c6ce4c2abfb753b695811221 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:11:15 -0600 Subject: [PATCH 32/98] CI --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 28fb389..8dd51fc 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -40,7 +40,7 @@ case $ACTION in $(tags --tag) \ . for image in $images; do - run docker image push -a $image + run docker image push --all-tags $image done ;; *) From 62b043354be9d237d6b02ea0697f7b44c0b211ef Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:12:36 -0600 Subject: [PATCH 33/98] CI --- build/ci/ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 8dd51fc..5bc5eea 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -39,8 +39,8 @@ case $ACTION in --file build/package/Containerfile \ $(tags --tag) \ . - for image in $images; do - run docker image push --all-tags $image + tags | while read image; do + run docker push $image done ;; *) From d51e4c2504171c40d3fd5b1e276868770063ee22 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Oct 2021 20:17:22 -0600 Subject: [PATCH 34/98] CI --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 5bc5eea..7bdb4e3 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -2,7 +2,7 @@ set -e -images="ghcr.io/dirtbags/moth dirtbags/moth" +images="ghcr.io/dirtbags/moth" ACTION=$1 if [ -z "$ACTION" ]; then From e349a188613d2889694a806219b88369b8863d8a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 11:29:55 -0600 Subject: [PATCH 35/98] Trying to isolate a race condition in tests --- .gitlab-ci.yml | 2 +- cmd/mothd/httpd_test.go | 50 ++++++++++++++++++++++++++++++++++++---- cmd/mothd/mothballs.go | 7 ++++++ cmd/mothd/server.go | 3 +++ cmd/mothd/server_test.go | 3 +++ cmd/mothd/theme.go | 4 ++++ cmd/mothd/transpiler.go | 4 ++++ 7 files changed, 68 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b6d291d..7682c41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ test: - main - merge_requests script: - - go test ./... + - go test -race ./... push: stage: push diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 6c623c3..2337bec 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -4,10 +4,12 @@ import ( "bytes" "encoding/json" "fmt" + "io/ioutil" + "log" "net/http/httptest" "net/url" + "strings" "testing" - "time" "github.com/spf13/afero" ) @@ -33,7 +35,8 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest } func TestHttpd(t *testing.T) { - hs := NewHTTPServer("/", NewTestServer()) + server := NewTestServer() + hs := NewHTTPServer("/", server) if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -106,11 +109,26 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { t.Error(r.Result()) + } else if strings.Contains(r.Body.String(), "incorrect answer") { + // Pernicious intermittent bug + t.Error("Incorrect answer that was actually correct") + for _, provider := range server.PuzzleProviders { + if mb, ok := provider.(*Mothballs); !ok { + t.Error("Provider is not a mothball") + } else { + cat, _ := mb.getCat("pategory") + f, _ := cat.Open("answers.txt") + defer f.Close() + answersBytes, _ := ioutil.ReadAll(f) + t.Errorf("Correct answers: %v", string(answersBytes)) + } + } + t.Error("Wrong answer") } else if r.Body.String() != `{"status":"success","data":{"short":"accepted","description":"1 points awarded in pategory"}}` { t.Error("Unexpected body", r.Body.String()) } - time.Sleep(TestMaintenanceInterval) + server.State.refresh() if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -122,13 +140,37 @@ func TestHttpd(t *testing.T) { } else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil { t.Error(err) } else if len(state.PointsLog) != 1 { - t.Error("Points log wrong length") + switch v := server.State.(type) { + case *State: + log.Print(v) + } + + t.Errorf("Points log wrong length. Wanted 1, got %v", state.PointsLog) } else if len(state.Puzzles["pategory"]) != 2 { t.Error("Didn't unlock next puzzle") } if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { t.Error(r.Result()) + } else if strings.Contains(r.Body.String(), "incorrect answer") { + // Pernicious intermittent bug + t.Error("Incorrect answer that was actually correct") + for _, provider := range server.PuzzleProviders { + if mb, ok := provider.(*Mothballs); !ok { + t.Error("Provider is not a mothball") + } else { + if cat, ok := mb.getCat("pategory"); !ok { + t.Error("opening pategory failed") + } else if f, err := cat.Open("answers.txt"); err != nil { + t.Error("opening answers.txt", err) + } else { + defer f.Close() + answersBytes, _ := ioutil.ReadAll(f) + t.Errorf("Correct answers: %#v len %d", string(answersBytes), len(answersBytes)) + } + } + } + t.Error("Wrong answer") } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` { t.Error("Unexpected body", r.Body.String()) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 67b7679..cd252fd 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -92,23 +92,30 @@ func (m *Mothballs) Inventory() []Category { func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) { zfs, ok := m.getCat(cat) if !ok { + log.Println("There's no such category") return false, fmt.Errorf("no such category: %s", cat) } + log.Println("Opening answers.txt") af, err := zfs.Open("answers.txt") if err != nil { + log.Println("I did not find an answer") return false, fmt.Errorf("no answers.txt file") } defer af.Close() + log.Println("I'm going to start looking for an answer") needle := fmt.Sprintf("%d %s", points, answer) scanner := bufio.NewScanner(af) for scanner.Scan() { + log.Println("testing equality between", scanner.Text(), needle) if scanner.Text() == needle { return true, nil } } + log.Println("I did not find the answer", answer) + return false, nil } diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 608e6d6..49d3f54 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -68,6 +68,9 @@ type Maintainer interface { // It will only be called once, when execution begins. // It's okay to just exit if there's no maintenance to be done. Maintain(updateInterval time.Duration) + + // refresh is a shortcut used internally for testing + refresh() } // MothServer gathers together the providers that make up a MOTH server. diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 2434e22..224d397 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -11,6 +11,9 @@ import ( const TestMaintenanceInterval = time.Millisecond * 1 const TestTeamID = "teamID" +// NewTestServer creates a new MothServer with NewTestMothballs and some initial state. +// +// See function definition for details. func NewTestServer() *MothServer { puzzles := NewTestMothballs() go puzzles.Maintain(TestMaintenanceInterval) diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index a70ca32..2b9b0ff 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -38,3 +38,7 @@ func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) { func (t *Theme) Maintain(i time.Duration) { // No periodic tasks for a theme } + +func (t *Theme) refresh() { + // Nothing to do for a theme +} diff --git a/cmd/mothd/transpiler.go b/cmd/mothd/transpiler.go index 7103b20..ddeb3da 100644 --- a/cmd/mothd/transpiler.go +++ b/cmd/mothd/transpiler.go @@ -79,3 +79,7 @@ func (p TranspilerProvider) Mothball(cat string, w io.Writer) error { func (p TranspilerProvider) Maintain(updateInterval time.Duration) { // Nothing to do here. } + +func (p TranspilerProvider) refresh() { + // Nothing to do for a theme +} From 459d7747266e16f451835d29bca2e9df31121b90 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 11:30:53 -0600 Subject: [PATCH 36/98] Always run tests in CI, not just on main branch --- .gitlab-ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7682c41..725caaa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,10 +5,6 @@ stages: test: stage: test image: golang:1.17 - only: - refs: - - main - - merge_requests script: - go test -race ./... From ce037ebca300e08d01029de50c444149cc977087 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 11:43:48 -0600 Subject: [PATCH 37/98] Stop pushing images from Github --- .github/workflows/build+test.yml | 57 ++------------------------------ 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build+test.yml b/.github/workflows/build+test.yml index e25dda5..b24e26f 100644 --- a/.github/workflows/build+test.yml +++ b/.github/workflows/build+test.yml @@ -1,13 +1,4 @@ -name: Build/Test/Push - -on: - push: - branches: - - v3 - - devel - - main - tags: - - 'v*.*.*' +name: Test jobs: test-mothd: @@ -17,54 +8,10 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.13 + go-version: 1.17 - name: Retrieve code uses: actions/checkout@v2 - name: Test run: go test ./... - - publish: - name: Publish container images - runs-on: ubuntu-latest - steps: - - name: Retrieve code - uses: actions/checkout@v2 - - - name: Gitlab variables - id: vars - run: build/ci/gitlab-vars - - - name: Login to GitHub Packages Docker Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.CR_PAT }} - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: neale - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - # Currently required, because buildx doesn't support auto-push from docker - - name: Set up builder - uses: docker/setup-buildx-action@v1 - id: buildx - - - name: Build and push moth image - uses: docker/build-push-action@v2 - with: - builder: ${{ steps.buildx.outputs.name }} - target: moth - file: build/package/Containerfile - push: true - platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 - tags: | - dirtbags/moth:${{ steps.vars.outputs.tag }} - ghcr.io/dirtbags/moth:${{ steps.vars.outputs.tag }} From 6f1f889be730867b014b3d3156797e1537ea9f08 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 13:10:24 -0600 Subject: [PATCH 38/98] Attempt to reproduce #154 --- cmd/transpile/main.go | 2 +- cmd/transpile/main_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 58a0bdb..f225464 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -81,7 +81,7 @@ func (t *T) ParseArgs() (Command, error) { default: fmt.Fprintln(t.Stderr, "ERROR:", t.Args[1], "is not a valid command") usage(t.Stderr) - return nothing, fmt.Errorf("Invalid command") + return nothing, fmt.Errorf("invalid command") } if err := flags.Parse(t.Args[2:]); err != nil { diff --git a/cmd/transpile/main_test.go b/cmd/transpile/main_test.go index e501337..042795a 100644 --- a/cmd/transpile/main_test.go +++ b/cmd/transpile/main_test.go @@ -4,7 +4,9 @@ import ( "archive/zip" "bytes" "encoding/json" + "fmt" "io/ioutil" + "os" "strings" "testing" @@ -202,3 +204,32 @@ func TestFilesystem(t *testing.T) { t.Error("Wrong file pulled", stdout.String()) } } + +func TestCwd(t *testing.T) { + testwd, err := os.Getwd() + if err != nil { + t.Error("Can't get current working directory!") + return + } + defer os.Chdir(testwd) + + stdin := new(bytes.Buffer) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + tp := T{ + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + BaseFs: afero.NewOsFs(), + } + + stdout.Reset() + os.Chdir("/") + if err := tp.Run( + "file", + fmt.Sprintf("-dir=%s/testdata/cat1/1", testwd), + "moo.txt", + ); err != nil { + t.Error(err) + } +} From 6e5e2c3adf382a2da102b5c902bb8ef3983869fd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:22:21 -0600 Subject: [PATCH 39/98] v4.3.3 release --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c87828..b8d207b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] +## [v4.3.3] - 2021-10-20 ### Fixed - Points awarded while scoring is paused are now correctly sorted (#168) - Writing a new mothball with the same name is now detected and the new mothball loaded (#172) @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Many error messages were changed to start with a lower-case letter, in order to satisfy a new linter check. - CI/CD moved to our Cyber Fire Gitlab instance +- I attempted to have the build thingy automatically build moth:v4 and moth:v4.3 and moth:v4.3.3 images, + but I can't test it without tagging a release. + So v4.3.4 might come out very soon after this ;) ## [v4.2.2] - 2021-09-30 ### Added From e1e9157841680eab025313ba90dd17674460b37e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:22:30 -0600 Subject: [PATCH 40/98] v4.3.3? --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d207b..fda815b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Points awarded while scoring is paused are now correctly sorted (#168) - Writing a new mothball with the same name is now detected and the new mothball loaded (#172) - Regression test for issue where URL path leading directories were ignored (#144) +- A few other very minor bugs were closed when I couldn't reproduce them or decided they weren't actually bugs. ### Changed - Many error messages were changed to start with a lower-case letter, From e15a505d7be080ea9f8c871ee3457b339d8e993d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:27:32 -0600 Subject: [PATCH 41/98] v4.4.4. Sigh. --- .gitlab-ci.yml | 2 +- CHANGELOG.md | 4 ++++ build/ci/ci.sh | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b6d291d..a1e5b42 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ push: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - mkdir ~/.docker - - echo "$DOCKER_AUTH_CONFIG" > ~/.docker/config.json + - echo "$DOCKER_AUTH_CONFIG" | tee ~/.docker/config.json | md5sum - sh build/ci/ci.sh publish - > docker build diff --git a/CHANGELOG.md b/CHANGELOG.md index fda815b..7913147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v4.4.4] - 2021-10-20 +### Changed +- Trying to get CI push of built images. I expect this to fail, too. But in a way that can help me debug the issue. + ## [v4.3.3] - 2021-10-20 ### Fixed - Points awarded while scoring is paused are now correctly sorted (#168) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 7bdb4e3..8e87f52 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -27,9 +27,9 @@ run () { tags () { pfx=$1 for base in $images; do - echo $pfx $base:${CI_COMMIT_REF_SLUG} - echo $pfx $base:${CI_COMMIT_REF_SLUG%.*} - echo $pfx $base:${CI_COMMIT_REF_SLUG%.*.*} + echo $pfx $base:${CI_COMMIT_REF_NAME} + echo $pfx $base:${CI_COMMIT_REF_NAME%.*} + echo $pfx $base:${CI_COMMIT_REF_NAME%.*.*} done | uniq } From 127beca1fc84c3432e67085ca441ef33e28b8553 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:35:21 -0600 Subject: [PATCH 42/98] Remove superfluous CI build script line in --- .gitlab-ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a1e5b42..fa43a60 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,8 +21,3 @@ push: - mkdir ~/.docker - echo "$DOCKER_AUTH_CONFIG" | tee ~/.docker/config.json | md5sum - sh build/ci/ci.sh publish - - > - docker build - --tag ghcr.io/dirtbags/moth:$CI_COMMIT_REF_SLUG - --file build/package/Containerfile - . From a2ce3682ab1770a6e77510173a68dccd4b64d606 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:47:32 -0600 Subject: [PATCH 43/98] Push images to docker hub, but say to use ghcr --- CHANGELOG.md | 4 ++++ README.md | 2 +- docs/development.md | 4 ++-- docs/getting-started.md | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7913147..21ba822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] - 2021-10-20 +### Added +- Images deploying to docker hub too. We're now at capacity for our Docker Hub team. + ## [v4.4.4] - 2021-10-20 ### Changed - Trying to get CI push of built images. I expect this to fail, too. But in a way that can help me debug the issue. diff --git a/README.md b/README.md index c6709cb..b78bab7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ You can read more about why we made these decisions in [philosophy](docs/philoso Run in demonstration mode =========== - docker run --rm -it -p 8080:8080 dirtbags/moth-devel + docker run --rm -it -p 8080:8080 ghcr.io/dirtbags/moth-devel Then open http://localhost:8080/ and check out the example puzzles. diff --git a/docs/development.md b/docs/development.md index 1a14b32..ccc789a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -25,12 +25,12 @@ so you can watch the access log and any error messages. ### Podman - podman run --rm -it -p 8080:8080 -v /srv/moth/puzzles:/puzzles:ro dirtbags/moth-devel + podman run --rm -it -p 8080:8080 -v /srv/moth/puzzles:/puzzles:ro ghcr.io/dirtbags/moth-devel ### Docker - docker run --rm -it -p 8080:8080 -v /srv/moth/puzzles:/puzzles:ro dirtbags/moth-devel + docker run --rm -it -p 8080:8080 -v /srv/moth/puzzles:/puzzles:ro ghcr.io/dirtbags/moth-devel ### Native diff --git a/docs/getting-started.md b/docs/getting-started.md index a1a9258..9cc6185 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -33,11 +33,11 @@ We're going to assume you put everything in `/srv/moth`, like we suggested. ### Podman - podman run --name=moth -d -v /srv/moth/mothballs:/mothballs:ro -v /srv/moth/state:/state dirtbags/moth + podman run --name=moth -d -v /srv/moth/mothballs:/mothballs:ro -v /srv/moth/state:/state ghcr.io/dirtbags/moth ### Docker - docker run --name=moth -d -v /srv/moth/mothballs:/mothballs:ro -v /srv/moth/state:/state dirtbags/moth + docker run --name=moth -d -v /srv/moth/mothballs:/mothballs:ro -v /srv/moth/state:/state ghcr.io/dirtbags/moth ### Native From 40f8f717785a36a63315cd4bc7ce17f432c3f9b1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 20 Oct 2021 14:49:40 -0600 Subject: [PATCH 44/98] oops, add in dockerhub repo to ci.sh --- build/ci/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/ci.sh b/build/ci/ci.sh index 8e87f52..773dba4 100755 --- a/build/ci/ci.sh +++ b/build/ci/ci.sh @@ -2,7 +2,7 @@ set -e -images="ghcr.io/dirtbags/moth" +images="ghcr.io/dirtbags/moth dirtbags/moth" ACTION=$1 if [ -z "$ACTION" ]; then From 2003b20cc411cd7a48708a50cd02a36d119c4031 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 21 Oct 2021 17:40:40 -0600 Subject: [PATCH 45/98] fix documentation error --- docs/logs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/logs.md b/docs/logs.md index 24e4704..2217f04 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -50,7 +50,7 @@ Each line has six fields minimum: | `timestamp` | `event` | `participantID` | `teamID` | `category` | `points` | `extra`... | | --- | --- | --- | --- | --- | --- | --- | | int | string | string | string | string | int | string... | -| Unix epoch | Event type | Team's unique ID| Participant's (hopefully) unique ID | Name of category, if any | Points awarded, if any | Additional fields, if any | +| Unix epoch | Event type | Participant's (hopefully) unique ID | Team's unique ID | Name of category, if any | Points awarded, if any | Additional fields, if any | Fields after `points` contain extra fields associated with the event. From ace940ba12f1e0a99823bd270322580d3f5ac8e2 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 21 Oct 2021 18:25:49 -0600 Subject: [PATCH 46/98] further update events.csv description --- docs/logs.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/logs.md b/docs/logs.md index 2217f04..a5b5422 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -41,10 +41,12 @@ Each line has four fields: 1602702913 2255 sequence 16 ``` -`events.log` format +`events.csv` format ---------------------- -The events log is a space-separated file. +The events log is a comma-separated variable (CSV) file. +It ought to import into any spreadsheet program painlessly. + Each line has six fields minimum: | `timestamp` | `event` | `participantID` | `teamID` | `category` | `points` | `extra`... | @@ -61,6 +63,7 @@ These may change in the future. * init: startup of server * disabled: points accumulation disabled * enabled: points accumulation re-enabled +* register: team registration * load: puzzle load * wrong: wrong answer submitted * correct: correct answer submitted @@ -68,14 +71,14 @@ These may change in the future. ### Example ``` -1602716345 init - - - - 0 -1602716349 load 2255 player5 sequence 1 -1602716450 load 4824 player3 sequence 1 -1602716359 correct 2255 player5 sequence 1 -1602716423 wrong 4824 player3 sequence 1 -1602716428 correct 4824 player3 sequence 1 -1602716530 correct 4824 player3 sequence 1 -1602716546 abduction 4824 player3 - 0 alien FM1490 +1602716345,init,-,-,-,-,0 +1602716349,load,2255,player5,sequence,1 +1602716450,load,4824,player3,sequence,1 +1602716359,correct,2255,player5,sequence,1 +1602716423,wrong,4824,player3,sequence,1 +1602716428,correct,4824,player3,sequence,1 +1602716530,correct,4824,player3,sequence,1 +1602716546,abduction,4824,player3,-,0,alien,FM1490 ``` The final entry is a made-up "alien abduction" entry, From 4bb68193199baa65732ff8fd4a52b5a487b35178 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 25 Oct 2021 13:37:56 -0600 Subject: [PATCH 47/98] Fix to reopening all mothballs every 2s #180 --- cmd/mothd/mothballs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 67b7679..f204975 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -168,6 +168,7 @@ func (m *Mothballs) refresh() { m.categories[categoryName] = zipCategory{ Fs: zipfs.New(zrc), Closer: f, + mtime: fi.ModTime(), } log.Println("Adding category:", categoryName) From 471ded7303fe40a0ab04f1e9ecd7047e13bd881e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 26 Oct 2021 12:48:23 -0600 Subject: [PATCH 48/98] Cache state --- CHANGELOG.md | 7 ++- cmd/mothd/httpd_test.go | 2 + cmd/mothd/server_test.go | 5 +- cmd/mothd/state.go | 115 +++++++++++++++++++++++++++++---------- cmd/mothd/state_test.go | 29 ++++++---- 5 files changed, 116 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ba822..6173708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] - 2021-10-20 +## [unreleased] - 2021-10-26 +### Added +- State is now cached in memory, in an attempt to reduce filesystem metadata operations, + which kill NFS. + +## [v4.4.5] - 2021-10-26 ### Added - Images deploying to docker hub too. We're now at capacity for our Docker Hub team. diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 6c623c3..fc4ae9a 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -70,6 +70,8 @@ func TestHttpd(t *testing.T) { t.Error("Register failed", r.Body.String()) } + time.Sleep(TestMaintenanceInterval) + if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` { diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 2434e22..6b6e621 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -80,13 +80,16 @@ func TestProdServer(t *testing.T) { t.Error("index.html wrong contents", contents) } + // Wait for refresh to pick everything up + time.Sleep(TestMaintenanceInterval) + { es := handler.ExportState() if es.Config.Devel { t.Error("Marked as development server", es.Config) } if len(es.Puzzles) != 1 { - t.Error("Puzzle categories wrong length") + t.Error("Puzzle categories wrong length", len(es.Puzzles)) } if es.Messages != "messages.html" { t.Error("Messages has wrong contents") diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index b8cb231..9d11621 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "time" "github.com/dirtbags/moth/pkg/award" @@ -42,6 +43,12 @@ type State struct { eventStream chan []string eventWriter *csv.Writer eventWriterFile afero.File + + // Caches, so we're not hammering NFS with metadata operations + teamNames map[string]string + pointsLog award.List + messages string + lock sync.RWMutex } // NewState returns a new State struct backed by the given Fs @@ -51,6 +58,8 @@ func NewState(fs afero.Fs) *State { Enabled: true, refreshNow: make(chan bool, 5), eventStream: make(chan []string, 80), + + teamNames: make(map[string]string), } if err := s.reopenEventLog(); err != nil { log.Fatal(err) @@ -120,16 +129,13 @@ func (s *State) updateEnabled() { // TeamName returns team name given a team ID. func (s *State) TeamName(teamID string) (string, error) { - teamFs := afero.NewBasePathFs(s.Fs, "teams") - teamNameBytes, err := afero.ReadFile(teamFs, teamID) - if os.IsNotExist(err) { + s.lock.RLock() + name, ok := s.teamNames[teamID] + s.lock.RUnlock() + if !ok { return "", fmt.Errorf("unregistered team ID: %s", teamID) - } else if err != nil { - return "", fmt.Errorf("unregistered team ID: %s (%s)", teamID, err) } - - teamName := strings.TrimSpace(string(teamNameBytes)) - return teamName, nil + return name, nil } // SetTeamName writes out team name. @@ -163,36 +169,26 @@ func (s *State) SetTeamName(teamID, teamName string) error { log.Printf("Setting team name [%s] in file %s", teamName, teamFilename) fmt.Fprintln(teamFile, teamName) teamFile.Close() + + s.refreshNow <- true + return nil } // PointsLog retrieves the current points log. func (s *State) PointsLog() award.List { - f, err := s.Open("points.log") - if err != nil { - log.Println(err) - return nil - } - defer f.Close() - - pointsLog := make(award.List, 0, 200) - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - cur, err := award.Parse(line) - if err != nil { - log.Printf("Skipping malformed award line %s: %s", line, err) - continue - } - pointsLog = append(pointsLog, cur) - } - return pointsLog + s.lock.RLock() + ret := make(award.List, len(s.pointsLog)) + copy(ret, s.pointsLog) + s.lock.RUnlock() + return ret } // Messages retrieves the current messages. func (s *State) Messages() string { - bMessages, _ := afero.ReadFile(s, "messages.html") - return string(bMessages) + s.lock.RLock() // It's not clear to me that this actually needs to happen + defer s.lock.RUnlock() + return s.messages } // AwardPoints gives points to teamID in category. @@ -260,12 +256,14 @@ func (s *State) collectPoints() { } duplicate := false - for _, e := range s.PointsLog() { + s.lock.RLock() + for _, e := range s.pointsLog { if awd.Equal(e) { duplicate = true break } } + s.lock.RUnlock() if duplicate { log.Print("Skipping duplicate points: ", awd.String()) @@ -279,6 +277,11 @@ func (s *State) collectPoints() { } fmt.Fprintln(logf, awd.String()) logf.Close() + + // Stick this on the cache too + s.lock.Lock() + s.pointsLog = append(s.pointsLog, awd) + s.lock.Unlock() } if err := s.Remove(filename); err != nil { @@ -402,12 +405,64 @@ func (s *State) reopenEventLog() error { return nil } +func (s *State) updateCaches() { + s.lock.Lock() + defer s.lock.Unlock() + + if f, err := s.Open("points.log"); err != nil { + log.Println(err) + } else { + defer f.Close() + + pointsLog := make(award.List, 0, 200) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + cur, err := award.Parse(line) + if err != nil { + log.Printf("Skipping malformed award line %s: %s", line, err) + continue + } + pointsLog = append(pointsLog, cur) + } + s.pointsLog = pointsLog + } + + { + // The compiler recognizes this as an optimization case + for k := range s.teamNames { + delete(s.teamNames, k) + } + + teamsFs := afero.NewBasePathFs(s.Fs, "teams") + if dirents, err := afero.ReadDir(teamsFs, "."); err != nil { + log.Printf("Reading team ids: %v", err) + } else { + for _, dirent := range dirents { + teamID := dirent.Name() + if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil { + log.Printf("Reading team %s: %v", teamID, err) + } else { + teamName := strings.TrimSpace(string(teamNameBytes)) + s.teamNames[teamID] = teamName + } + } + } + + } + + if bMessages, err := afero.ReadFile(s, "messages.html"); err == nil { + s.messages = string(bMessages) + } +} + func (s *State) refresh() { s.maybeInitialize() s.updateEnabled() if s.Enabled { s.collectPoints() } + s.updateCaches() } // Maintain performs housekeeping on a State struct. diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 5cb1097..21f1ea2 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -62,6 +62,7 @@ func TestState(t *testing.T) { if err := s.SetTeamName(teamID, "wat"); err == nil { t.Errorf("Registering team a second time didn't fail") } + s.refresh() if name, err := s.TeamName(teamID); err != nil { t.Error(err) } else if name != teamName { @@ -73,9 +74,6 @@ func TestState(t *testing.T) { if err := s.AwardPoints(teamID, category, points); err != nil { t.Error(err) } - if err := s.AwardPoints(teamID, category, points); err != nil { - t.Error("Two awards before refresh:", err) - } // Flex duplicate detection with different timestamp if f, err := s.Create("points.new/moo"); err != nil { t.Error("Creating duplicate points file:", err) @@ -83,24 +81,34 @@ func TestState(t *testing.T) { fmt.Fprintln(f, time.Now().Unix()+1, teamID, category, points) f.Close() } + + s.AwardPoints(teamID, category, points) s.refresh() + pl = s.PointsLog() + if len(pl) != 1 { + for i, award := range pl { + t.Logf("pl[%d] == %s", i, award.String()) + } + t.Errorf("After awarding duplicate points, points log has length %d", len(pl)) + } else if (pl[0].TeamID != teamID) || (pl[0].Category != category) || (pl[0].Points != points) { + t.Errorf("Incorrect logged award %v", pl) + } if err := s.AwardPoints(teamID, category, points); err == nil { - t.Error("Duplicate points award didn't fail") + t.Error("Duplicate points award after refresh didn't fail") } if err := s.AwardPoints(teamID, category, points+1); err != nil { t.Error("Awarding more points:", err) } - pl = s.PointsLog() - if len(pl) != 1 { - t.Errorf("After awarding points, points log has length %d", len(pl)) - } else if (pl[0].TeamID != teamID) || (pl[0].Category != category) || (pl[0].Points != points) { - t.Errorf("Incorrect logged award %v", pl) + s.refresh() + if len(s.PointsLog()) != 2 { + t.Errorf("There should be two awards") } afero.WriteFile(s, "points.log", []byte("intentional parse error\n"), 0644) + s.refresh() if len(s.PointsLog()) != 0 { t.Errorf("Intentional parse error breaks pointslog") } @@ -108,7 +116,8 @@ func TestState(t *testing.T) { t.Error(err) } s.refresh() - if len(s.PointsLog()) != 2 { + if len(s.PointsLog()) != 1 { + t.Log(s.PointsLog()) t.Error("Intentional parse error screws up all parsing") } From bb41697ba6b13d1896ba9526666a55c92034315e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 26 Oct 2021 13:33:57 -0600 Subject: [PATCH 49/98] v4.4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6173708..1d0aa54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] - 2021-10-26 +## [v4.4.6] - 2021-10-26 ### Added - State is now cached in memory, in an attempt to reduce filesystem metadata operations, which kill NFS. From b6eea388d9d5a6affd0a6eafa48db091158e697e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 5 Nov 2021 14:30:06 -0600 Subject: [PATCH 50/98] Remove github workflow --- .github/workflows/build+test.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/build+test.yml diff --git a/.github/workflows/build+test.yml b/.github/workflows/build+test.yml deleted file mode 100644 index b24e26f..0000000 --- a/.github/workflows/build+test.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Test - -jobs: - test-mothd: - name: Test mothd - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: 1.17 - - - name: Retrieve code - uses: actions/checkout@v2 - - - name: Test - run: go test ./... From eea674b1a45e1bde43a21d66d983ba1f8505fba2 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 13:30:44 -0600 Subject: [PATCH 51/98] Remove `events.csv` on init. Fixes #177 --- cmd/mothd/state.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 9d11621..d8f09d1 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -303,6 +303,7 @@ func (s *State) maybeInitialize() { s.Remove("enabled") s.Remove("hours.txt") s.Remove("points.log") + s.Remove("events.csv") s.Remove("messages.html") s.Remove("mothd.log") s.RemoveAll("points.tmp") From e5a3b26c934e77798166490dfab1bba174931a3c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 15:26:01 -0600 Subject: [PATCH 52/98] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0aa54..65d2a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v4.4.7] - 2022-05-10 +### Changed +- Initializing an instance now truncates `events.csv` + ## [v4.4.6] - 2021-10-26 ### Added - State is now cached in memory, in an attempt to reduce filesystem metadata operations, From 5b6555cd9adafb7d57aaa7b079e41f2ebc4185e5 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 17:47:26 -0600 Subject: [PATCH 53/98] Check team existence before registering. Fixes #156 --- cmd/mothd/issues_test.go | 28 ++++++++++++++++++++++++++++ cmd/mothd/server.go | 7 +++++-- cmd/mothd/state.go | 7 +++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 cmd/mothd/issues_test.go diff --git a/cmd/mothd/issues_test.go b/cmd/mothd/issues_test.go new file mode 100644 index 0000000..26a6f6d --- /dev/null +++ b/cmd/mothd/issues_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + "github.com/spf13/afero" +) + +func TestIssue156(t *testing.T) { + puzzles := NewTestMothballs() + state := NewTestState() + theme := NewTestTheme() + server := NewMothServer(Configuration{}, theme, state, puzzles) + + afero.WriteFile(state, "teams/bloop", []byte("bloop: the team"), 0644) + state.refresh() + + handler := server.NewHandler("", "bloop") + es := handler.ExportState() + if _, ok := es.TeamNames["self"]; !ok { + t.Fail() + } + + err := handler.Register("bloop: the other team") + if err != ErrAlreadyRegistered { + t.Fail() + } +} diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 608e6d6..f53ecdf 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -184,12 +184,15 @@ func (mh *MothRequestHandler) ExportState() *StateExport { return mh.exportStateIfRegistered(false) } -func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExport { +// Export state, replacing the team ID with "self" if the team is registered. +// +// If forceRegistered is true, go ahead and export it anyway +func (mh *MothRequestHandler) exportStateIfRegistered(forceRegistered bool) *StateExport { export := StateExport{} export.Config = mh.Config teamName, err := mh.State.TeamName(mh.teamID) - registered := override || mh.Config.Devel || (err == nil) + registered := forceRegistered || mh.Config.Devel || (err == nil) export.Messages = mh.State.Messages() export.TeamNames = make(map[string]string) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index d8f09d1..eca4d5b 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -141,6 +141,13 @@ func (s *State) TeamName(teamID string) (string, error) { // SetTeamName writes out team name. // This can only be done once per team. func (s *State) SetTeamName(teamID, teamName string) error { + s.lock.RLock() + _, ok := s.teamNames[teamID] + s.lock.RUnlock() + if ok { + return ErrAlreadyRegistered + } + idsFile, err := s.Open("teamids.txt") if err != nil { return fmt.Errorf("team IDs file does not exist") From 6d7fb9ebf51eb45f573e8e82b4670f97bf5cb4cc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 17:53:31 -0600 Subject: [PATCH 54/98] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d2a40..215c54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v4.4.8] - 2022-05-10 +### Changed +- You can now join with a team ID not appearing in `teamids.txt`, + as long as it is registered (in the `teams/` directory) + ## [v4.4.7] - 2022-05-10 ### Changed - Initializing an instance now truncates `events.csv` From d014384b05ec181ecf9a9bbb75455e5326bb4cc0 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 17:59:08 -0600 Subject: [PATCH 55/98] A possible fix for #179 --- cmd/mothd/server_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 6b6e621..ab4f986 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -13,11 +13,13 @@ const TestTeamID = "teamID" func NewTestServer() *MothServer { puzzles := NewTestMothballs() + puzzles.refresh() go puzzles.Maintain(TestMaintenanceInterval) state := NewTestState() afero.WriteFile(state, "teamids.txt", []byte("teamID\n"), 0644) afero.WriteFile(state, "messages.html", []byte("messages.html"), 0644) + state.refresh() go state.Maintain(TestMaintenanceInterval) theme := NewTestTheme() From be74961e942a9c57e92f98f1114c3a6b28d8ad0f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 19:11:47 -0600 Subject: [PATCH 56/98] still trying to fix race condition --- .gitlab-ci.yml | 2 +- cmd/mothd/httpd_test.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fa43a60..65fc796 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ stages: test: stage: test - image: golang:1.17 + image: golang:1.18 only: refs: - main diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index fc4ae9a..f5482d2 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -33,7 +33,9 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest } func TestHttpd(t *testing.T) { - hs := NewHTTPServer("/", NewTestServer()) + server := NewTestServer() + hs := NewHTTPServer("/", server) + stateProvider := server.State.(*State) if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -113,6 +115,7 @@ func TestHttpd(t *testing.T) { } time.Sleep(TestMaintenanceInterval) + stateProvider.refresh() if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) From 85f5b96a40be5b4f6bc114e805915c3ab658fba8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 19:36:36 -0600 Subject: [PATCH 57/98] Stop running goroutines in unit tests --- cmd/mothd/httpd_test.go | 5 ++--- cmd/mothd/server_test.go | 13 ++++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index f551df4..9ddee02 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -74,7 +74,7 @@ func TestHttpd(t *testing.T) { t.Error("Register failed", r.Body.String()) } - time.Sleep(TestMaintenanceInterval) + stateProvider.refresh() if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -131,7 +131,6 @@ func TestHttpd(t *testing.T) { t.Error("Unexpected body", r.Body.String()) } - time.Sleep(TestMaintenanceInterval) stateProvider.refresh() if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { @@ -149,7 +148,7 @@ func TestHttpd(t *testing.T) { log.Print(v) } - t.Errorf("Points log wrong length. Wanted 1, got %v", state.PointsLog) + t.Errorf("Points log wrong length. Wanted 1, got %v (length %d)", state.PointsLog, len(state.PointsLog)) } else if len(state.Puzzles["pategory"]) != 2 { t.Error("Didn't unlock next puzzle") } diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 0c49285..92c8839 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -3,12 +3,10 @@ package main import ( "io/ioutil" "testing" - "time" "github.com/spf13/afero" ) -const TestMaintenanceInterval = time.Millisecond * 1 const TestTeamID = "teamID" // NewTestServer creates a new MothServer with NewTestMothballs and some initial state. @@ -17,17 +15,14 @@ const TestTeamID = "teamID" func NewTestServer() *MothServer { puzzles := NewTestMothballs() puzzles.refresh() - go puzzles.Maintain(TestMaintenanceInterval) state := NewTestState() afero.WriteFile(state, "teamids.txt", []byte("teamID\n"), 0644) afero.WriteFile(state, "messages.html", []byte("messages.html"), 0644) state.refresh() - go state.Maintain(TestMaintenanceInterval) theme := NewTestTheme() afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644) - go theme.Maintain(TestMaintenanceInterval) return NewMothServer(Configuration{}, theme, state, puzzles) } @@ -54,6 +49,7 @@ func TestProdServer(t *testing.T) { teamID := TestTeamID server := NewTestServer() + state := server.State.(*State) handler := server.NewHandler(participantID, teamID) anonHandler := server.NewHandler("badParticipantId", "badTeamId") @@ -85,8 +81,7 @@ func TestProdServer(t *testing.T) { t.Error("index.html wrong contents", contents) } - // Wait for refresh to pick everything up - time.Sleep(TestMaintenanceInterval) + state.refresh() { es := handler.ExportState() @@ -139,7 +134,7 @@ func TestProdServer(t *testing.T) { t.Error("Right answer marked wrong", err) } - time.Sleep(TestMaintenanceInterval) + state.refresh() { es := handler.ExportState() @@ -168,7 +163,7 @@ func TestProdServer(t *testing.T) { t.Error("Right answer marked wrong:", err) } - time.Sleep(TestMaintenanceInterval) + state.refresh() { es := anonHandler.ExportState() From bde4b2c86ddc9da439bb3e13e897291b360a8283 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 19:48:51 -0600 Subject: [PATCH 58/98] A bit cleaner test interface, maybe --- cmd/mothd/httpd_test.go | 11 +++++------ cmd/mothd/server_test.go | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 9ddee02..385e188 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -36,8 +36,7 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest func TestHttpd(t *testing.T) { server := NewTestServer() - hs := NewHTTPServer("/", server) - stateProvider := server.State.(*State) + hs := NewHTTPServer("/", server.MothServer) if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -74,7 +73,7 @@ func TestHttpd(t *testing.T) { t.Error("Register failed", r.Body.String()) } - stateProvider.refresh() + server.refresh() if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -131,7 +130,7 @@ func TestHttpd(t *testing.T) { t.Error("Unexpected body", r.Body.String()) } - stateProvider.refresh() + server.refresh() if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) @@ -183,7 +182,7 @@ func TestDevelMemHttpd(t *testing.T) { srv := NewTestServer() { - hs := NewHTTPServer("/", srv) + hs := NewHTTPServer("/", srv.MothServer) if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 404 { t.Error("Should have gotten a 404 for mothballer in prod mode") @@ -192,7 +191,7 @@ func TestDevelMemHttpd(t *testing.T) { { srv.Config.Devel = true - hs := NewHTTPServer("/", srv) + hs := NewHTTPServer("/", srv.MothServer) if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 500 { t.Log(r.Body.String()) diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 92c8839..4be46c6 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -9,10 +9,14 @@ import ( const TestTeamID = "teamID" +type TestServer struct { + *MothServer +} + // NewTestServer creates a new MothServer with NewTestMothballs and some initial state. // // See function definition for details. -func NewTestServer() *MothServer { +func NewTestServer() TestServer { puzzles := NewTestMothballs() puzzles.refresh() @@ -24,7 +28,14 @@ func NewTestServer() *MothServer { theme := NewTestTheme() afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644) - return NewMothServer(Configuration{}, theme, state, puzzles) + return TestServer{NewMothServer(Configuration{}, theme, state, puzzles)} +} + +func (ts TestServer) refresh() { + ts.State.(*State).refresh() + for _, pp := range ts.PuzzleProviders { + pp.(*Mothballs).refresh() + } } func TestDevelServer(t *testing.T) { @@ -49,7 +60,6 @@ func TestProdServer(t *testing.T) { teamID := TestTeamID server := NewTestServer() - state := server.State.(*State) handler := server.NewHandler(participantID, teamID) anonHandler := server.NewHandler("badParticipantId", "badTeamId") @@ -81,7 +91,7 @@ func TestProdServer(t *testing.T) { t.Error("index.html wrong contents", contents) } - state.refresh() + server.refresh() { es := handler.ExportState() @@ -134,7 +144,7 @@ func TestProdServer(t *testing.T) { t.Error("Right answer marked wrong", err) } - state.refresh() + server.refresh() { es := handler.ExportState() @@ -163,7 +173,7 @@ func TestProdServer(t *testing.T) { t.Error("Right answer marked wrong:", err) } - state.refresh() + server.refresh() { es := anonHandler.ExportState() From 243fdfd0063d1dbc8e7acebaf24c01c8b8ab230a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 19:57:07 -0600 Subject: [PATCH 59/98] Remove some debugging --- cmd/mothd/httpd_test.go | 36 ------------------------------------ cmd/mothd/mothballs.go | 7 ------- cmd/mothd/server_test.go | 1 + 3 files changed, 1 insertion(+), 43 deletions(-) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 385e188..e5a51a7 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -4,11 +4,9 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" "log" "net/http/httptest" "net/url" - "strings" "testing" "github.com/spf13/afero" @@ -111,21 +109,6 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if strings.Contains(r.Body.String(), "incorrect answer") { - // Pernicious intermittent bug - t.Error("Incorrect answer that was actually correct") - for _, provider := range server.PuzzleProviders { - if mb, ok := provider.(*Mothballs); !ok { - t.Error("Provider is not a mothball") - } else { - cat, _ := mb.getCat("pategory") - f, _ := cat.Open("answers.txt") - defer f.Close() - answersBytes, _ := ioutil.ReadAll(f) - t.Errorf("Correct answers: %v", string(answersBytes)) - } - } - t.Error("Wrong answer") } else if r.Body.String() != `{"status":"success","data":{"short":"accepted","description":"1 points awarded in pategory"}}` { t.Error("Unexpected body", r.Body.String()) } @@ -154,25 +137,6 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if strings.Contains(r.Body.String(), "incorrect answer") { - // Pernicious intermittent bug - t.Error("Incorrect answer that was actually correct") - for _, provider := range server.PuzzleProviders { - if mb, ok := provider.(*Mothballs); !ok { - t.Error("Provider is not a mothball") - } else { - if cat, ok := mb.getCat("pategory"); !ok { - t.Error("opening pategory failed") - } else if f, err := cat.Open("answers.txt"); err != nil { - t.Error("opening answers.txt", err) - } else { - defer f.Close() - answersBytes, _ := ioutil.ReadAll(f) - t.Errorf("Correct answers: %#v len %d", string(answersBytes), len(answersBytes)) - } - } - } - t.Error("Wrong answer") } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` { t.Error("Unexpected body", r.Body.String()) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index bc1ff72..f204975 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -92,30 +92,23 @@ func (m *Mothballs) Inventory() []Category { func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) { zfs, ok := m.getCat(cat) if !ok { - log.Println("There's no such category") return false, fmt.Errorf("no such category: %s", cat) } - log.Println("Opening answers.txt") af, err := zfs.Open("answers.txt") if err != nil { - log.Println("I did not find an answer") return false, fmt.Errorf("no answers.txt file") } defer af.Close() - log.Println("I'm going to start looking for an answer") needle := fmt.Sprintf("%d %s", points, answer) scanner := bufio.NewScanner(af) for scanner.Scan() { - log.Println("testing equality between", scanner.Text(), needle) if scanner.Text() == needle { return true, nil } } - log.Println("I did not find the answer", answer) - return false, nil } diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 4be46c6..caaf61a 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -36,6 +36,7 @@ func (ts TestServer) refresh() { for _, pp := range ts.PuzzleProviders { pp.(*Mothballs).refresh() } + ts.Theme.(*Theme).refresh() } func TestDevelServer(t *testing.T) { From cbe231ef12b11f6887a24ef0d93740b11f42d162 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 May 2022 20:05:16 -0600 Subject: [PATCH 60/98] Remove more debugging --- cmd/mothd/httpd_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index e5a51a7..b56f56c 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "log" "net/http/httptest" "net/url" "testing" @@ -125,11 +124,6 @@ func TestHttpd(t *testing.T) { } else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil { t.Error(err) } else if len(state.PointsLog) != 1 { - switch v := server.State.(type) { - case *State: - log.Print(v) - } - t.Errorf("Points log wrong length. Wanted 1, got %v (length %d)", state.PointsLog, len(state.PointsLog)) } else if len(state.Puzzles["pategory"]) != 2 { t.Error("Didn't unlock next puzzle") From a85df224797ea3a2e83e708a8826cd691d76c85f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 12 May 2022 18:03:26 -0600 Subject: [PATCH 61/98] Upgrades, NFS optimization --- cmd/mothd/state.go | 49 +++--- go.mod | 6 +- go.sum | 420 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+), 23 deletions(-) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index eca4d5b..aa5a1f0 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -45,10 +45,11 @@ type State struct { eventWriterFile afero.File // Caches, so we're not hammering NFS with metadata operations - teamNames map[string]string - pointsLog award.List - messages string - lock sync.RWMutex + teamNamesLastChange time.Time + teamNames map[string]string + pointsLog award.List + messages string + lock sync.RWMutex } // NewState returns a new State struct backed by the given Fs @@ -436,27 +437,35 @@ func (s *State) updateCaches() { s.pointsLog = pointsLog } + // Only do this if the teams directory has a newer mtime; directories with + // hundreds of team names can cause NFS I/O storms { - // The compiler recognizes this as an optimization case - for k := range s.teamNames { - delete(s.teamNames, k) - } + _, ismmfs := s.Fs.(*afero.MemMapFs) // Tests run so quickly that the time check isn't precise enough + if fi, err := s.Fs.Stat("teams"); err != nil { + log.Printf("Getting modification time of teams directory: %v", err) + } else if ismmfs || s.teamNamesLastChange.Before(fi.ModTime()) { + s.teamNamesLastChange = fi.ModTime() - teamsFs := afero.NewBasePathFs(s.Fs, "teams") - if dirents, err := afero.ReadDir(teamsFs, "."); err != nil { - log.Printf("Reading team ids: %v", err) - } else { - for _, dirent := range dirents { - teamID := dirent.Name() - if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil { - log.Printf("Reading team %s: %v", teamID, err) - } else { - teamName := strings.TrimSpace(string(teamNameBytes)) - s.teamNames[teamID] = teamName + // The compiler recognizes this as an optimization case + for k := range s.teamNames { + delete(s.teamNames, k) + } + + teamsFs := afero.NewBasePathFs(s.Fs, "teams") + if dirents, err := afero.ReadDir(teamsFs, "."); err != nil { + log.Printf("Reading team ids: %v", err) + } else { + for _, dirent := range dirents { + teamID := dirent.Name() + if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil { + log.Printf("Reading team %s: %v", teamID, err) + } else { + teamName := strings.TrimSpace(string(teamNameBytes)) + s.teamNames[teamID] = teamName + } } } } - } if bMessages, err := afero.ReadFile(s, "messages.html"); err == nil { diff --git a/go.mod b/go.mod index b2e9f71..74b10f6 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.13 require ( github.com/kr/text v0.2.0 // indirect - github.com/spf13/afero v1.5.1 - github.com/yuin/goldmark v1.3.1 - golang.org/x/text v0.3.5 // indirect + github.com/spf13/afero v1.8.2 + github.com/yuin/goldmark v1.4.12 + golang.org/x/text v0.3.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 8af9236..ea814aa 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,125 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -9,37 +128,338 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8= github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I= github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= +github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 8e0f4561a56b4312914c22b5658d4e8819d9df3a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 12 May 2022 18:15:46 -0600 Subject: [PATCH 62/98] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 215c54a..b87666a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v4.4.9] - 2022-05-12 +### Changed +- Added a performance optimization for events with a large number of teams + backed by NFS + ## [v4.4.8] - 2022-05-10 ### Changed - You can now join with a team ID not appearing in `teamids.txt`, From 92d904150a6f1086404101cae90842d36a04cba2 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 21 Oct 2022 14:52:26 -0700 Subject: [PATCH 63/98] Add better reporting on unit tests --- .gitlab-ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1cd0d50..5c50c8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,8 +11,15 @@ test: - main - merge_requests script: - - go test -race ./... - + - go install + - go test -coverprofile=coverage.txt -covermode=atomic -race ./... + - go get github.com/boumenot/gocover-cobertura + - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml push: stage: push rules: From 466de2d9c627060aaa84ab86e2c5799ff28100bf Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 21 Oct 2022 14:54:00 -0700 Subject: [PATCH 64/98] Let the test handler take care of installing stuff --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5c50c8f..fc59a1b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,6 @@ test: - main - merge_requests script: - - go install - go test -coverprofile=coverage.txt -covermode=atomic -race ./... - go get github.com/boumenot/gocover-cobertura - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml From 3bd1cdcc56b2d265b69982ae3f54ea5ed98f3e8b Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 25 Oct 2022 20:37:56 +0000 Subject: [PATCH 65/98] Add overall report coverage number --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fc59a1b..1ee5cae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,8 @@ test: - go test -coverprofile=coverage.txt -covermode=atomic -race ./... - go get github.com/boumenot/gocover-cobertura - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml + - go tool cover -func coverage.txt + coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/ artifacts: reports: coverage_report: From 190657f2fa5d2c9f6fe1ca02b774d74efac98025 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 25 Oct 2022 13:48:58 -0700 Subject: [PATCH 66/98] Add more coverage artifacts --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ee5cae..2a2d301 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,7 @@ test: - go test -coverprofile=coverage.txt -covermode=atomic -race ./... - go get github.com/boumenot/gocover-cobertura - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml + - go tool cover -html=coverage.txt -o coverage.html - go tool cover -func coverage.txt coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/ artifacts: @@ -21,6 +22,9 @@ test: coverage_report: coverage_format: cobertura path: coverage.xml + paths: + - coverage.html + - coverage.txt push: stage: push rules: From 7925547daf731446d6e1ea3155a13c37b0de25ba Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Oct 2022 12:15:00 -0700 Subject: [PATCH 67/98] Coverage xml wasn't getting added, now --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2a2d301..a802f5e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,7 @@ test: paths: - coverage.html - coverage.txt + - coverage.xml push: stage: push rules: From c3a7ee0d4f1651ca3d9eea849ea528b9c5731450 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Oct 2022 19:27:20 +0000 Subject: [PATCH 68/98] Pull XML reporting into its own job, so it still shows up --- .gitlab-ci.yml | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a802f5e..f697f2d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,32 +2,42 @@ stages: - test - push -test: +Run unit tests: stage: test - - image: golang:1.18 + image: &goimage golang:1.18 only: refs: - main - merge_requests script: - go test -coverprofile=coverage.txt -covermode=atomic -race ./... - - go get github.com/boumenot/gocover-cobertura - - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml - go tool cover -html=coverage.txt -o coverage.html - go tool cover -func coverage.txt coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/ + artifacts: + paths: + - coverage.html + - coverage.txt +Generage coverage XML: + stage: test + image: *goimage + needs: ["Run unit tests"] + script: + - go get github.com/boumenot/gocover-cobertura + - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml + only: + refs: + - main + - merge_requests artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml - paths: - - coverage.html - - coverage.txt - - coverage.xml + push: stage: push + needs: ["Run unit tests"] rules: - if: $CI_COMMIT_TAG - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 887e4b3eaf27bdd9d5f7433132bccdf7d09d4304 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 29 Nov 2022 15:48:35 -0700 Subject: [PATCH 69/98] Remove disabled, better hours.txt logs --- cmd/mothd/state.go | 55 ++++++++++++++-------------- cmd/mothd/state_test.go | 54 +++++++++++++++++++--------- docs/FAQ.md | 79 +++++++++++++++++++++++++++++++++++++++++ docs/administration.md | 9 ++--- docs/overview.md | 21 +++-------- 5 files changed, 153 insertions(+), 65 deletions(-) create mode 100644 docs/FAQ.md diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index aa5a1f0..76fe13e 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -39,6 +39,7 @@ type State struct { // Enabled tracks whether the current State system is processing updates Enabled bool + enabledWhy string refreshNow chan bool eventStream chan []string eventWriter *csv.Writer @@ -71,11 +72,10 @@ func NewState(fs afero.Fs) *State { // updateEnabled checks a few things to see if this state directory is "enabled". func (s *State) updateEnabled() { nextEnabled := true - why := "`state/enabled` present, `state/hours.txt` missing" + why := "state/hours.txt has no timestamps before now" if untilFile, err := s.Open("hours.txt"); err == nil { defer untilFile.Close() - why = "`state/hours.txt` present" scanner := bufio.NewScanner(untilFile) for scanner.Scan() { @@ -95,35 +95,36 @@ func (s *State) updateEnabled() { case '#': continue default: - log.Println("Misformatted line in hours.txt file") + log.Println("state/hours.txt has bad line:", line) } + line, _, _ = strings.Cut(line, "#") // Remove inline comments line = strings.TrimSpace(line) - until, err := time.Parse(time.RFC3339, line) - if err != nil { - until, err = time.Parse(RFC3339Space, line) - } - if err != nil { - log.Println("Suspended: Unparseable until date:", line) + until := time.Time{} + if len(line) == 0 { + // Let it stay as zero time, so it's always before now + } else if until, err = time.Parse(time.RFC3339, line); err == nil { + // Great, it was RFC 3339 + } else if until, err = time.Parse(RFC3339Space, line); err == nil { + // Great, it was RFC 3339 with a space instead of a 'T' + } else { + log.Println("state/hours.txt has bad timestamp:", line) continue } if until.Before(time.Now()) { nextEnabled = thisEnabled + why = fmt.Sprint("state/hours.txt most recent timestamp:", line) } } } - if _, err := s.Stat("enabled"); os.IsNotExist(err) { - nextEnabled = false - why = "`state/enabled` missing" - } - - if nextEnabled != s.Enabled { + if (nextEnabled != s.Enabled) || (why != s.enabledWhy) { s.Enabled = nextEnabled - log.Printf("Setting enabled=%v: %s", s.Enabled, why) + s.enabledWhy = why + log.Printf("Setting enabled=%v: %s", s.Enabled, s.enabledWhy) if s.Enabled { - s.LogEvent("enabled", "", "", "", 0, why) + s.LogEvent("enabled", "", "", "", 0, s.enabledWhy) } else { - s.LogEvent("disabled", "", "", "", 0, why) + s.LogEvent("disabled", "", "", "", 0, s.enabledWhy) } } } @@ -350,21 +351,19 @@ func (s *State) maybeInitialize() { f.Close() } - if f, err := s.Create("enabled"); err == nil { - fmt.Fprintln(f, "enabled: remove or rename to suspend the contest.") - f.Close() - } - if f, err := s.Create("hours.txt"); err == nil { fmt.Fprintln(f, "# hours.txt: when the contest is enabled") fmt.Fprintln(f, "#") - fmt.Fprintln(f, "# Enable: + timestamp") - fmt.Fprintln(f, "# Disable: - timestamp") + fmt.Fprintln(f, "# Enable: + [timestamp]") + fmt.Fprintln(f, "# Disable: - [timestamp]") fmt.Fprintln(f, "#") - fmt.Fprintln(f, "# You can have multiple start/stop times.") - fmt.Fprintln(f, "# Whatever time is the most recent, wins.") - fmt.Fprintln(f, "# Times in the future are ignored.") + fmt.Fprintln(f, "# This file, and all files in this directory, are re-read periodically.") + fmt.Fprintln(f, "# Default is enabled.") + fmt.Fprintln(f, "# Rules with only '-' or '+' are also allowed.") + fmt.Fprintln(f, "# Rules apply from the top down.") + fmt.Fprintln(f, "# If you put something in out of order, it's going to be bonkers.") fmt.Fprintln(f) + fmt.Fprintln(f, "- 1970-01-01T00:00:00Z") fmt.Fprintln(f, "+", now) fmt.Fprintln(f, "- 2519-10-31T00:00:00Z") f.Close() diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 21f1ea2..0101dec 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -17,8 +17,16 @@ func NewTestState() *State { return s } +func slurp(c chan bool) { + for range c { + // Nothing + } +} + func TestState(t *testing.T) { s := NewTestState() + defer close(s.refreshNow) + go slurp(s.refreshNow) mustExist := func(path string) { _, err := s.Fs.Stat(path) @@ -33,7 +41,6 @@ func TestState(t *testing.T) { } mustExist("initialized") - mustExist("enabled") mustExist("hours.txt") teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt") @@ -163,6 +170,9 @@ func TestStateEvents(t *testing.T) { if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init::::0" { t.Error("Wrong message from event stream:", msg) } + if msg := <-s.eventStream; !strings.HasPrefix(msg[6], "state/hours.txt") { + t.Error("Wrong message from event stream:", msg[6]) + } if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo::::0" { t.Error("Wrong message from event stream:", msg) } @@ -184,19 +194,37 @@ func TestStateDisabled(t *testing.T) { t.Error(err) } defer hoursFile.Close() + s.refresh() + if !s.Enabled { + t.Error("Empty hours.txt not enabled") + } fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z") hoursFile.Sync() s.refresh() if s.Enabled { - t.Error("Disabling 1970-01-01") + t.Error("1970-01-01") } - fmt.Fprintln(hoursFile, "+ 1970-01-01 01:01:01+05:00") + fmt.Fprintln(hoursFile, "+ 1970-01-02 01:01:01+05:00") hoursFile.Sync() s.refresh() if !s.Enabled { - t.Error("Enabling 1970-01-02") + t.Error("1970-01-02") + } + + fmt.Fprintln(hoursFile, "-") + hoursFile.Sync() + s.refresh() + if s.Enabled { + t.Error("bare -") + } + + fmt.Fprintln(hoursFile, "+") + hoursFile.Sync() + s.refresh() + if !s.Enabled { + t.Error("bare +") } fmt.Fprintln(hoursFile, "") @@ -204,7 +232,7 @@ func TestStateDisabled(t *testing.T) { hoursFile.Sync() s.refresh() if !s.Enabled { - t.Error("Comments") + t.Error("Comment") } fmt.Fprintln(hoursFile, "intentional parse error") @@ -218,7 +246,7 @@ func TestStateDisabled(t *testing.T) { hoursFile.Sync() s.refresh() if s.Enabled { - t.Error("Disabling 1980-01-01") + t.Error("1980-01-01") } if err := s.Remove("hours.txt"); err != nil { @@ -229,14 +257,6 @@ func TestStateDisabled(t *testing.T) { t.Error("Removing `hours.txt` disabled event") } - if err := s.Remove("enabled"); err != nil { - t.Error(err) - } - s.refresh() - if s.Enabled { - t.Error("Removing `enabled` didn't disable") - } - s.Remove("initialized") s.refresh() if !s.Enabled { @@ -291,11 +311,11 @@ func TestStateMaintainer(t *testing.T) { eventLog, err := afero.ReadFile(s.Fs, "events.csv") if err != nil { t.Error(err) - } else if events := strings.Split(string(eventLog), "\n"); len(events) != 3 { + } else if events := strings.Split(string(eventLog), "\n"); len(events) != 4 { t.Log("Events:", events) t.Error("Wrong event log length:", len(events)) - } else if events[2] != "" { - t.Error("Event log didn't end with newline") + } else if events[3] != "" { + t.Error("Event log didn't end with newline", events) } } diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..4ea7496 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,79 @@ +Frequently Asked Questions +================= + +I should probably move this somewhere else, +since most of it is about + +Main Application Questions +===================== + +## Can we add some instructions to the user interface? It's confusing. + +The lack of instruction was a deliberate design decision made about 9 years ago +when we found in A/B testing that college students are a lot more motivated by +vague instruction and mystery than precise instruction. We've since found that +people who are inclined to "play" our events are similarly motivated by +weirdness and mystery: they enjoy fiddling around with things until they've +worked it out experimentally. + +Oddly, the group who seems to be the most perturbed by the vagueness is +professionals. This may be because many of these folks spend long amounts of +time trying to make things accessible and precise, and this looks like a train +wreck from that perspective. + +Another way to think about it: this is supposed to be a game, like Super Mario +Brothers. We were very careful about designing the puzzles so that you could +learn by playing. The whimsical design is meant to make it feel like trying +things out will not result in a catastrophic failure anywhere, and we've found +that most people figure it out very quickly without any instruction at all, +despite feeling a little confused or disoriented at first. + +## Why can't I choose my team from a list when I log in? + +We actually started this way, but we quickly learned that there were exploitable +attack avenues available when any participant can join any team. One individual +in 2010, having a bad day, decided to enter every answer they had, for every +team in the contest, as a way of sabotaging the event. It worked: everyone's +motivation to try and solve puzzles tanked, and people were angry that they'd +been working on content only to find that they already had the points. + +## Why won't you add this helpful text to the login page? + +It has been our experience that the more words we have on that page, the less +likely any of them will be read. We strive now to have no instruction at all, +and to design the interface in a way that it's obvious what you have to do. + +## Why aren't we providing a link to the scoreboard? + +It's because the scoreboard looks horrible on a mobile phone: +it was designed for a projector. +Once we have a scoreboard that is readable on mobile, +I'll add that link. + +## Why can't we show a list of teams to log in to? + +At a previous event, +we had a participant log in as other teams and solve every puzzle, +because they were upset about something. +This ruined the event for everyone, +because it took away the challenge of scoring points. + + +Scoreboard Questions +================= + +## Why are there no links or title on the scoreboard? + +The scoreboard is supposed to be projected at events, to participants. The current scoreboard isn't something we intend participants to pull up on their mobile devices or laptops. + +Think of the scoreboard as sort of like the menu screens at Burger King. + + +## Will you change the scoreboard color scheme? + +The scoreboard colors and layout were carefully chosen to be distinguishable for +all forms of color blindness, and even accessible by users with total blindness +using screen readers. This is why we decided to put the category name inside the +bar and just deal with it being a little weird. + +I'm open to suggestions, but they need to work for all users. diff --git a/docs/administration.md b/docs/administration.md index 5690373..a95d5cb 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -45,8 +45,8 @@ Scores Pausing/resuming scoring ------------------- - rm /srv/moth/state/enabled # Pause scoring - touch /srv/moth/state/enabled # Resume scoring + echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring + sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring When scoring is paused, participants can still submit answers, @@ -54,12 +54,13 @@ and the system will tell them whether the answer is correct. As soon as you unpause, all correctly-submitted answers will be scored. + Adjusting scores ------------------ - rm /srv/moth/state/enabled # Suspend scoring + echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring nano /srv/moth/state/points.log # Replace nano with your preferred editor - touch /srv/moth/state/enabled # Resume scoring + sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring We don't warn participants before we do this: any points scored while scoring is suspended are queued up and processed as soon as scoreing is resumed. diff --git a/docs/overview.md b/docs/overview.md index 5242aed..ec722cb 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -33,24 +33,13 @@ The state directory is also used to communicate actions to mothd. Remove this file to reset the state. This will blow away team assignments and the points log. -`disabled` ----------- - -Create this file to pause collection of points and other maintenance. -Contestants can still submit answers, -but they won't show up on the scoreboard until you remove this file. - -This file does not normally exist. - - -`until` +`hours.txt` ------- -Put an RFC3337 date/time stamp in here to have the server pause itself at a given time. -Remember that time zones exist! -I recommend always using Zulu time. - -This file does not normally exist. +A list of start and stop hours. +If all the hours are in the future, the event defaults to running. +"Stop" here just pertains to scoreboard updates and puzzle unlocking. +People can still submit answers and their awards are queued up for the next start. `teamids.txt` From ded29f92c11b6db77c176450805a73279fe4abc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Feb 2023 05:10:45 +0000 Subject: [PATCH 70/98] Bump golang.org/x/text from 0.3.7 to 0.3.8 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 35 ++++++++++++----------------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 74b10f6..af3862d 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( github.com/kr/text v0.2.0 // indirect github.com/spf13/afero v1.8.2 - github.com/yuin/goldmark v1.4.12 - golang.org/x/text v0.3.7 // indirect + github.com/yuin/goldmark v1.4.13 + golang.org/x/text v0.3.8 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index ea814aa..ca4b9b0 100644 --- a/go.sum +++ b/go.sum @@ -123,22 +123,14 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8= -github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= -github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -148,12 +140,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I= -github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= -github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -163,10 +152,10 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -201,6 +190,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -232,6 +222,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -251,6 +242,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -285,22 +277,22 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -347,8 +339,8 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -441,15 +433,12 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 67e8dda39df80232d60d11691f68c9ac5d91e9e8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 23 Mar 2023 14:28:11 -0600 Subject: [PATCH 71/98] Remove use of participant ID fixes #176 --- cmd/mothd/httpd.go | 3 +-- cmd/mothd/httpd_test.go | 3 --- cmd/mothd/issues_test.go | 2 +- cmd/mothd/server.go | 20 +++++++++----------- cmd/mothd/server_test.go | 7 +++---- cmd/mothd/state.go | 9 ++++----- cmd/mothd/state_test.go | 16 ++++++++-------- docs/logs.md | 8 ++++---- theme/index.html | 5 ----- theme/moth.js | 9 +-------- 10 files changed, 31 insertions(+), 51 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 54ca156..f9cc27c 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -44,9 +44,8 @@ func (h *HTTPServer) HandleMothFunc( mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request), ) { handler := func(w http.ResponseWriter, req *http.Request) { - participantID := req.FormValue("pid") teamID := req.FormValue("id") - mh := h.server.NewHandler(participantID, teamID) + mh := h.server.NewHandler(teamID) mothHandler(mh, w, req) } h.HandleFunc(h.base+pattern, handler) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index b56f56c..f77e044 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -11,11 +11,8 @@ import ( "github.com/spf13/afero" ) -const TestParticipantID = "shipox" - func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest.ResponseRecorder { vals := url.Values{} - vals.Set("pid", TestParticipantID) vals.Set("id", TestTeamID) for k, v := range args { vals.Set(k, v) diff --git a/cmd/mothd/issues_test.go b/cmd/mothd/issues_test.go index 26a6f6d..fefd29a 100644 --- a/cmd/mothd/issues_test.go +++ b/cmd/mothd/issues_test.go @@ -15,7 +15,7 @@ func TestIssue156(t *testing.T) { afero.WriteFile(state, "teams/bloop", []byte("bloop: the team"), 0644) state.refresh() - handler := server.NewHandler("", "bloop") + handler := server.NewHandler("bloop") es := handler.ExportState() if _, ok := es.TeamNames["self"]; !ok { t.Fail() diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 0c5de3f..c7fe1f0 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -58,7 +58,7 @@ type StateProvider interface { TeamName(teamID string) (string, error) SetTeamName(teamID, teamName string) error AwardPoints(teamID string, cat string, points int) error - LogEvent(event, participantID, teamID, cat string, points int, extra ...string) + LogEvent(event, teamID, cat string, points int, extra ...string) Maintainer } @@ -92,19 +92,17 @@ func NewMothServer(config Configuration, theme ThemeProvider, state StateProvide } // NewHandler returns a new http.RequestHandler for the provided teamID. -func (s *MothServer) NewHandler(participantID, teamID string) MothRequestHandler { +func (s *MothServer) NewHandler(teamID string) MothRequestHandler { return MothRequestHandler{ - MothServer: s, - participantID: participantID, - teamID: teamID, + MothServer: s, + teamID: teamID, } } // MothRequestHandler provides http.RequestHandler for a MothServer. type MothRequestHandler struct { *MothServer - participantID string - teamID string + teamID string } // PuzzlesOpen opens a file associated with a puzzle. @@ -131,7 +129,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) ( // Log puzzle.json loads if path == "puzzle.json" { - mh.State.LogEvent("load", mh.participantID, mh.teamID, cat, points) + mh.State.LogEvent("load", mh.teamID, cat, points) } return @@ -148,11 +146,11 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) } } if !correct { - mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points) + mh.State.LogEvent("wrong", mh.teamID, cat, points) return fmt.Errorf("incorrect answer") } - mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points) + mh.State.LogEvent("correct", mh.teamID, cat, points) if _, err := mh.State.TeamName(mh.teamID); err != nil { return fmt.Errorf("invalid team ID") @@ -175,7 +173,7 @@ func (mh *MothRequestHandler) Register(teamName string) error { if teamName == "" { return fmt.Errorf("empty team name") } - mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0) + mh.State.LogEvent("register", mh.teamID, "", 0) return mh.State.SetTeamName(mh.teamID, teamName) } diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index caaf61a..828d08b 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -42,7 +42,7 @@ func (ts TestServer) refresh() { func TestDevelServer(t *testing.T) { server := NewTestServer() server.Config.Devel = true - anonHandler := server.NewHandler("badParticipantId", "badTeamId") + anonHandler := server.NewHandler("badTeamId") { es := anonHandler.ExportState() @@ -57,12 +57,11 @@ func TestDevelServer(t *testing.T) { func TestProdServer(t *testing.T) { teamName := "OurTeam" - participantID := "participantID" teamID := TestTeamID server := NewTestServer() - handler := server.NewHandler(participantID, teamID) - anonHandler := server.NewHandler("badParticipantId", "badTeamId") + handler := server.NewHandler(teamID) + anonHandler := server.NewHandler("badTeamId") { es := handler.ExportState() diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 76fe13e..c611d88 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -122,9 +122,9 @@ func (s *State) updateEnabled() { s.enabledWhy = why log.Printf("Setting enabled=%v: %s", s.Enabled, s.enabledWhy) if s.Enabled { - s.LogEvent("enabled", "", "", "", 0, s.enabledWhy) + s.LogEvent("enabled", "", "", 0, s.enabledWhy) } else { - s.LogEvent("disabled", "", "", "", 0, s.enabledWhy) + s.LogEvent("disabled", "", "", 0, s.enabledWhy) } } } @@ -323,7 +323,7 @@ func (s *State) maybeInitialize() { if err := s.reopenEventLog(); err != nil { log.Fatal(err) } - s.LogEvent("init", "", "", "", 0) + s.LogEvent("init", "", "", 0) // Make sure various subdirectories exist s.Mkdir("points.tmp", 0755) @@ -380,12 +380,11 @@ func (s *State) maybeInitialize() { } // LogEvent writes to the event log -func (s *State) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) { +func (s *State) LogEvent(event, teamID, cat string, points int, extra ...string) { s.eventStream <- append( []string{ strconv.FormatInt(time.Now().Unix(), 10), event, - participantID, teamID, cat, strconv.Itoa(points), diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 0101dec..ed7c9e2 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -164,19 +164,19 @@ func TestStateOutOfOrderAward(t *testing.T) { func TestStateEvents(t *testing.T) { s := NewTestState() - s.LogEvent("moo", "", "", "", 0) - s.LogEvent("moo 2", "", "", "", 0) + s.LogEvent("moo", "", "", 0) + s.LogEvent("moo 2", "", "", 0) - if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init::::0" { + if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init:::0" { t.Error("Wrong message from event stream:", msg) } - if msg := <-s.eventStream; !strings.HasPrefix(msg[6], "state/hours.txt") { - t.Error("Wrong message from event stream:", msg[6]) + if msg := <-s.eventStream; !strings.HasPrefix(msg[5], "state/hours.txt") { + t.Error("Wrong message from event stream:", msg[5]) } - if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo::::0" { + if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo:::0" { t.Error("Wrong message from event stream:", msg) } - if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo 2::::0" { + if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo 2:::0" { t.Error("Wrong message from event stream:", msg) } } @@ -286,7 +286,7 @@ func TestStateMaintainer(t *testing.T) { t.Error("Team ID too short:", teamID) } - s.LogEvent("Hello!", "", "", "", 0) + s.LogEvent("Hello!", "", "", 0) if len(s.PointsLog()) != 0 { t.Error("Points log is not empty") diff --git a/docs/logs.md b/docs/logs.md index a5b5422..53362fd 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -49,10 +49,10 @@ It ought to import into any spreadsheet program painlessly. Each line has six fields minimum: -| `timestamp` | `event` | `participantID` | `teamID` | `category` | `points` | `extra`... | -| --- | --- | --- | --- | --- | --- | --- | -| int | string | string | string | string | int | string... | -| Unix epoch | Event type | Participant's (hopefully) unique ID | Team's unique ID | Name of category, if any | Points awarded, if any | Additional fields, if any | +| `timestamp` | `event` | `teamID` | `category` | `points` | `extra`... | +| --- | --- | --- | --- | --- | --- | +| int | string | string | string | int | string... | +| Unix epoch | Event type | Team's unique ID | Name of category, if any | Points awarded, if any | Additional fields, if any | Fields after `points` contain extra fields associated with the event. diff --git a/theme/index.html b/theme/index.html index f780301..32e2115 100644 --- a/theme/index.html +++ b/theme/index.html @@ -16,11 +16,6 @@
- Team ID:
Team name:
diff --git a/theme/moth.js b/theme/moth.js index eb1742c..d88e610 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -105,7 +105,6 @@ function renderState(obj) { if (devel) { let params = new URLSearchParams(window.location.search) sessionStorage.id = "1" - sessionStorage.pid = "rodney" renderPuzzles(obj.Puzzles) } else if (Object.keys(obj.Puzzles).length > 0) { renderPuzzles(obj.Puzzles) @@ -115,12 +114,9 @@ function renderState(obj) { function heartbeat() { let teamId = sessionStorage.id || "" - let participantId = sessionStorage.pid let url = new URL("state", window.location) url.searchParams.set("id", teamId) - if (participantId) { - url.searchParams.set("pid", participantId) - } + let fd = new FormData() fd.append("id", teamId) fetch(url) @@ -152,8 +148,6 @@ function login(e) { e.preventDefault() let name = document.querySelector("[name=name]").value let teamId = document.querySelector("[name=id]").value - let pide = document.querySelector("[name=pid]") - let participantId = pide?pide.value:Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) fetch("register", { method: "POST", @@ -166,7 +160,6 @@ function login(e) { if ((obj.status == "success") || (obj.data.short == "Already registered")) { toast("Logged in") sessionStorage.id = teamId - sessionStorage.pid = participantId showPuzzles() heartbeat() } else { From d2971ee740245250b319005e9834907a08d40a22 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 11 Apr 2023 17:56:59 -0600 Subject: [PATCH 72/98] Attempt to fix packages --- cmd/mothd/httpd.go | 2 +- cmd/mothd/providercommand.go | 2 +- cmd/mothd/server.go | 2 +- cmd/mothd/state.go | 2 +- cmd/mothd/transpiler.go | 2 +- cmd/transpile/main.go | 2 +- cmd/transpile/main_test.go | 2 +- go.mod | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index f9cc27c..26b64f9 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/dirtbags/moth/pkg/jsend" + "github.com/dirtbags/moth/v4/pkg/jsend" ) // HTTPServer is a MOTH HTTP server diff --git a/cmd/mothd/providercommand.go b/cmd/mothd/providercommand.go index 1bdd693..e7397e7 100644 --- a/cmd/mothd/providercommand.go +++ b/cmd/mothd/providercommand.go @@ -15,7 +15,7 @@ import ( "strings" "time" - "github.com/dirtbags/moth/pkg/transpile" + "github.com/dirtbags/moth/v4/pkg/transpile" ) // ProviderCommand specifies a command to run for the puzzle API diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index c7fe1f0..7fe3849 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/dirtbags/moth/pkg/award" + "github.com/dirtbags/moth/v4/pkg/award" ) // Category represents a puzzle category. diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index c611d88..718371e 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "github.com/dirtbags/moth/pkg/award" + "github.com/dirtbags/moth/v4/pkg/award" "github.com/spf13/afero" ) diff --git a/cmd/mothd/transpiler.go b/cmd/mothd/transpiler.go index ddeb3da..c3be4e2 100644 --- a/cmd/mothd/transpiler.go +++ b/cmd/mothd/transpiler.go @@ -7,7 +7,7 @@ import ( "log" "time" - "github.com/dirtbags/moth/pkg/transpile" + "github.com/dirtbags/moth/v4/pkg/transpile" "github.com/spf13/afero" ) diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index f225464..4f97d7e 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -9,7 +9,7 @@ import ( "os" "sort" - "github.com/dirtbags/moth/pkg/transpile" + "github.com/dirtbags/moth/v4/pkg/transpile" "github.com/spf13/afero" ) diff --git a/cmd/transpile/main_test.go b/cmd/transpile/main_test.go index 042795a..b544af1 100644 --- a/cmd/transpile/main_test.go +++ b/cmd/transpile/main_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/dirtbags/moth/pkg/transpile" + "github.com/dirtbags/moth/v4/pkg/transpile" "github.com/spf13/afero" ) diff --git a/go.mod b/go.mod index af3862d..e39a379 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/dirtbags/moth +module github.com/dirtbags/moth/v4 go 1.13 From a3d0f5516031674bc6f4ad1bae0a5d8159330d3f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 13 Apr 2023 15:32:15 -0600 Subject: [PATCH 73/98] Try to fix CI/CD build for tags --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f697f2d..058787d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ Run unit tests: only: refs: - main + - tags - merge_requests script: - go test -coverprofile=coverage.txt -covermode=atomic -race ./... @@ -28,6 +29,7 @@ Generage coverage XML: only: refs: - main + - tags - merge_requests artifacts: reports: From fcfa11b01237aebbe741cf025981f5ee2def8a77 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 1 Sep 2023 17:59:09 -0600 Subject: [PATCH 74/98] Initial work on #190 --- theme/moth.mjs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ theme/puzzle.mjs | 0 2 files changed, 83 insertions(+) create mode 100644 theme/moth.mjs create mode 100644 theme/puzzle.mjs diff --git a/theme/moth.mjs b/theme/moth.mjs new file mode 100644 index 0000000..daa5ad8 --- /dev/null +++ b/theme/moth.mjs @@ -0,0 +1,83 @@ +class Server { + constructor(baseUrl) { + this.baseUrl = new URL(baseUrl) + this.teamId = null + } + + /** + * Fetch a MOTH resource. + * + * This is just a convenience wrapper to always send teamId. + * If body is set, POST will be used instead of GET + * + * @param {String} path Path to API endpoint + * @param {Object} body Key/Values to send in POST data + * @returns {Promise} Response + */ + fetch(path, body) { + let url = new URL(path, this.baseUrl) + if (this.teamId & (!(body && body.id))) { + url.searchParams.set("id", this.teamId) + } + return fetch(url, { + method: body?"POST":"GET", + body, + }) + } + + /** + * Send a request to a JSend API endpoint. + * + * @param {String} path Path to API endpoint + * @param {Object} args Key/Values to send in POST + * @returns JSend Data + */ + async postJSend(path, args) { + let resp = await this.fetch(path, args) + if (!resp.ok) { + throw new Error(resp.statusText) + } + let obj = await resp.json() + switch (obj.status) { + case "success": + return obj.data + case "failure": + throw new Error(obj.data.description || obj.data.short || obj.data) + case "error": + throw new Error(obj.message) + default: + throw new Error(`Unknown JSend status: ${obj.status}`) + } + } + + /** + * Register a team name with a team ID. + * + * This is similar to, but not exactly the same as, logging in. + * See MOTH documentation for details. + * + * @param {String} teamId + * @param {String} teamName + * @returns {String} Success message from server + */ + async Register(teamId, teamName) { + let data = await postJSend("/login", {id: teamId, name: teamName}) + this.teamId = teamId + this.teamName = teamName + return data.description || data.short + } + + /** + * Fetch current contest status. + * + * @returns {Object} Contest status + */ + async Status() { + let data = await this.postJSend("/status") + return data + } +} + +export { + Server +} \ No newline at end of file diff --git a/theme/puzzle.mjs b/theme/puzzle.mjs new file mode 100644 index 0000000..e69de29 From 99d7245c498cbbe31ac3b380e3e398455048aebf Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 7 Sep 2023 16:16:46 -0600 Subject: [PATCH 75/98] Full moth.mjs, and an example to use it --- theme/moth.mjs | 341 ++++++++++++++++++++++++++++++++++++++--- theme/reports/ksa.html | 39 +++++ theme/reports/ksa.mjs | 49 ++++++ 3 files changed, 410 insertions(+), 19 deletions(-) create mode 100644 theme/reports/ksa.html create mode 100644 theme/reports/ksa.mjs diff --git a/theme/moth.mjs b/theme/moth.mjs index daa5ad8..26c5552 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -1,18 +1,284 @@ +/** + * A point award. + */ +class Award { + constructor(when, teamid, category, points) { + /** Unix epoch timestamp for this award + * @type {Number} + */ + this.When = when + /** Team ID this award belongs to + * @type {String} + */ + this.TeamID = teamid + /** Puzzle category for this award + * @type {String} + */ + this.Category = category + /** Points value of this award + * @type {Number} + */ + this.Points = points + } +} + +/** + * A puzzle. + * + * A new Puzzle only knows its category and point value. + * If you want to populate it with meta-information, you must call Get(). + */ +class Puzzle { + /** + * + * @param {Server} server + * @param {String} category + * @param {Number} points + */ + constructor (server, category, points) { + if (points < 1) { + throw(`Invalid points value: ${points}`) + } + + /** Server where this puzzle lives + * @type {Server} + */ + this.server = server + /** Category this puzzle belongs to + * @type {String} + */ + this.Category = category + /** Point value of this puzzle + * @type {Number} + */ + this.Points = points + } + + /** Error returned trying to fetch this puzzle */ + Error = { + /** Status code provided by server */ + Status: 0, + /** Status text provided by server */ + StatusText: "", + /** Full text of server error */ + Body: "", + } + /** Hashes of answers + * @type {String[]} + */ + AnswerHashes = [] + /** Pattern that answer should match + * @type {String[]} + */ + AnswerPattern = "" + /** Accepted answers + * @type {String[]} + */ + Answers = [] + /** Other files attached to this puzzles + * @type {String[]} + */ + Attachments = [] + /** This puzzle's authors + * @type {String[]} + */ + Authors = [] + /** HTML body of this puzzle */ + Body = "" + /** Debugging information */ + Debug = { + Errors: [], + Hints: [], + Log: [], + Notes: "", + Summary: "", + } + /** KSAs met by solving this puzzle + * @type {String[]} + */ + KSAs = [] + /** Learning objective for this puzzle */ + Objective = "" + /** ECMAScript scripts needed for this puzzle + * @type {String[]} + */ + Scripts = [] + /** Criteria for succeeding at this puzzle */ + Success = { + /** Acceptable Minimum criteria for success */ + Minimum: "", + /** Criteria for demonstrating mastery of this puzzle */ + Mastery: "", + } + + /** + * Populate this Puzzle object with meta-information from the server. + */ + async Populate() { + let resp = await this.Get("puzzle.json") + if (!resp.ok) { + let body = await resp.text() + this.Error = { + Status: resp.status, + StatusText: resp.statusText, + Body: body, + } + throw(this.Error) + } + let obj = await resp.json() + Object.assign(this, obj) + + // Make sure lists are lists + this.AnswerHashes ||= [] + this.Answers ||= [] + this.Attachments ||= [] + this.Authors ||= [] + this.Debug.Errors ||= [] + this.Debug.Hints ||= [] + this.Debug.Log ||= [] + this.KSAs ||= [] + this.Scripts ||= [] + } + + /** + * Get a resource associated with this puzzle. + * + * @param {String} filename Attachment/Script to retrieve + * @returns {Promise} + */ + Get(filename) { + return this.server.GetContent(this.Category, this.Points, filename) + } +} + +/** + * MOTH instance state. + * + * @property {Object} Config + * @property {Boolean} Config.Enabled Are points log updates enabled? + * @property {String} Messages Global broadcast messages, in HTML + * @property {Object.} TeamNames Mapping from IDs to team names + * @property {Object.} PointsByCategory Map from category name to open puzzle point values + * @property {Award[]} PointsLog Log of points awarded + */ +class State { + /** + * @param {Server} server Server where we got this + * @param {Object} obj Raw state data + */ + constructor(server, obj) { + for (let key of ["Config", "Messages", "TeamNames", "PointsLog"]) { + if (!obj[key]) { + throw(`Missing state property: ${key}`) + } + } + this.server = server + + /** Configuration */ + this.Config = { + /** Is the server in debug mode? + * @type {Boolean} + */ + Debug: obj.Config.Debug, + } + /** Global messages, in HTML + * @type {String} + */ + this.Messages = obj.Messages + /** Map from Team ID to Team Name + * @type {Object.} + */ + this.TeamNames = obj.TeamNames + /** Map from category name to puzzle point values + * @type {Object. new Award(t,i,c,p)) + } + + /** + * Returns a sorted list of open category names + * + * @returns {String[]} List of categories + */ + Categories() { + let ret = [] + for (let category in this.PointsByCategory) { + ret.push(category) + } + ret.sort() + return ret + } + + /** + * Check whether a category has unsolved puzzles. + * + * The server adds a puzzle with 0 points in every "solved" category, + * so this just checks whether there is a 0-point puzzle in the category's point list. + * + * @param {String} category + * @returns {Boolean} + */ + HasUnsolved(category) { + return !this.PointsByCategory[category].includes(0) + } + + /** + * Return all open puzzles. + * + * The returned list will be sorted by (category, points). + * If not categories are given, all puzzles will be returned. + * + * @param {String} categories Limit results to these categories + * @returns {Puzzle[]} + */ + Puzzles(...categories) { + if (categories.length == 0) { + categories = this.Categories() + } + let ret = [] + for (let category of categories) { + for (let points of this.PointsByCategory[category]) { + if (0 == points) { + // This means all potential puzzles in the category are open + continue + } + let p = new Puzzle(this.server, category, points) + ret.push(p) + } + } + return ret + } +} + +/** + * A MOTH Server interface. + * + * This uses localStorage to remember Team ID, + * and will send a Team ID with every request, if it can find one. + */ class Server { constructor(baseUrl) { - this.baseUrl = new URL(baseUrl) - this.teamId = null + this.baseUrl = new URL(baseUrl, location) + this.teameIdKey = this.baseUrl.toString() + " teamID" + this.teamId = localStorage[this.teameIdKey] } /** * Fetch a MOTH resource. * - * This is just a convenience wrapper to always send teamId. + * If anything other than a 2xx code is returned, + * this function throws an error. + * + * This always sends teamId. * If body is set, POST will be used instead of GET * * @param {String} path Path to API endpoint - * @param {Object} body Key/Values to send in POST data - * @returns {Promise} Response + * @param {Object} body Key/Values to send in POST data + * @returns {Promise} Response */ fetch(path, body) { let url = new URL(path, this.baseUrl) @@ -29,14 +295,11 @@ class Server { * Send a request to a JSend API endpoint. * * @param {String} path Path to API endpoint - * @param {Object} args Key/Values to send in POST - * @returns JSend Data + * @param {Object} args Key/Values to send in POST + * @returns {Promise} JSend Data */ - async postJSend(path, args) { + async call(path, args) { let resp = await this.fetch(path, args) - if (!resp.ok) { - throw new Error(resp.statusText) - } let obj = await resp.json() switch (obj.status) { case "success": @@ -50,6 +313,27 @@ class Server { } } + /** + * Forget about any previous Team ID. + * + * This is equivalent to logging out. + */ + Reset() { + localStorage.removeItem(this.teameIdKey) + this.teamId = null + } + + /** + * Fetch current contest state. + * + * @returns {State} + */ + async GetState() { + let resp = await this.fetch("/state") + let obj = await resp.json() + return new State(this, obj) + } + /** * Register a team name with a team ID. * @@ -58,23 +342,42 @@ class Server { * * @param {String} teamId * @param {String} teamName - * @returns {String} Success message from server + * @returns {Promise} Success message from server */ async Register(teamId, teamName) { - let data = await postJSend("/login", {id: teamId, name: teamName}) + let data = await this.call("/login", {id: teamId, name: teamName}) this.teamId = teamId this.teamName = teamName + localStorage[this.teameIdKey] = teamId return data.description || data.short } /** - * Fetch current contest status. - * - * @returns {Object} Contest status + * Submit a puzzle answer for points. + * + * The returned promise will fail if anything goes wrong, including the + * answer being rejected. + * + * @param {String} category Category of puzzle + * @param {Number} points Point value of puzzle + * @param {String} answer Answer to submit + * @returns {Promise} Was the answer accepted? */ - async Status() { - let data = await this.postJSend("/status") - return data + async SubmitAnswer(category, points, answer) { + await this.call("/answer", {category, points, answer}) + return true + } + + /** + * Fetch a file associated with a puzzle. + * + * @param {String} category Category of puzzle + * @param {Number} points Point value of puzzle + * @param {String} filename + * @returns {Promise} + */ + GetContent(category, points, filename) { + return this.fetch(`/content/${category}/${points}/${filename}`) } } diff --git a/theme/reports/ksa.html b/theme/reports/ksa.html new file mode 100644 index 0000000..592a355 --- /dev/null +++ b/theme/reports/ksa.html @@ -0,0 +1,39 @@ + + + + KSA Report + + + + +

KSA Report

+

+ This report shows all KSAs covered by this server so far. + This is not a report on your progress, but rather + what you would have covered if you had worked every exercise available. +

+ + + +
+ + + + + + + + + + + +
CategoryPointsKSAsErrors
+ + \ No newline at end of file diff --git a/theme/reports/ksa.mjs b/theme/reports/ksa.mjs new file mode 100644 index 0000000..82a555e --- /dev/null +++ b/theme/reports/ksa.mjs @@ -0,0 +1,49 @@ +import * as moth from "../moth.mjs" + +function doing(what) { + for (let e of document.querySelectorAll(".doing")) { + if (what) { + e.style.display = "inherit" + } else { + e.style.display = "none" + } + for (let p of e.querySelectorAll("p")) { + p.textContent = what + } + } +} + +async function init() { + let server = new moth.Server("../") + + doing("Retrieving server state") + let state = await server.GetState() + + doing("Retrieving all puzzles") + let puzzles = state.Puzzles() + for (let p of puzzles) { + await p.Populate().catch(x => {}) + } + + doing("Filling table") + let puzzlerowTemplate = document.querySelector("template#puzzlerow") + for (let tbody of document.querySelectorAll("tbody")) { + for (let puzzle of puzzles) { + let row = puzzlerowTemplate.content.cloneNode(true) + row.querySelector(".category").textContent = puzzle.Category + row.querySelector(".points").textContent = puzzle.Points + row.querySelector(".ksas").textContent = puzzle.KSAs.join(" ") + row.querySelector(".error").textContent = puzzle.Error.Body + tbody.appendChild(row) + } + } + + doing() +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +} + \ No newline at end of file From 47671b9a121d4f832efbee1712db7cf85aa1cb8c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 7 Sep 2023 16:32:06 -0600 Subject: [PATCH 76/98] jsdoc fixes (maybe?) --- theme/moth.mjs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/theme/moth.mjs b/theme/moth.mjs index 26c5552..9369494 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -144,7 +144,7 @@ class Puzzle { * Get a resource associated with this puzzle. * * @param {String} filename Attachment/Script to retrieve - * @returns {Promise} + * @returns {Promise.} */ Get(filename) { return this.server.GetContent(this.Category, this.Points, filename) @@ -153,13 +153,6 @@ class Puzzle { /** * MOTH instance state. - * - * @property {Object} Config - * @property {Boolean} Config.Enabled Are points log updates enabled? - * @property {String} Messages Global broadcast messages, in HTML - * @property {Object.} TeamNames Mapping from IDs to team names - * @property {Object.} PointsByCategory Map from category name to open puzzle point values - * @property {Award[]} PointsLog Log of points awarded */ class State { /** @@ -190,7 +183,7 @@ class State { */ this.TeamNames = obj.TeamNames /** Map from category name to puzzle point values - * @type {Object.} */ this.PointsByCategory = obj.Puzzles /** Log of points awarded @@ -277,8 +270,8 @@ class Server { * If body is set, POST will be used instead of GET * * @param {String} path Path to API endpoint - * @param {Object} body Key/Values to send in POST data - * @returns {Promise} Response + * @param {Object.} body Key/Values to send in POST data + * @returns {Promise.} Response */ fetch(path, body) { let url = new URL(path, this.baseUrl) @@ -295,8 +288,8 @@ class Server { * Send a request to a JSend API endpoint. * * @param {String} path Path to API endpoint - * @param {Object} args Key/Values to send in POST - * @returns {Promise} JSend Data + * @param {Object.} args Key/Values to send in POST + * @returns {Promise.} JSend Data */ async call(path, args) { let resp = await this.fetch(path, args) @@ -342,7 +335,7 @@ class Server { * * @param {String} teamId * @param {String} teamName - * @returns {Promise} Success message from server + * @returns {Promise.} Success message from server */ async Register(teamId, teamName) { let data = await this.call("/login", {id: teamId, name: teamName}) @@ -361,7 +354,7 @@ class Server { * @param {String} category Category of puzzle * @param {Number} points Point value of puzzle * @param {String} answer Answer to submit - * @returns {Promise} Was the answer accepted? + * @returns {Promise.} Was the answer accepted? */ async SubmitAnswer(category, points, answer) { await this.call("/answer", {category, points, answer}) @@ -374,7 +367,7 @@ class Server { * @param {String} category Category of puzzle * @param {Number} points Point value of puzzle * @param {String} filename - * @returns {Promise} + * @returns {Promise.} */ GetContent(category, points, filename) { return this.fetch(`/content/${category}/${points}/${filename}`) From 8ff91e79ec0ca394d3ee6ea4769c0fd53f8f9000 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 7 Sep 2023 17:29:21 -0600 Subject: [PATCH 77/98] Refer to server docs for Puzzle fields --- pkg/transpile/puzzle.go | 46 ++++++++++++++++------ theme/moth.mjs | 86 ++++++++++------------------------------- theme/reports/ksa.mjs | 14 +++---- 3 files changed, 59 insertions(+), 87 deletions(-) diff --git a/pkg/transpile/puzzle.go b/pkg/transpile/puzzle.go index 44d0ab5..e4045ce 100644 --- a/pkg/transpile/puzzle.go +++ b/pkg/transpile/puzzle.go @@ -37,23 +37,45 @@ type PuzzleDebug struct { Summary string } -// Puzzle contains everything about a puzzle that a client would see. +// Puzzle contains everything about a puzzle that a client will see. type Puzzle struct { - Debug PuzzleDebug - Authors []string - Attachments []string - Scripts []string - Body string + // Debug contains debugging information, omitted in mothballs + Debug PuzzleDebug + + // Authors names all authors of this puzzle + Authors []string + + // Attachments is a list of filenames used by this puzzle + Attachments []string + + // Scripts is a list of EMCAScript files needed by the client for this puzzle + Scripts []string + + // Body is the HTML rendering of this puzzle + Body string + + // AnswerPattern contains the pattern (regular expression?) used to match valid answers AnswerPattern string - AnswerHashes []string - Objective string - KSAs []string - Success struct { + + // AnswerHashes contains hashes of all answers for this puzzle + AnswerHashes []string + + // Objective is the learning objective for this puzzle + Objective string + + // KSAs lists all KSAs achieved upon successfull completion of this puzzle + KSAs []string + + // Success lists the criteria for successfully understanding this puzzle + Success struct { + // Acceptable describes the minimum work required to be considered successfully understanding this puzzle's concepts Acceptable string - Mastery string + + // Mastery describes the work required to be considered mastering this puzzle's conceptss + Mastery string } - // Answers will be empty in a mothball + // Answers lists all acceptable answers, omitted in mothballs Answers []string } diff --git a/theme/moth.mjs b/theme/moth.mjs index 9369494..3a31b18 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -26,11 +26,14 @@ class Award { * A puzzle. * * A new Puzzle only knows its category and point value. - * If you want to populate it with meta-information, you must call Get(). + * If you want to populate it with meta-information, you must call Populate(). + * + * Parameters created by Populate are described in the server source code: + * {@link https://pkg.go.dev/github.com/dirtbags/moth/v4/pkg/transpile#Puzzle} + * */ class Puzzle { /** - * * @param {Server} server * @param {String} category * @param {Number} points @@ -44,71 +47,22 @@ class Puzzle { * @type {Server} */ this.server = server - /** Category this puzzle belongs to - * @type {String} - */ - this.Category = category - /** Point value of this puzzle - * @type {Number} - */ - this.Points = points - } + + /** Category this puzzle belongs to */ + this.Category = String(category) + + /** Point value of this puzzle */ + this.Points = Number(points) - /** Error returned trying to fetch this puzzle */ - Error = { - /** Status code provided by server */ - Status: 0, - /** Status text provided by server */ - StatusText: "", - /** Full text of server error */ - Body: "", - } - /** Hashes of answers - * @type {String[]} - */ - AnswerHashes = [] - /** Pattern that answer should match - * @type {String[]} - */ - AnswerPattern = "" - /** Accepted answers - * @type {String[]} - */ - Answers = [] - /** Other files attached to this puzzles - * @type {String[]} - */ - Attachments = [] - /** This puzzle's authors - * @type {String[]} - */ - Authors = [] - /** HTML body of this puzzle */ - Body = "" - /** Debugging information */ - Debug = { - Errors: [], - Hints: [], - Log: [], - Notes: "", - Summary: "", - } - /** KSAs met by solving this puzzle - * @type {String[]} - */ - KSAs = [] - /** Learning objective for this puzzle */ - Objective = "" - /** ECMAScript scripts needed for this puzzle - * @type {String[]} - */ - Scripts = [] - /** Criteria for succeeding at this puzzle */ - Success = { - /** Acceptable Minimum criteria for success */ - Minimum: "", - /** Criteria for demonstrating mastery of this puzzle */ - Mastery: "", + /** Error returned trying to fetch this puzzle */ + this.Error = { + /** Status code provided by server */ + Status: 0, + /** Status text provided by server */ + StatusText: "", + /** Full text of server error */ + Body: "", + } } /** diff --git a/theme/reports/ksa.mjs b/theme/reports/ksa.mjs index 82a555e..3cc786e 100644 --- a/theme/reports/ksa.mjs +++ b/theme/reports/ksa.mjs @@ -20,19 +20,15 @@ async function init() { let state = await server.GetState() doing("Retrieving all puzzles") - let puzzles = state.Puzzles() - for (let p of puzzles) { - await p.Populate().catch(x => {}) - } - - doing("Filling table") let puzzlerowTemplate = document.querySelector("template#puzzlerow") - for (let tbody of document.querySelectorAll("tbody")) { - for (let puzzle of puzzles) { + let puzzles = state.Puzzles() + for (let puzzle of puzzles) { + await puzzle.Populate().catch(x => {}) + for (let tbody of document.querySelectorAll("tbody")) { let row = puzzlerowTemplate.content.cloneNode(true) row.querySelector(".category").textContent = puzzle.Category row.querySelector(".points").textContent = puzzle.Points - row.querySelector(".ksas").textContent = puzzle.KSAs.join(" ") + row.querySelector(".ksas").textContent = (puzzle.KSAs || []).join(" ") row.querySelector(".error").textContent = puzzle.Error.Body tbody.appendChild(row) } From a896788cc5f979fba0b45d3dd4602c787c598cbd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 8 Sep 2023 11:31:41 -0600 Subject: [PATCH 78/98] Also list KSAs by Category --- theme/reports/ksa.html | 4 +++- theme/reports/ksa.mjs | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/theme/reports/ksa.html b/theme/reports/ksa.html index 592a355..268925c 100644 --- a/theme/reports/ksa.html +++ b/theme/reports/ksa.html @@ -12,9 +12,11 @@ This is not a report on your progress, but rather what you would have covered if you had worked every exercise available.

- +
+

All KSAs by Category

+
diff --git a/theme/reports/ksa.mjs b/theme/reports/ksa.mjs index 3cc786e..1409b2d 100644 --- a/theme/reports/ksa.mjs +++ b/theme/reports/ksa.mjs @@ -24,6 +24,20 @@ async function init() { let puzzles = state.Puzzles() for (let puzzle of puzzles) { await puzzle.Populate().catch(x => {}) + } + + doing("Filling tables") + let KSAsByCategory = {} + for (let puzzle of puzzles) { + let KSAs = KSAsByCategory[puzzle.Category] + if (!KSAs) { + KSAs = new Set() + KSAsByCategory[puzzle.Category] = KSAs + } + for (let KSA of (puzzle.KSAs || [])) { + KSAs.add(KSA) + } + for (let tbody of document.querySelectorAll("tbody")) { let row = puzzlerowTemplate.content.cloneNode(true) row.querySelector(".category").textContent = puzzle.Category @@ -34,6 +48,20 @@ async function init() { } } + doing("Filling KSAs By Category") + for (let div of document.querySelectorAll(".KSAsByCategory")) { + for (let category of state.Categories()) { + let KSAs = [...KSAsByCategory[category]] + KSAs.sort() + + div.appendChild(document.createElement("h3")).textContent = category + let ul = div.appendChild(document.createElement("ul")) + for (let k of KSAs) { + ul.appendChild(document.createElement("li")).textContent = k + } + } + } + doing() } From 551afe04a5b8b2ba373dba934755591009facb23 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 8 Sep 2023 18:05:51 -0600 Subject: [PATCH 79/98] Puzzle start using new lib +bg animation --- theme/background.mjs | 126 +++++++++++++++++++++++++++++++++++++++++++ theme/basic.css | 107 ++++++++++++++++++------------------ theme/moth.mjs | 14 +++++ theme/puzzle.html | 44 +++++++-------- theme/puzzle.mjs | 60 +++++++++++++++++++++ 5 files changed, 275 insertions(+), 76 deletions(-) create mode 100644 theme/background.mjs diff --git a/theme/background.mjs b/theme/background.mjs new file mode 100644 index 0000000..f2d1509 --- /dev/null +++ b/theme/background.mjs @@ -0,0 +1,126 @@ +function randint(max) { + return Math.floor(Math.random() * max) +} + +const MILLISECOND = 1 +const SECOND = MILLISECOND * 1000 + +class Line { + /** + * @param {CanvasRenderingContext2D} ctx canvas context + * @param {Number} hue Hue, in % of one circle [0,tau) + * @param {Number} a First point of line + * @param {Number} b Second point of line + */ + constructor(ctx, hue, a, b) { + this.ctx = ctx + this.hue = hue + this.a = a + this.b = b + } + + bounce(point, v) { + let ret = [ + point[0] + v[0], + point[1] + v[1], + ] + if ((ret[0] > this.ctx.canvas.width) || (ret[0] < 0)) { + v[0] *= -1 + ret[0] += v[0] * 2 + } + if ((ret[1] > this.ctx.canvas.height) || (ret[1] < 0)) { + v[1] *= -1 + ret[1] += v[1] * 2 + } + return ret + } + + Add(hue, a, b) { + return new Line( + this.ctx, + (this.hue + hue) % 1.0, + this.bounce(this.a, a), + this.bounce(this.b, b), + ) + } + + Draw() { + this.ctx.save() + this.ctx.strokeStyle = `hwb(${this.hue}turn 0% 50%)` + this.ctx.beginPath() + this.ctx.moveTo(this.a[0], this.a[1]) + this.ctx.lineTo(this.b[0], this.b[1]) + this.ctx.stroke() + this.ctx.restore() + } +} + +class LengoBackground { + constructor() { + this.canvas = document.createElement("canvas") + document.body.insertBefore(this.canvas, document.body.firstChild) + this.canvas.style.position = "fixed" + this.canvas.style.zIndex = -1000 + this.canvas.style.opacity = 0.3 + this.canvas.style.top = 0 + this.canvas.style.left = 0 + this.canvas.style.width = "99vw" + this.canvas.style.height = "99vh" + this.canvas.width = 2000 + this.canvas.height = 2000 + this.ctx = this.canvas.getContext("2d") + this.ctx.lineWidth = 1 + + this.lines = [] + for (let i = 0; i < 18; i++) { + this.lines.push( + new Line(this.ctx, 0, [0, 0], [0, 0]) + ) + } + this.velocities = { + hue: 0.001, + a: [20 + randint(10), 20 + randint(10)], + b: [5 + randint(10), 5 + randint(10)], + } + this.nextFrame = performance.now()-1 + this.frameInterval = 100 * MILLISECOND + + //addEventListener("resize", e => this.resizeEvent()) + //this.resizeEvent() + //this.animate(this.nextFrame) + setInterval(() => this.animate(this.nextFrame+1), SECOND/6) + } + + + /** + * Animate one frame + * + * @param {DOMHighResTimeStamp} timestamp + */ + animate(timestamp) { + if (timestamp >= this.nextFrame) { + this.lines.shift() + let lastLine = this.lines.pop() + let nextLine = lastLine.Add(this.velocities.hue, this.velocities.a, this.velocities.b) + this.lines.push(lastLine) + this.lines.push(nextLine) + + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height) + for (let line of this.lines) { + line.Draw() + } + this.nextFrame += this.frameInterval + } + //requestAnimationFrame((ts) => this.animate(ts)) + } +} + +function init() { + new LengoBackground() +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +} diff --git a/theme/basic.css b/theme/basic.css index 14a5a1e..42d1b5c 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -1,29 +1,58 @@ -/* http://paletton.com/#uid=63T0u0k7O9o3ouT6LjHih7ltq4c */ +/* + * Colors + * + * This uses the alpha channel to apply hue tinting to elements, to get a + * similar effect in light or dark mode. + * + * http://paletton.com/#uid=33x0u0klrl-4ON9dhtKtAdqMQ4T + */ body { - font-family: sans-serif; - max-width: 40em; - background: #282a33; - color: #f6efdc; + background: #010e19; + color: #edd488; } -body.wide { - max-width: 100%; +main { + background: #000d; } -a:any-link { - color: #8b969a; +h1, h2, h3, h4, h5, h6 { + color: #cb2408cc; } h1 { - background: #5e576b; - color: #9e98a8; + background: #cb240844; } -.Fail, .Error, #messages { - background: #3a3119; - color: #ffcc98; +a:any-link { + color: #b9cbd8; } -.Fail:before { - content: "Fail: "; +.notification { + background: #ac8f3944; } -.Error:before { - content: "Error: "; +.error { + background: red; + color: white; +} +@media (prefers-color-scheme: light) { + body { + background: #b9cbd8; + color: black; + } + main { + background: #fffd; + } + a:any-link { + color: #092b45; + } +} + +body { + font-family: sans-serif; +} +main { + max-width: 40em; + margin: auto; + padding: 1px 3px; + border-radius: 5px; +} +h1 { + padding: 3px; } p { margin: 1em 0em; @@ -36,9 +65,11 @@ input, select { margin: 0.2em; max-width: 30em; } -nav { - border: solid black 2px; +.notification, .error { + padding: 0 1em; + border-radius: 8px; } + nav ul, .category ul { padding: 1em; } @@ -58,7 +89,6 @@ input:invalid { } #messages { min-height: 3em; - border: solid black 2px; } #rankings { width: 100%; @@ -91,40 +121,7 @@ input:invalid { #devel { - background-color: #eee; - color: black; - overflow: scroll; -} -#devel .string { - color: #9c27b0; -} -#devel .body { - background-color: #ffc107; -} -.kvpair { - border: solid black 2px; -} - -.spinner { - display: inline-block; - width: 64px; - height: 64px; - display: block; - width: 46px; - height: 46px; - margin: 1px; - border-radius: 50%; - border: 5px solid #fff; - border-color: #fff transparent #fff transparent; - animation: rotate 1.2s linear infinite; -} -@keyframes rotate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + overflow: auto; } li[draggable]::before { diff --git a/theme/moth.mjs b/theme/moth.mjs index 3a31b18..f76f5e0 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -326,6 +326,20 @@ class Server { GetContent(category, points, filename) { return this.fetch(`/content/${category}/${points}/${filename}`) } + + /** + * Return a Puzzle object. + * + * New Puzzle objects only know their category and point value. + * See docstrings on the Puzzle object for more information. + * + * @param {String} category + * @param {Number} points + * @returns {Puzzle} + */ + GetPuzzle(category, points) { + return new Puzzle(this, category, points) + } } export { diff --git a/theme/puzzle.html b/theme/puzzle.html index 37206f6..46c1ad9 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -1,32 +1,34 @@ - + Puzzle - - + + -

Puzzle

-
-
-
    -

    Puzzle by

    -
    -
    - - - - - Team ID:
    - Answer:
    - - -
    +
    +

    [loading]

    +
    +
    +

    + Starting script... +

    +
    +
      +

      Puzzle by [loading]

      +
      +
      + + + Team ID:
      + Answer:
      + + +
      +
      - - - - - - - - - - - -
      CategoryPointsKSAsErrors
      + + + + + + \ No newline at end of file diff --git a/theme/reports/ksa.mjs b/theme/reports/ksa.mjs index 1409b2d..d7f5b79 100644 --- a/theme/reports/ksa.mjs +++ b/theme/reports/ksa.mjs @@ -1,73 +1,140 @@ import * as moth from "../moth.mjs" +import * as common from "../common.mjs" -function doing(what) { +const server = new moth.Server("../") + +/** + * Update "doing" indicators + * + * @param {String | null} what Text to display, or null to not update text + * @param {Number | null} finished Percentage complete to display, or null to not update progress + */ +function doing(what, finished = null) { for (let e of document.querySelectorAll(".doing")) { + e.classList.remove("hidden") if (what) { - e.style.display = "inherit" + e.textContent = what + } + if (finished) { + e.value = finished } else { - e.style.display = "none" - } - for (let p of e.querySelectorAll("p")) { - p.textContent = what + e.removeAttribute("value") } } } +function done() { + for (let e of document.querySelectorAll(".doing")) { + e.classList.add("hidden") + } +} + +async function GetNice() { + let NiceElementsByIdentifier = {} + let resp = await fetch("NICEFramework2017.json") + let obj = await resp.json() + for (let e of obj.elements) { + NiceElementsByIdentifier[e.element_identifier] = e + } + return NiceElementsByIdentifier +} + +/** + * Fetch a puzzle, and fill its KSAs and rows. + * + * This is done once per puzzle, in an asynchronous function, allowing the + * application to perform multiple blocking operations simultaneously. + */ +async function FetchAndFill(puzzle, KSAs, rows) { + try { + await puzzle.Populate() + } + catch (error) { + // Keep on going with whatever Populate was able to fill + } + for (let KSA of (puzzle.KSAs || [])) { + KSAs.add(KSA) + } + + for (let row of rows) { + row.querySelector(".category").textContent = puzzle.Category + row.querySelector(".points").textContent = puzzle.Points + row.querySelector(".ksas").textContent = (puzzle.KSAs || []).join(" ") + row.querySelector(".error").textContent = puzzle.Error.Body + } +} async function init() { - let server = new moth.Server("../") + doing("Fetching NICE framework data") + let nicePromise = GetNice() doing("Retrieving server state") let state = await server.GetState() doing("Retrieving all puzzles") + let KSAsByCategory = {} let puzzlerowTemplate = document.querySelector("template#puzzlerow") let puzzles = state.Puzzles() - for (let puzzle of puzzles) { - await puzzle.Populate().catch(x => {}) + let promises = [] + for (let category of state.Categories()) { + KSAsByCategory[category] = new Set() } - - doing("Filling tables") - let KSAsByCategory = {} + let pending = puzzles.length for (let puzzle of puzzles) { - let KSAs = KSAsByCategory[puzzle.Category] - if (!KSAs) { - KSAs = new Set() - KSAsByCategory[puzzle.Category] = KSAs - } - for (let KSA of (puzzle.KSAs || [])) { - KSAs.add(KSA) - } - + // Make space in the table, so everything fills in sorted order + let rows = [] for (let tbody of document.querySelectorAll("tbody")) { - let row = puzzlerowTemplate.content.cloneNode(true) - row.querySelector(".category").textContent = puzzle.Category - row.querySelector(".points").textContent = puzzle.Points - row.querySelector(".ksas").textContent = (puzzle.KSAs || []).join(" ") - row.querySelector(".error").textContent = puzzle.Error.Body + let row = puzzlerowTemplate.content.cloneNode(true).firstElementChild tbody.appendChild(row) + rows.push(row) + } + + // Queue up a fetch, and update progress bar + let promise = FetchAndFill(puzzle, KSAsByCategory[puzzle.Category], rows) + promises.push(promise) + promise.then(() => doing(null, 1 - (--pending / puzzles.length))) + + if (promises.length > 50) { + // Chrome runs out of resources if you queue up too many of these at once + await Promise.all(promises) + promises = [] } } + await Promise.all(promises) + + doing("Retrieving NICE identifiers") + let NiceElementsByIdentifier = await nicePromise + doing("Filling KSAs By Category") + let allKSAs = new Set() for (let div of document.querySelectorAll(".KSAsByCategory")) { for (let category of state.Categories()) { + doing(`Filling KSAs for category: ${category}`) let KSAs = [...KSAsByCategory[category]] KSAs.sort() div.appendChild(document.createElement("h3")).textContent = category let ul = div.appendChild(document.createElement("ul")) for (let k of KSAs) { - ul.appendChild(document.createElement("li")).textContent = k + let ksa = k.split(/\s+/)[0] + let ne = NiceElementsByIdentifier[ksa] || { text: "???" } + let text = `${ksa}: ${ne.text}` + ul.appendChild(document.createElement("li")).textContent = text + allKSAs.add(text) } } } - doing() + doing("Filling KSAs") + for (let e of document.querySelectorAll(".allKSAs")) { + let KSAs = [...allKSAs] + KSAs.sort() + for (let text of KSAs) { + e.appendChild(document.createElement("li")).textContent = text + } + } + + done() } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init) -} else { - init() -} - \ No newline at end of file +common.WhenDOMLoaded(init) From c72d13af327eedb353a327234cb074d405a30d79 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 14 Sep 2023 19:08:44 -0600 Subject: [PATCH 90/98] Some twiddling to prepare for a scoreboard update --- theme/index.mjs | 15 ++--- theme/moth.mjs | 146 +++++++++++++++++++++++++++++++---------------- theme/puzzle.mjs | 25 ++++---- 3 files changed, 114 insertions(+), 72 deletions(-) diff --git a/theme/index.mjs b/theme/index.mjs index 886982e..9bc79a2 100644 --- a/theme/index.mjs +++ b/theme/index.mjs @@ -39,8 +39,8 @@ class App { /** * Attempt to log in to the server. * - * @param {String} teamID - * @param {String} teamName + * @param {string} teamID + * @param {string} teamName */ async Login(teamID, teamName) { try { @@ -114,7 +114,7 @@ class App { /** * Render a login box. * - * This just toggles visibility, there's nothing dynamic in a login box. + * Just toggles visibility, there's nothing dynamic in a login box. */ renderLogin(element, visible) { element.classList.toggle("hidden", !visible) @@ -123,7 +123,7 @@ class App { /** * Render a puzzles box. * - * This updates the list of open puzzles, and adds mothball download links + * Displays the list of open puzzles, and adds mothball download links * if the server is in development mode. */ renderPuzzles(element, visible) { @@ -177,9 +177,4 @@ function init() { } } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init) -} else { - init() -} - \ No newline at end of file +common.WhenDOMLoaded(init) diff --git a/theme/moth.mjs b/theme/moth.mjs index 1be980a..bc5d190 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -7,8 +7,8 @@ class Hash { * * Used until MOTH v3.5 * - * @param {String} buf Input - * @returns {Number} + * @param {string} buf Input + * @returns {number} */ static djb2(buf) { let h = 5381 @@ -23,8 +23,8 @@ class Hash { /** * Dan Bernstein hash with xor improvement * - * @param {String} buf Input - * @returns {Number} + * @param {string} buf Input + * @returns {number} */ static djb2xor(buf) { let h = 5381 @@ -39,8 +39,8 @@ class Hash { * * Used until MOTH v4.5 * - * @param {String} buf Input - * @returns {Promise.} hex-encoded digest + * @param {string} buf Input + * @returns {Promise.} hex-encoded digest */ static async sha256(buf) { const msgUint8 = new TextEncoder().encode(buf) @@ -52,8 +52,8 @@ class Hash { /** * Hex-encode a byte array * - * @param {Number[]} buf Byte array - * @returns {String} + * @param {number[]} buf Byte array + * @returns {string} */ static hexlify(buf) { return buf.map(b => b.toString(16).padStart(2, "0")).join("") @@ -62,8 +62,8 @@ class Hash { /** * Apply every hash to the input buffer. * - * @param {String} buf Input - * @returns {Promise.} + * @param {string} buf Input + * @returns {Promise.} */ static async All(buf) { return [ @@ -80,19 +80,19 @@ class Hash { class Award { constructor(when, teamid, category, points) { /** Unix epoch timestamp for this award - * @type {Number} + * @type {number} */ this.When = when /** Team ID this award belongs to - * @type {String} + * @type {string} */ this.TeamID = teamid /** Puzzle category for this award - * @type {String} + * @type {string} */ this.Category = category /** Points value of this award - * @type {Number} + * @type {number} */ this.Points = points } @@ -111,8 +111,8 @@ class Award { class Puzzle { /** * @param {Server} server - * @param {String} category - * @param {Number} points + * @param {string} category + * @param {number} points */ constructor (server, category, points) { if (points < 1) { @@ -173,7 +173,7 @@ class Puzzle { /** * Get a resource associated with this puzzle. * - * @param {String} filename Attachment/Script to retrieve + * @param {string} filename Attachment/Script to retrieve * @returns {Promise.} */ Get(filename) { @@ -193,8 +193,8 @@ class Puzzle { * you still have to pick through a lot of potentially correct answers when * it's done. * - * @param {String} str User-submitted possible answer - * @returns {Promise.} + * @param {string} str User-submitted possible answer + * @returns {Promise.} */ async IsPossiblyCorrect(str) { let userAnswerHashes = await Hash.All(str) @@ -215,8 +215,8 @@ class Puzzle { * The returned promise will fail if anything goes wrong, including the * proposed answer being rejected. * - * @param {String} proposed Answer to submit - * @returns {Promise.} Success message + * @param {string} proposed Answer to submit + * @returns {Promise.} Success message */ SubmitAnswer(proposed) { return this.server.SubmitAnswer(this.Category, this.Points, proposed) @@ -242,23 +242,23 @@ class State { /** Configuration */ this.Config = { /** Is the server in development mode? - * @type {Boolean} + * @type {boolean} */ Devel: obj.Config.Devel, } /** Global messages, in HTML - * @type {String} + * @type {string} */ this.Messages = obj.Messages /** Map from Team ID to Team Name - * @type {Object.} + * @type {Object.} */ this.TeamNames = obj.TeamNames /** Map from category name to puzzle point values - * @type {Object.} + * @type {Object.} */ this.PointsByCategory = obj.Puzzles @@ -271,7 +271,7 @@ class State { /** * Returns a sorted list of open category names * - * @returns {String[]} List of categories + * @returns {string[]} List of categories */ Categories() { let ret = [] @@ -288,8 +288,8 @@ class State { * The server adds a puzzle with 0 points in every "solved" category, * so this just checks whether there is a 0-point puzzle in the category's point list. * - * @param {String} category - * @returns {Boolean} + * @param {string} category + * @returns {boolean} */ ContainsUnsolved(category) { return !this.PointsByCategory[category].includes(0) @@ -298,7 +298,7 @@ class State { /** * Is the server in development mode? * - * @returns {Boolean} + * @returns {boolean} */ DevelopmentMode() { return this.Config && this.Config.Devel @@ -310,7 +310,7 @@ class State { * The returned list will be sorted by (category, points). * If not categories are given, all puzzles will be returned. * - * @param {String} categories Limit results to these categories + * @param {string} categories Limit results to these categories * @returns {Puzzle[]} */ Puzzles(...categories) { @@ -335,8 +335,8 @@ class State { * Has this puzzle been solved by this team? * * @param {Puzzle} puzzle - * @param {String} teamID Team to check, default the logged-in team - * @returns {Boolean} + * @param {string} teamID Team to check, default the logged-in team + * @returns {boolean} */ IsSolved(puzzle, teamID="self") { for (let award of this.PointsLog) { @@ -350,6 +350,52 @@ class State { } return false } + + /** + * Map from team ID to points. + * + * A special "max" property contains the highest number of points in this map. + * + * @typedef {Object.} TeamPointsDict + * @property {Number} max Highest number of points + */ + + /** + * Map from category to PointsDict. + * + * @typedef {Object.} CategoryTeamPointsDict + */ + + /** + * Score snapshot. + * + * @typedef {Object} ScoreSnapshot + * @property {number} when Epoch time of this snapshot + * @property {CategoryTeamPointsDict} snapshot + */ + + /** + * Replay scores. + * + * @yields {ScoreSnapshot} Snapshot at a point in time + */ + * ScoreHistory() { + /** @type {CategoryTeamPointsDict} */ + let categoryTeamPoints = {} + for (let award of this.PointsLog) { + let teamPoints = (categoryTeamPoints[award.Category] ??= {}) + let points = teamPoints[award.TeamID] || 0 + let max = teamPoints.max || 0 + + points += award.Points + teamPoints[award.TeamID] = points + teamPoints.max = Math.max(points, max) + + /** @type ScoreSnapshot */ + let snapshot = {when: award.When, snapshot: categoryTeamPoints} + yield snapshot + } + } } /** @@ -360,7 +406,7 @@ class State { */ class Server { /** - * @param {String | URL} baseUrl Base URL to server, for constructing API URLs + * @param {string | URL} baseUrl Base URL to server, for constructing API URLs */ constructor(baseUrl) { if (!baseUrl) { @@ -380,8 +426,8 @@ class Server { * This always sends teamID. * If args is set, POST will be used instead of GET * - * @param {String} path Path to API endpoint - * @param {Object.} args Key/Values to send in POST data + * @param {string} path Path to API endpoint + * @param {Object.} args Key/Values to send in POST data * @returns {Promise.} Response */ fetch(path, args={}) { @@ -400,8 +446,8 @@ class Server { /** * Send a request to a JSend API endpoint. * - * @param {String} path Path to API endpoint - * @param {Object.} args Key/Values to send in POST + * @param {string} path Path to API endpoint + * @param {Object.} args Key/Values to send in POST * @returns {Promise.} JSend Data */ async call(path, args={}) { @@ -434,7 +480,7 @@ class Server { /** * Are we logged in to the server? * - * @returns {Boolean} + * @returns {boolean} */ LoggedIn() { return this.TeamID ? true : false @@ -467,9 +513,9 @@ class Server { * This calls the server's registration endpoint; if the call succeds, or * fails with "team already exists", the login is returned as successful. * - * @param {String} teamID - * @param {String} teamName - * @returns {Promise.} Success message from server + * @param {string} teamID + * @param {string} teamName + * @returns {Promise.} Success message from server */ async Login(teamID, teamName) { let data = await this.call("/register", {id: teamID, name: teamName}) @@ -485,10 +531,10 @@ class Server { * The returned promise will fail if anything goes wrong, including the * proposed answer being rejected. * - * @param {String} category Category of puzzle - * @param {Number} points Point value of puzzle - * @param {String} proposed Answer to submit - * @returns {Promise.} Success message + * @param {string} category Category of puzzle + * @param {number} points Point value of puzzle + * @param {string} proposed Answer to submit + * @returns {Promise.} Success message */ async SubmitAnswer(category, points, proposed) { let data = await this.call("/answer", { @@ -502,9 +548,9 @@ class Server { /** * Fetch a file associated with a puzzle. * - * @param {String} category Category of puzzle - * @param {Number} points Point value of puzzle - * @param {String} filename + * @param {string} category Category of puzzle + * @param {number} points Point value of puzzle + * @param {string} filename * @returns {Promise.} */ GetContent(category, points, filename) { @@ -517,8 +563,8 @@ class Server { * New Puzzle objects only know their category and point value. * See docstrings on the Puzzle object for more information. * - * @param {String} category - * @param {Number} points + * @param {string} category + * @param {number} points * @returns {Puzzle} */ GetPuzzle(category, points) { diff --git a/theme/puzzle.mjs b/theme/puzzle.mjs index 8c5fd7d..c53b6a1 100644 --- a/theme/puzzle.mjs +++ b/theme/puzzle.mjs @@ -9,7 +9,7 @@ const server = new moth.Server(".") /** * Handle a submit event on a form. * - * This event will be called when the user submits the form, + * Called when the user submits the form, * either by clicking a "submit" button, * or by some other means provided by the browser, * like hitting the Enter key. @@ -22,7 +22,7 @@ async function formSubmitHandler(event) { let proposed = data.get("answer") let message - console.group("Submit answer") + console.groupCollapsed("Submit answer") console.info(`Proposed answer: ${proposed}`) try { message = await window.app.puzzle.SubmitAnswer(proposed) @@ -56,7 +56,7 @@ async function answerInputHandler(event) { /** * Return the puzzle content element, possibly with everything cleared out of it. * - * @param {Boolean} clear Should the element be cleared of children? Default true. + * @param {boolean} clear Should the element be cleared of children? Default true. * @returns {Element} */ function puzzleElement(clear=true) { @@ -69,10 +69,11 @@ function puzzleElement(clear=true) { /** * Display an error in the puzzle area, and also send it to the console. - * - * This makes it so the user can see a bit more about what the problem is. - * - * @param {String} error + * + * Errors are rendered in the puzzle area, so the user can see a bit more about + * what the problem is. + * + * @param {string} error */ function error(error) { console.error(error) @@ -84,9 +85,9 @@ function error(error) { /** * Set the answer and invoke input handlers. * - * This makes sure the Circle Of Success gets updated. + * Makes sure the Circle Of Success gets updated. * - * @param {String} s + * @param {string} s */ function SetAnswer(s) { let e = document.querySelector("#answer") @@ -125,11 +126,11 @@ function writeObject(e, obj) { /** * Load the given puzzle. * - * @param {String} category - * @param {Number} points + * @param {string} category + * @param {number} points */ async function loadPuzzle(category, points) { - console.group("Loading puzzle:", category, points) + console.groupCollapsed("Loading puzzle:", category, points) let contentBase = new URL(`content/${category}/${points}/`, location) // Tell user we're loading From f49eb3ed46ad47c416985c18874fc9a071691ef4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 15 Sep 2023 12:34:31 -0600 Subject: [PATCH 91/98] =?UTF-8?q?Change=20answer=20hash=20algorithm=20to?= =?UTF-8?q?=20SHA1=E2=82=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++-- pkg/transpile/puzzle.go | 6 +++--- pkg/transpile/puzzle_test.go | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fc8422..5fd80c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v4.6.0] - unreleased ### Changed -- We are now using djb2xor instead of sha256 to hash puzzle answers -- Lots of work on the built-in theme +- Answer hashes are now the first 4 characters of the hex-encoded SHA1 digest +- Reworked the built-in theme - [moth.mjs](theme/moth.mjs) is now the standard MOTH library for ECMAScript - Devel mode no longer accepts an empty team ID diff --git a/pkg/transpile/puzzle.go b/pkg/transpile/puzzle.go index e4045ce..23d8a90 100644 --- a/pkg/transpile/puzzle.go +++ b/pkg/transpile/puzzle.go @@ -4,7 +4,7 @@ import ( "bufio" "bytes" "context" - "crypto/sha256" + "crypto/sha1" "encoding/json" "errors" "fmt" @@ -85,9 +85,9 @@ func (puzzle *Puzzle) computeAnswerHashes() { } puzzle.AnswerHashes = make([]string, len(puzzle.Answers)) for i, answer := range puzzle.Answers { - sum := sha256.Sum256([]byte(answer)) + sum := sha1.Sum([]byte(answer)) hexsum := fmt.Sprintf("%x", sum) - puzzle.AnswerHashes[i] = hexsum + puzzle.AnswerHashes[i] = hexsum[:4] } } diff --git a/pkg/transpile/puzzle_test.go b/pkg/transpile/puzzle_test.go index 47db867..d6b9022 100644 --- a/pkg/transpile/puzzle_test.go +++ b/pkg/transpile/puzzle_test.go @@ -23,6 +23,12 @@ func TestPuzzle(t *testing.T) { if (len(p.Answers) == 0) || (p.Answers[0] != "YAML answer") { t.Error("Answers are wrong", p.Answers) } + if len(p.Answers) != len(p.AnswerHashes) { + t.Error("Answer hashes length does not match answers length") + } + if len(p.AnswerHashes[0]) != 4 { + t.Error("Answer hash is wrong length") + } if (len(p.Authors) != 3) || (p.Authors[1] != "Buster") { t.Error("Authors are wrong", p.Authors) } From d18de0fe8b33ca74a8b2d333b20733bd6d91e6f1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 15 Sep 2023 15:17:07 -0600 Subject: [PATCH 92/98] working scoreboard --- docs/scoring.md | 73 +++++++++ theme/basic.css | 16 +- theme/moth.mjs | 198 +++++++++++++++++++------ theme/scoreboard.html | 1 - theme/scoreboard.mjs | 334 ++++++++++-------------------------------- 5 files changed, 309 insertions(+), 313 deletions(-) create mode 100644 docs/scoring.md diff --git a/docs/scoring.md b/docs/scoring.md new file mode 100644 index 0000000..885f657 --- /dev/null +++ b/docs/scoring.md @@ -0,0 +1,73 @@ +Scoring +======= + +MOTH does not carry any notion of who is winning: we consider this a user +interface issue. The server merely provides a timestamped log of point awards. + +The bundled scoreboard provides one way to interpret the scores: this is the +main algorithm we use at Cyber Fire events. We use other views of the scoreboard +in other contexts, though! Here are some ideas: + + +Percentage of Each Category +--------------------- + +This is implemented in the scoreboard distributed with MOTH, and is how our +primary score calculation at Cyber Fire. + +For each category: + +* Divide the team's score in this category by the highest score in this category +* Add that to the team's overall score + +This means the highest theoretical score in any event is the number of open +categories. + +This algorithm means that point values only matter relative to other point +values within that category. A category with 5 total points is worth the same as +a category with 5000 total points, and a 2 point puzzle in the first category is +worth as much as a 2000 point puzzle in the second. + +One interesting effect here is that a team solving a previously-unsolved puzzle +will reduce everybody else's ranking in that category, because it increases the +divisor for calculating that category's score. + +Cyber Fire used to not display overall score: we would only show each team's +relative ranking per category. We may go back to this at some point! + + +Category Completion +---------------- + +Cyber Fire also has a scoreboard called the "class" scoreboard, which lists each +team, and which puzzles they have completed. This provides instructors with a +graphical overview of how people are progressing through content. We can provide +assistance to the general group when we see that a large number of teams are +stuck on a particular puzzle, and we can provide individual assistance if we see +that someone isn't keeping up with the class. + + +Monarch Of The Hill +---------------- + +You could also implement a "winner takes all" approach: any team with the +maximum number of points in a category gets 1 point, and all other teams get 0. + + +Time Bonuses +----------- + +If you wanted to provide extra points to whichever team solves a puzzle first, +this is possible with the log. You could either boost a puzzle's point value or +decay it; either by timestamp, or by how many teams had solved it prior. + + +Bonkers Scoring +------------- + +Other zany options exist: + +* The first team to solve a puzzle with point value divisible by 7 gets double + points. +* [Tokens](tokens.md) with negative point values could be introduced, allowing + teams to manipulate other teams' scores, if they know the team ID. diff --git a/theme/basic.css b/theme/basic.css index 90be911..d67ad72 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -118,18 +118,18 @@ input:invalid { width: 100%; position: relative; } - -#rankings span { - font-size: 75%; - display: inline-block; - overflow: hidden; - height: 1.7em; +#rankings div:nth-child(6n){ + background-color: #8881; +} +#rankings div:nth-child(6n+3) { + background-color: #0f01; } #rankings span.teamname { + height: auto; font-size: inherit; color: white; - text-shadow: 0 0 3px black; - opacity: 0.8; + background-color: #000e; + border-radius: 3px; position: absolute; right: 0.2em; } diff --git a/theme/moth.mjs b/theme/moth.mjs index bc5d190..a617753 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -21,7 +21,7 @@ class Hash { } /** - * Dan Bernstein hash with xor improvement + * Dan Bernstein hash with xor * * @param {string} buf Input * @returns {number} @@ -49,15 +49,30 @@ class Hash { return this.hexlify(hashArray); } - /** - * Hex-encode a byte array - * - * @param {number[]} buf Byte array - * @returns {string} - */ - static hexlify(buf) { - return buf.map(b => b.toString(16).padStart(2, "0")).join("") - } + /** + * SHA 1, but only the first 4 hexits (2 octets). + * + * Git uses this technique with 7 hexits (default) as a "short identifier". + * + * @param {string} buf Input + */ + static async sha1_slice(buf, end=4) { + const msgUint8 = new TextEncoder().encode(buf) + const hashBuffer = await crypto.subtle.digest("SHA-1", msgUint8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hexits = this.hexlify(hashArray) + return hexits.slice(0, end) + } + + /** + * Hex-encode a byte array + * + * @param {number[]} buf Byte array + * @returns {string} + */ + static hexlify(buf) { + return buf.map(b => b.toString(16).padStart(2, "0")).join("") + } /** * Apply every hash to the input buffer. @@ -68,8 +83,8 @@ class Hash { static async All(buf) { return [ String(this.djb2(buf)), - String(this.djb2xor(buf)), await this.sha256(buf), + await this.sha1_slice(buf), ] } } @@ -223,6 +238,111 @@ class Puzzle { } } +/** + * A snapshot of scores. + */ +class Scores { + constructor() { + /** + * Timestamp of this score snapshot + * @type number + */ + this.Timestamp = 0 + + /** + * All categories present in this snapshot. + * + * ECMAScript sets preserve order, so iterating over this will yield + * categories as they were added to the points log. + * + * @type {Set.} + */ + this.Categories = new Set() + + /** + * All team IDs present in this snapshot + * @type {Set.} + */ + this.TeamIDs = new Set() + + /** + * Highest score in each category + * @type {Object.} + */ + this.MaxPoints = {} + + this.categoryTeamPoints = {} + } + + /** + * Return a sorted list of category names + * + * @returns {string[]} + */ + SortedCategories() { + let categories = [...this.Categories] + categories.sort((a,b) => a.localeCompare(b, "en", {sensitivity: "base"})) + return categories + } + + /** + * Add an award to a team's score. + * + * Updates this.Timestamp to the award's timestamp. + * + * @param {Award} award + */ + Add(award) { + this.Timestamp = award.Timestamp + this.Categories.add(award.Category) + this.TeamIDs.add(award.TeamID) + + let teamPoints = (this.categoryTeamPoints[award.Category] ??= {}) + let points = (teamPoints[award.TeamID] || 0) + award.Points + teamPoints[award.TeamID] = points + + let max = this.MaxPoints[award.Category] || 0 + this.MaxPoints[award.Category] = Math.max(max, points) + } + + /** + * Get a team's score within a category. + * + * @param {string} category + * @param {string} teamID + * @returns {number} + */ + GetPoints(category, teamID) { + let teamPoints = this.categoryTeamPoints[category] || {} + return teamPoints[teamID] || 0 + } + + /** + * Calculate a team's score in a category, using the Cyber Fire algorithm. + * + *@param {string} category + * @param {string} teamID + */ + CyFiCategoryScore(category, teamID) { + return this.GetPoints(category, teamID) / this.MaxPoints[category] + } + + /** + * Calculate a team's overall score, using the Cyber Fire algorithm. + * + *@param {string} category + * @param {string} teamID + * @returns {number} + */ + CyFiScore(teamID) { + let score = 0 + for (let category of this.Categories) { + score += this.CyFiCategoryScore(category, teamID) + } + return score + } +} + /** * MOTH instance state. */ @@ -351,51 +471,33 @@ class State { return false } - /** - * Map from team ID to points. - * - * A special "max" property contains the highest number of points in this map. - * - * @typedef {Object.} TeamPointsDict - * @property {Number} max Highest number of points - */ - - /** - * Map from category to PointsDict. - * - * @typedef {Object.} CategoryTeamPointsDict - */ - - /** - * Score snapshot. - * - * @typedef {Object} ScoreSnapshot - * @property {number} when Epoch time of this snapshot - * @property {CategoryTeamPointsDict} snapshot - */ - /** * Replay scores. * - * @yields {ScoreSnapshot} Snapshot at a point in time + * MOTH has no notion of who is "winning", we consider this a user interface + * decision. There are lots of interesting options: see + * [scoring]{@link ../docs/scoring.md} for more. + * + * @yields {Scores} Snapshot at a point in time */ * ScoreHistory() { - /** @type {CategoryTeamPointsDict} */ - let categoryTeamPoints = {} + let scores = new Scores() for (let award of this.PointsLog) { - let teamPoints = (categoryTeamPoints[award.Category] ??= {}) - let points = teamPoints[award.TeamID] || 0 - let max = teamPoints.max || 0 - - points += award.Points - teamPoints[award.TeamID] = points - teamPoints.max = Math.max(points, max) - - /** @type ScoreSnapshot */ - let snapshot = {when: award.When, snapshot: categoryTeamPoints} - yield snapshot + scores.Add(award) + yield scores } } + + /** + * Calculate the current scores. + * + * @returns {Scores} + */ + CurrentScore() { + let scores + for (scores of this.ScoreHistory()); + return scores + } } /** diff --git a/theme/scoreboard.html b/theme/scoreboard.html index 8b1f787..74af715 100644 --- a/theme/scoreboard.html +++ b/theme/scoreboard.html @@ -12,7 +12,6 @@
      -
      diff --git a/theme/scoreboard.mjs b/theme/scoreboard.mjs index e2d7cfc..8f7ce6f 100644 --- a/theme/scoreboard.mjs +++ b/theme/scoreboard.mjs @@ -1,267 +1,89 @@ -// jshint asi:true +import * as moth from "./moth.mjs" +import * as common from "./common.mjs" -// import { Chart, registerables } from "https://cdn.jsdelivr.net/npm/chart.js@3.0.2" -// import {DateTime} from "https://cdn.jsdelivr.net/npm/luxon@1.26.0" -// import "https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@0.1.1" -// Chart.register(...registerables) +const server = new moth.Server(".") +const ReplayDuration = 3 * common.Second +const MaxFrameRate = 24 +/** Don't let any team's score exceed this percentage width */ +const MaxScoreWidth = 95 -const MILLISECOND = 1 -const SECOND = 1000 * MILLISECOND -const MINUTE = 60 * SECOND +/** + * Returns a promise that resolves after timeout. + * + * @param {Number} timeout How long to sleep (milliseconds) + * @returns {Promise} + */ +function sleep(timeout) { + return new Promise(resolve => setTimeout(resolve, timeout)); +} -// If all else fails... -setInterval(() => location.reload(), 30 * SECOND) +/** + * Pull new points log, and update the scoreboard. + * + * The update is animated, because I think that looks cool. + */ +async function update() { + let state = await server.GetState() + let rankingsElement = document.querySelector("#rankings") + let logSize = state.PointsLog.length -function scoreboardInit() { - let chartColors = [ - "rgb(255, 99, 132)", - "rgb(255, 159, 64)", - "rgb(255, 205, 86)", - "rgb(75, 192, 192)", - "rgb(54, 162, 235)", - "rgb(153, 102, 255)", - "rgb(201, 203, 207)" - ] - - for (let q of document.querySelectorAll("[data-url]")) { - let url = new URL(q.dataset.url, document.location) - q.textContent = url.hostname - if (url.port) { - q.textContent += `:${url.port}` - } - if (url.pathname != "/") { - q.textContent += url.pathname - } - } - for (let q of document.querySelectorAll(".qrcode")) { - let url = new URL(q.dataset.url, document.location) - let qr = new QRious({ - element: q, - value: url.toString(), - }) + // Figure out the timing so that we can replay the scoreboard in about + // ReplayDuration, but no more than 24 frames per second. + let frameModulo = 1 + let delay = 0 + while (delay < (common.Second / MaxFrameRate)) { + frameModulo += 1 + delay = ReplayDuration / (logSize / frameModulo) } - let chart - let canvas = document.querySelector("#chart canvas") - if (canvas) { - chart = new Chart(canvas.getContext("2d"), { - type: "line", - options: { - responsive: true, - scales: { - x: { - type: "time", - time: { - // XXX: the manual says this should do something, it does something in the samples, IDK - tooltipFormat: "HH:mm" - }, - title: { - display: true, - text: "Time" - } - }, - y: { - title: { - display: true, - text: "Points" - } - } - }, - tooltips: { - mode: "index", - intersect: false - }, - hover: { - mode: "nearest", - intersect: true + let frame = 0 + for (let scores of state.ScoreHistory()) { + frame += 1 + if ((frame < state.PointsLog.length) && (frame % frameModulo)) { + continue + } + + while (rankingsElement.firstChild) rankingsElement.firstChild.remove() + + let sortedTeamIDs = [...scores.TeamIDs] + sortedTeamIDs.sort((a, b) => scores.CyFiScore(a) - scores.CyFiScore(b)) + sortedTeamIDs.reverse() + + let topScore = scores.CyFiScore(sortedTeamIDs[0]) + for (let teamID of sortedTeamIDs) { + let teamName = state.TeamNames[teamID] + + let row = rankingsElement.appendChild(document.createElement("div")) + + let heading = row.appendChild(document.createElement("span")) + heading.textContent = teamName + heading.classList.add("teamname") + + let categoryNumber = 0 + for (let category of scores.Categories) { + let score = scores.CyFiCategoryScore(category, teamID) + if (!score) { + continue } - } - }) + + let block = row.appendChild(document.createElement("span")) + let points = scores.GetPoints(category, teamID) + let width = MaxScoreWidth * score / topScore + + block.textContent = category + block.title = `${points} points` + block.style.width = `${width}%` + block.classList.add(`cat${categoryNumber}`) + categoryNumber += 1 + } + } + await sleep(delay) } - - async function refresh() { - let resp = await fetch("../state") - let state = await resp.json() - - for (let rotate of document.querySelectorAll(".rotate")) { - rotate.appendChild(rotate.firstElementChild) - } - window.scrollTo(0,0) - - let element = document.getElementById("rankings") - let teamNames = state.TeamNames - let pointsLog = state.PointsLog - - // Every machine that's displaying the scoreboard helpfully stores the last 20 values of - // points.json for us, in case of catastrophe. Thanks, y'all! - // - // We have been doing some variation on this "everybody backs up the server state" trick since 2009. - // We have needed it 0 times. - let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || [] - if (pointsHistory.length >= 20) { - pointsHistory.shift() - } - pointsHistory.push(pointsLog) - localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory)) - - let teams = {} - let highestCategoryScore = {} // map[string]int - - // Initialize data structures - for (let teamId in teamNames) { - teams[teamId] = { - categoryScore: {}, // map[string]int - overallScore: 0, // int - historyLine: [], // []{x: int, y: int} - name: teamNames[teamId], - id: teamId - } - } - - // Dole out points - for (let entry of pointsLog) { - let timestamp = entry[0] - let teamId = entry[1] - let category = entry[2] - let points = entry[3] - - let team = teams[teamId] - - let score = team.categoryScore[category] || 0 - score += points - team.categoryScore[category] = score - - let highest = highestCategoryScore[category] || 0 - if (score > highest) { - highestCategoryScore[category] = score - } - } - - for (let teamId in teamNames) { - teams[teamId].categoryScore = {} - } - - for (let entry of pointsLog) { - let timestamp = entry[0] - let teamId = entry[1] - let category = entry[2] - let points = entry[3] - - let team = teams[teamId] - - let score = team.categoryScore[category] || 0 - score += points - team.categoryScore[category] = score - - let overall = 0 - for (let cat in team.categoryScore) { - overall += team.categoryScore[cat] / highestCategoryScore[cat] - } - - team.historyLine.push({x: timestamp * 1000, y: overall}) - } - - // Compute overall scores based on current highest - for (let teamId in teams) { - let team = teams[teamId] - team.overallScore = 0 - for (let cat in team.categoryScore) { - team.overallScore += team.categoryScore[cat] / highestCategoryScore[cat] - } - } - - // Sort by team score - function teamCompare(a, b) { - return a.overallScore - b.overallScore - } - - // Figure out how to order each team on the scoreboard - let winners = [] - for (let teamId in teams) { - winners.push(teams[teamId]) - } - winners.sort(teamCompare) - winners.reverse() - - // Let's make some better names for things we've computed - let winningScore = winners[0].overallScore - let numCategories = Object.keys(highestCategoryScore).length - - // Clear out the element we're about to populate - Array.from(element.childNodes).map(e => e.remove()) - - let maxWidth = 100 / winningScore - for (let team of winners) { - let row = document.createElement("div") - let ncat = 0 - for (let category in highestCategoryScore) { - let catHigh = highestCategoryScore[category] - let catTeam = team.categoryScore[category] || 0 - let catPct = catTeam / catHigh - let width = maxWidth * catPct - - let bar = document.createElement("span") - bar.classList.add("category") - bar.classList.add("cat" + ncat) - bar.style.width = width + "%" - bar.textContent = category + ": " + catTeam - bar.title = bar.textContent - - row.appendChild(bar) - ncat += 1 - } - - let te = document.createElement("span") - te.classList.add("teamname") - te.textContent = team.name - row.appendChild(te) - - element.appendChild(row) - } - - if (!chart) { - return - } - - /* - * Update chart - */ - chart.data.datasets = [] - for (let i in winners) { - if (i > 5) { - break - } - let team = winners[i] - let color = chartColors[i % chartColors.length] - chart.data.datasets.push({ - label: team.name, - backgroundColor: color, - borderColor: color, - data: team.historyLine, - lineTension: 0, - fill: false - }) - } - chart.update() - window.chart = chart - } - - function init() { - let base = window.location.href.replace("scoreboard.html", "") - let location = document.querySelector("#location") - if (location) { - location.textContent = base - } - - setInterval(refresh, 20 * SECOND) - refresh() - } - - init() } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", scoreboardInit) -} else { - scoreboardInit() +function init() { + setInterval(update, common.Minute) + update() } + +common.WhenDOMLoaded(init) \ No newline at end of file From bb4859e7a96c382eea70e8bf7cfb9ec7ec275c90 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 15 Sep 2023 16:09:08 -0600 Subject: [PATCH 93/98] URL in scoreboard (configurable) --- theme/basic.css | 35 ++--------------------------------- theme/common.mjs | 21 +++++++++++++++++++++ theme/config.json | 1 + theme/index.mjs | 6 ++---- theme/moth.mjs | 1 + theme/puzzle.mjs | 4 ++-- theme/scoreboard.css | 33 ++++++++++++++++++++++++++++----- theme/scoreboard.html | 7 +++---- theme/scoreboard.mjs | 10 ++++++++-- 9 files changed, 68 insertions(+), 50 deletions(-) diff --git a/theme/basic.css b/theme/basic.css index d67ad72..667e99d 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -113,36 +113,7 @@ input:invalid { cursor: help; } -/** Scoreboard */ -#rankings { - width: 100%; - position: relative; -} -#rankings div:nth-child(6n){ - background-color: #8881; -} -#rankings div:nth-child(6n+3) { - background-color: #0f01; -} -#rankings span.teamname { - height: auto; - font-size: inherit; - color: white; - background-color: #000e; - border-radius: 3px; - position: absolute; - right: 0.2em; -} -#rankings div * {white-space: nowrap;} -.cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;} -.cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;} -.cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;} -.cat3, .cat11, .cat19 {background-color: #33a02c; color: white;} -.cat4, .cat12, .cat20 {background-color: #fb9a99; color: black;} -.cat5, .cat13, .cat21 {background-color: #e31a1c; color: white;} -.cat6, .cat14, .cat22 {background-color: #fdbf6f; color: black;} -.cat7, .cat15, .cat23 {background-color: #ff7f00; color: black;} - +/** Development mode information */ .debug { overflow: auto; padding: 1em; @@ -203,11 +174,9 @@ li[draggable] { } @media (prefers-color-scheme: light) { - /* - * This uses the alpha channel to apply hue tinting to elements, to get a + /* We uses the alpha channel to apply hue tinting to elements, to get a * similar effect in light or dark mode. That means there aren't a whole lot of * things to change between light and dark mode. - * */ body { background-color: #b9cbd8; diff --git a/theme/common.mjs b/theme/common.mjs index c797475..c9e49a8 100644 --- a/theme/common.mjs +++ b/theme/common.mjs @@ -5,6 +5,9 @@ const Millisecond = 1 const Second = Millisecond * 1000 const Minute = Second * 60 +/** URL to the top of this MOTH server */ +const BaseURL = new URL(".", location) + /** * Display a transient message to the user. * @@ -53,11 +56,29 @@ function Truthy(s) { return true } + +/** + * Fetch the configuration object for this theme. + * + * @returns {Promise.} + */ +async function Config() { + let resp = await fetch( + new URL("config.json", BaseURL), + { + cache: "no-cache" + }, + ) + return resp.json() +} + export { Millisecond, Second, Minute, + BaseURL, Toast, WhenDOMLoaded, Truthy, + Config, } diff --git a/theme/config.json b/theme/config.json index 9f39211..1d5a0a1 100644 --- a/theme/config.json +++ b/theme/config.json @@ -1,4 +1,5 @@ { "TrackSolved": true, + "URLInScoreboard": true, "__sentry__": "this is here so you don't have to remember to take the comma off the last item" } \ No newline at end of file diff --git a/theme/index.mjs b/theme/index.mjs index 9bc79a2..e9de612 100644 --- a/theme/index.mjs +++ b/theme/index.mjs @@ -6,7 +6,6 @@ import * as common from "./common.mjs" class App { constructor(basePath=".") { - this.configURL = new URL("config.json", location) this.config = {} this.server = new moth.Server(basePath) @@ -74,8 +73,7 @@ class App { * load, since configuration should (hopefully) change less frequently. */ async UpdateConfig() { - let resp = await fetch(this.configURL) - this.config = await resp.json() + this.config = await common.Config() } /** @@ -150,7 +148,7 @@ class App { for (let puzzle of this.state.Puzzles(cat)) { let i = l.appendChild(document.createElement("li")) - let url = new URL("puzzle.html", window.location) + let url = new URL("puzzle.html", common.BaseURL) url.hash = `${puzzle.Category}:${puzzle.Points}` let a = i.appendChild(document.createElement("a")) a.textContent = puzzle.Points diff --git a/theme/moth.mjs b/theme/moth.mjs index a617753..c1db74b 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -542,6 +542,7 @@ class Server { return fetch(url, { method: "POST", body, + cache: "no-cache", }) } diff --git a/theme/puzzle.mjs b/theme/puzzle.mjs index c53b6a1..6f886d5 100644 --- a/theme/puzzle.mjs +++ b/theme/puzzle.mjs @@ -131,7 +131,7 @@ function writeObject(e, obj) { */ async function loadPuzzle(category, points) { console.groupCollapsed("Loading puzzle:", category, points) - let contentBase = new URL(`content/${category}/${points}/`, location) + let contentBase = new URL(`content/${category}/${points}/`, common.BaseURL) // Tell user we're loading puzzleElement().appendChild(document.createElement("progress")) @@ -209,7 +209,7 @@ async function init() { // Make all links absolute, because we're going to be changing the base URL for (let e of document.querySelectorAll("[href]")) { - e.href = new URL(e.href, location) + e.href = new URL(e.href, common.BaseURL) } let hashpart = location.hash.split("#")[1] || "" diff --git a/theme/scoreboard.css b/theme/scoreboard.css index 470e72d..a3f55d1 100644 --- a/theme/scoreboard.css +++ b/theme/scoreboard.css @@ -47,6 +47,18 @@ text-align: center; } +.location { + color: #acf; + background-color: #0008; + position: fixed; + right: 30vw; + bottom: 0; + padding: 1em; + margin: 0; + font-size: 1.2rem; + font-weight:bold; + text-decoration: underline; +} .qrcode { width: 30vw; } @@ -60,23 +72,34 @@ max-width: 40%; } +/** Scoreboard */ #rankings { width: 100%; - position: relative; - background-color: rgba(0, 0, 0, 0.8); + position: relative; + background-color: #000c; +} +#rankings div { + height: 1.4rem; +} +#rankings div:nth-child(6n){ + background-color: #ccc1; +} +#rankings div:nth-child(6n+3) { + background-color: #0f01; } #rankings span { font-size: 75%; display: inline-block; overflow: hidden; - height: 1.7em; + height: 1.4em; } #rankings span.teamname { + height: auto; font-size: inherit; color: white; - text-shadow: 0 0 3px black; - opacity: 0.8; + background-color: #000e; + border-radius: 3px; position: absolute; right: 0.2em; } diff --git a/theme/scoreboard.html b/theme/scoreboard.html index 74af715..1e14424 100644 --- a/theme/scoreboard.html +++ b/theme/scoreboard.html @@ -10,9 +10,8 @@ - -
      -
      -
      + +
      +
      diff --git a/theme/scoreboard.mjs b/theme/scoreboard.mjs index 8f7ce6f..5d2e161 100644 --- a/theme/scoreboard.mjs +++ b/theme/scoreboard.mjs @@ -2,8 +2,8 @@ import * as moth from "./moth.mjs" import * as common from "./common.mjs" const server = new moth.Server(".") -const ReplayDuration = 3 * common.Second -const MaxFrameRate = 24 +const ReplayDuration = 0.3 * common.Second +const MaxFrameRate = 60 /** Don't let any team's score exceed this percentage width */ const MaxScoreWidth = 95 @@ -23,6 +23,12 @@ function sleep(timeout) { * The update is animated, because I think that looks cool. */ async function update() { + let config = await common.Config() + for (let e of document.querySelectorAll(".location")) { + e.textContent = common.BaseURL + e.classList.toggle("hidden", !config.URLInScoreboard) + } + let state = await server.GetState() let rankingsElement = document.querySelector("#rankings") let logSize = state.PointsLog.length From 768600e48e6f869c0a62e7c3281c2f3d6d40840f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 15 Sep 2023 16:13:09 -0600 Subject: [PATCH 94/98] Logout in devel mode generates a new TeamID --- theme/index.mjs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/theme/index.mjs b/theme/index.mjs index e9de612..231c845 100644 --- a/theme/index.mjs +++ b/theme/index.mjs @@ -10,12 +10,6 @@ class App { this.server = new moth.Server(basePath) - let uuid = Math.floor(Math.random() * 1000000).toString(16) - this.fakeRegistration = { - TeamID: uuid, - TeamName: `Team ${uuid}`, - } - for (let form of document.querySelectorAll("form.login")) { form.addEventListener("submit", event => this.handleLoginSubmit(event)) } @@ -103,9 +97,10 @@ class App { } if (this.state.DevelopmentMode() && !this.server.LoggedIn()) { + let teamID = Math.floor(Math.random() * 1000000).toString(16) common.Toast("Automatically logging in to devel server") - console.info("Logging in with generated Team ID and Team Name", this.fakeRegistration) - return this.Login(this.fakeRegistration.TeamID, this.fakeRegistration.TeamName) + console.info(`Logging in with generated Team ID: ${teamID}`) + return this.Login(teamID, `Team ${teamID}`) } } From 5350cf73a08d5ed16db3cf7e462e868aab1012bc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 19 Sep 2023 16:48:24 -0600 Subject: [PATCH 95/98] leadership sprint bugfixes * Messages now in config.json * puzzle.html: display errors --- theme/config.json | 1 + theme/index.mjs | 9 +++++---- theme/moth.mjs | 2 +- theme/puzzle.mjs | 22 +++++++++++++++++----- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/theme/config.json b/theme/config.json index 1d5a0a1..d32b227 100644 --- a/theme/config.json +++ b/theme/config.json @@ -1,5 +1,6 @@ { "TrackSolved": true, "URLInScoreboard": true, + "Messages": "", "__sentry__": "this is here so you don't have to remember to take the comma off the last item" } \ No newline at end of file diff --git a/theme/index.mjs b/theme/index.mjs index 231c845..d30c013 100644 --- a/theme/index.mjs +++ b/theme/index.mjs @@ -68,6 +68,10 @@ class App { */ async UpdateConfig() { this.config = await common.Config() + + for (let e of document.querySelectorAll(".messages")) { + e.innerHTML = this.config.Messages || "" + } } /** @@ -79,9 +83,6 @@ class App { */ async UpdateState() { this.state = await this.server.GetState() - for (let e of document.querySelectorAll(".messages")) { - e.innerHTML = this.state.Messages - } // Update elements with data-track-solved for (let e of document.querySelectorAll("[data-track-solved]")) { @@ -133,7 +134,7 @@ class App { if (this.state.DevelopmentMode()) { let a = h.appendChild(document.createElement('a')) a.classList.add("mothball") - a.textContent = "📦" + a.textContent = "⬇️" a.href = this.server.URL(`mothballer/${cat}.mb`) a.title = "Download a compiled puzzle for this category" } diff --git a/theme/moth.mjs b/theme/moth.mjs index c1db74b..e59dfcb 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -352,7 +352,7 @@ class State { * @param {Object} obj Raw state data */ constructor(server, obj) { - for (let key of ["Config", "Messages", "TeamNames", "PointsLog"]) { + for (let key of ["Config", "TeamNames", "PointsLog"]) { if (!obj[key]) { throw(`Missing state property: ${key}`) } diff --git a/theme/puzzle.mjs b/theme/puzzle.mjs index 6f886d5..ef830a3 100644 --- a/theme/puzzle.mjs +++ b/theme/puzzle.mjs @@ -142,9 +142,24 @@ async function loadPuzzle(category, points) { } let puzzle = server.GetPuzzle(category, points) + console.time("Populate") - await puzzle.Populate() - console.timeEnd("Populate") + try { + await puzzle.Populate() + } + catch { + let error = puzzleElement().appendChild(document.createElement("pre")) + error.classList.add("notification", "error") + error.textContent = puzzle.Error.Body + return + } + finally { + console.timeEnd("Populate") + } + + console.info(`Setting base tag to ${contentBase}`) + let baseElement = document.head.appendChild(document.createElement("base")) + baseElement.href = contentBase console.info("Tweaking HTML...") let title = `${category} ${points}` @@ -183,9 +198,6 @@ async function loadPuzzle(category, points) { } } - let baseElement = document.head.appendChild(document.createElement("base")) - baseElement.href = contentBase - window.app.puzzle = puzzle console.info("window.app.puzzle =", window.app.puzzle) From 3282ad22b0ab90c26cfdd93e51169824ec401dba Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 27 Sep 2023 16:10:31 -0600 Subject: [PATCH 96/98] Scores, not Score --- .gitlab-ci.yml | 2 +- theme/moth.mjs | 4 ++-- theme/scoreboard.mjs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 058787d..abfb2c5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ stages: Run unit tests: stage: test - image: &goimage golang:1.18 + image: &goimage golang:1.21 only: refs: - main diff --git a/theme/moth.mjs b/theme/moth.mjs index e59dfcb..dd23081 100644 --- a/theme/moth.mjs +++ b/theme/moth.mjs @@ -480,7 +480,7 @@ class State { * * @yields {Scores} Snapshot at a point in time */ - * ScoreHistory() { + * ScoresHistory() { let scores = new Scores() for (let award of this.PointsLog) { scores.Add(award) @@ -493,7 +493,7 @@ class State { * * @returns {Scores} */ - CurrentScore() { + CurrentScores() { let scores for (scores of this.ScoreHistory()); return scores diff --git a/theme/scoreboard.mjs b/theme/scoreboard.mjs index 5d2e161..e600292 100644 --- a/theme/scoreboard.mjs +++ b/theme/scoreboard.mjs @@ -43,7 +43,7 @@ async function update() { } let frame = 0 - for (let scores of state.ScoreHistory()) { + for (let scores of state.ScoresHistory()) { frame += 1 if ((frame < state.PointsLog.length) && (frame % frameModulo)) { continue From 12979a55a3e35e94e6fc1dc07c3dc274328127d2 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 27 Sep 2023 16:14:23 -0600 Subject: [PATCH 97/98] We're not doing github builds any more --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b78bab7..f2ce709 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ Dirtbags Monarch Of The Hill Server ===================== -![Build badge](https://github.com/dirtbags/moth/workflows/Build/Test/Push/badge.svg) ![Go report card](https://goreportcard.com/badge/github.com/dirtbags/moth) Monarch Of The Hill (MOTH) is a puzzle server. From 1ca2ec284fdc3b749c4c8929864d7f6e663c121c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 27 Sep 2023 16:16:04 -0600 Subject: [PATCH 98/98] make the report card link to the report card --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2ce709..183e405 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Dirtbags Monarch Of The Hill Server ===================== -![Go report card](https://goreportcard.com/badge/github.com/dirtbags/moth) +[![Go report card](https://goreportcard.com/badge/github.com/dirtbags/moth)](https://goreportcard.com/report/github.com/dirtbags/moth) Monarch Of The Hill (MOTH) is a puzzle server. We (the authors) have used it for instructional and contest events called