diff --git a/.golangci.yml b/.golangci.yml index 1517a8c..7700db5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,7 +2,6 @@ linters: enable-all: true disable: - gofumpt - - interfacer - varcheck - nosnakecase - interfacer @@ -16,9 +15,10 @@ linters: - depguard - wsl - wrapcheck - - nlreturn - goerr113 - exhaustruct - godox - - varnamelen - - cyclop + +linters-settings: + cyclop: + max-complexity: 15 diff --git a/README.md b/README.md index a624586..6c55aa0 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,33 @@ blanka -i 51013 -t 51000 -e ![blanka logo](https://algolymp.ru/static/img/blanka.png) +## boban +*Filter Ejudge runs.* + +### Abount + +Filter and print Ejudge runs IDs in specified contest. + +### Flags +- `-i` - contest id (required) +- `-f` - filter expression (default: empty) +- `-c` - last runs count (default: 20) + +### Config +- `ejudge.url` +- `ejudge.login` +- `ejudge.password` + +### Examples +```bash +boban --help +boban -i 47106 -f "prob == 'A'" > runs.txt +boban -i 50014 -f "status == PR" -c 1000 +boban -i 50014 -c 10000 2> /dev/null | wc -l +``` + +![boban logo](https://algolymp.ru/static/img/boban.png) + ## casper *Change Ejudge contest visibility by id.* diff --git a/cmd/boban/main.go b/cmd/boban/main.go new file mode 100644 index 0000000..5f50d63 --- /dev/null +++ b/cmd/boban/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Gornak40/algolymp/config" + "github.com/Gornak40/algolymp/ejudge" + "github.com/akamensky/argparse" + "github.com/sirupsen/logrus" +) + +const ( + DefaultRunsCount = 20 +) + +func main() { + parser := argparse.NewParser("boban", "Filter Ejudge runs.") + cID := parser.Int("i", "cid", &argparse.Options{ + Required: true, + Help: "Ejudge contest ID", + }) + filter := parser.String("f", "filter", &argparse.Options{ + Required: false, + Help: "Filter expression", + }) + count := parser.Int("c", "count", &argparse.Options{ + Required: false, + Help: "Last runs count", + Default: DefaultRunsCount, + }) + if err := parser.Parse(os.Args); err != nil { + logrus.WithError(err).Fatal("bad arguments") + } + + cfg := config.NewConfig() + ejClient := ejudge.NewEjudge(&cfg.Ejudge) + + sid, err := ejClient.Login() + if err != nil { + logrus.WithError(err).Fatal("login failed") + } + + csid, err := ejClient.MasterLogin(sid, *cID) + if err != nil { + logrus.WithError(err).Fatal("master login failed") + } + + runs, err := ejClient.FilterRuns(csid, *filter, *count) + if err != nil { + logrus.WithError(err).Fatal("filter runs failed") + } + for _, run := range runs { + fmt.Println(run) //nolint:forbidigo // Basic functionality. + } + logrus.WithField("runs", len(runs)).Info("filter result") + + if err := ejClient.Logout(sid); err != nil { + logrus.WithError(err).Fatal("logout failed") + } +} diff --git a/cmd/ejik/main.go b/cmd/ejik/main.go index 6f5565f..f58319f 100644 --- a/cmd/ejik/main.go +++ b/cmd/ejik/main.go @@ -43,7 +43,11 @@ func main() { logrus.WithError(err).Fatal("check failed") } - if err := ejClient.ReloadConfig(sid, *cID); err != nil { + csid, err := ejClient.MasterLogin(sid, *cID) + if err != nil { + logrus.WithError(err).Fatal("master login failed") + } + if err := ejClient.ReloadConfig(csid); err != nil { logrus.WithError(err).Fatal("reload config failed") } diff --git a/config/config.go b/config/config.go index c477507..221a792 100644 --- a/config/config.go +++ b/config/config.go @@ -31,5 +31,6 @@ func NewConfig() *Config { if err := json.Unmarshal(data, &cfg); err != nil { logrus.WithError(err).Fatal("failed to unmarshal config") } + return &cfg } diff --git a/ejudge/ejudge.go b/ejudge/ejudge.go index 0d2b970..2dcd45b 100644 --- a/ejudge/ejudge.go +++ b/ejudge/ejudge.go @@ -1,10 +1,12 @@ package ejudge import ( + "errors" "fmt" "net/http" "net/http/cookiejar" "net/url" + "regexp" "strconv" "github.com/PuerkitoBio/goquery" @@ -32,6 +34,7 @@ type Ejudge struct { func NewEjudge(cfg *Config) *Ejudge { logrus.WithField("url", cfg.URL).Info("init ejudge engine") jar, _ := cookiejar.New(nil) + return &Ejudge{ cfg: cfg, client: &http.Client{ @@ -58,6 +61,7 @@ func (ej *Ejudge) postRequest(method string, params url.Values) (*http.Request, if err != nil { return nil, nil, err } + return resp.Request, doc, nil } @@ -74,6 +78,7 @@ func (ej *Ejudge) Login() (string, error) { return BadSID, err } logrus.WithField("SID", sid).Info("success login") + return sid, nil } @@ -87,16 +92,18 @@ func (ej *Ejudge) Logout(sid string) error { return err } logrus.WithField("SID", sid).Info("success logout") + return nil } func (ej *Ejudge) Lock(sid string, cid int) error { logrus.WithFields(logrus.Fields{"CID": cid, "SID": sid}).Info("lock contest for editing") _, _, err := ej.postRequest("serve-control", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "action": {"276"}, }) + return err } @@ -111,13 +118,14 @@ func (ej *Ejudge) Commit(sid string) error { } status := doc.Find("h2").First().Text() logrus.WithFields(logrus.Fields{"SID": sid}).Infof("ejudge answer %q", status) + return nil } func (ej *Ejudge) CheckContest(sid string, cid int, verbose bool) error { logrus.WithFields(logrus.Fields{"CID": cid, "SID": sid}).Info("check contest settings, wait please") _, doc, err := ej.postRequest("serve-control", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "action": {"262"}, }) @@ -129,43 +137,82 @@ func (ej *Ejudge) CheckContest(sid string, cid int, verbose bool) error { } status := doc.Find("h2").First().Text() logrus.WithFields(logrus.Fields{"CID": cid, "SID": sid}).Infof("ejudge answer %q", status) + return nil } -func (ej *Ejudge) ReloadConfig(sid string, cid int) error { +func (ej *Ejudge) MasterLogin(sid string, cid int) (string, error) { req, _, err := ej.postRequest("new-master", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "action": {"3"}, }) if err != nil { - return err + return "", err } csid := req.URL.Query().Get("SID") if csid == "" { - return ErrParseMasterSID + return "", ErrParseMasterSID } logrus.WithFields(logrus.Fields{"CID": cid, "CSID": csid, "SID": sid}).Info("success master login") - _, _, err = ej.postRequest("new-master", url.Values{ + + return csid, nil +} + +func (ej *Ejudge) FilterRuns(csid string, filter string, count int) ([]int, error) { + _, doc, err := ej.postRequest("new-master", url.Values{ + "SID": {csid}, + "filter_view": {"1"}, + "filter_expr": {filter}, + "filter_first_run": {"-1"}, + "filter_last_run": {strconv.Itoa(-count)}, + }) + if err != nil { + return nil, err + } + ejErr := doc.Find("#container > pre") + if ejErr.Text() != "" { + return nil, errors.New(ejErr.Text()) + } + res := doc.Find("#container > table:nth-child(18) > tbody > tr > td:nth-child(1)") + digits := regexp.MustCompile("[^0-9]+") + runsStr := res.Map(func(_ int, s *goquery.Selection) string { + return digits.ReplaceAllString(s.Text(), "") + }) + runs := make([]int, 0, len(runsStr)) + for _, s := range runsStr { + run, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + runs = append(runs, run) + } + + return runs, nil +} + +func (ej *Ejudge) ReloadConfig(csid string) error { + _, _, err := ej.postRequest("new-master", url.Values{ "SID": {csid}, "action": {"62"}, }) if err != nil { return err } - logrus.WithFields(logrus.Fields{"CID": cid, "CSID": csid, "SID": sid}).Info("success reload config") + logrus.WithFields(logrus.Fields{"CSID": csid}).Info("success reload config") + return nil } func (ej *Ejudge) CreateContest(sid string, cid int, tid int) error { logrus.WithFields(logrus.Fields{"CID": cid, "TID": tid, "SID": sid}).Info("create contest") _, doc, err := ej.postRequest("serve-control", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "num_mode": {"1"}, "action": {"259"}, "templ_mode": {"1"}, - "templ_id": {strconv.FormatInt(int64(tid), 10)}, + "templ_id": {strconv.Itoa(tid)}, }) if err != nil { return err @@ -175,25 +222,28 @@ func (ej *Ejudge) CreateContest(sid string, cid int, tid int) error { status = "OK" } logrus.WithFields(logrus.Fields{"CID": cid, "TID": tid, "SID": sid}).Infof("ejudge answer %q", status) + return nil } func (ej *Ejudge) MakeInvisible(sid string, cid int) error { logrus.WithFields(logrus.Fields{"CID": cid, "SID": sid}).Info("make invisible") _, _, err := ej.postRequest("serve-control", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "action": {"6"}, }) + return err } func (ej *Ejudge) MakeVisible(sid string, cid int) error { logrus.WithFields(logrus.Fields{"CID": cid, "SID": sid}).Info("make visible") _, _, err := ej.postRequest("serve-control", url.Values{ - "contest_id": {strconv.FormatInt(int64(cid), 10)}, + "contest_id": {strconv.Itoa(cid)}, "SID": {sid}, "action": {"7"}, }) + return err } diff --git a/logos/boban.png b/logos/boban.png new file mode 100644 index 0000000..d6c944c Binary files /dev/null and b/logos/boban.png differ diff --git a/logos/casper.png b/logos/casper.png index 53c77e7..41c55d3 100644 Binary files a/logos/casper.png and b/logos/casper.png differ diff --git a/polygon/api.go b/polygon/api.go index ba304c4..9d5edf4 100644 --- a/polygon/api.go +++ b/polygon/api.go @@ -36,6 +36,7 @@ type Polygon struct { func NewPolygon(cfg *Config) *Polygon { logrus.WithField("url", cfg.URL).Info("init polygon engine") + return &Polygon{ cfg: cfg, client: http.DefaultClient, @@ -60,6 +61,7 @@ func (p *Polygon) makeQuery(method string, link string) (*Answer, error) { if ans.Status != "OK" { return nil, errors.New(ans.Comment) } + return &ans, nil } @@ -71,6 +73,7 @@ func (p *Polygon) skipEscape(params url.Values) string { } } sort.Strings(pairs) + return strings.Join(pairs, "&") } @@ -120,6 +123,7 @@ func (p *Polygon) GetGroups(pID int) ([]GroupAnswer, error) { } var groups []GroupAnswer _ = json.Unmarshal(ansG.Result, &groups) + return groups, nil } @@ -135,6 +139,7 @@ func (p *Polygon) GetTests(pID int) ([]TestAnswer, error) { } var tests []TestAnswer _ = json.Unmarshal(ansT.Result, &tests) + return tests, nil } @@ -145,6 +150,7 @@ func (p *Polygon) EnableGroups(pID int) error { "enable": []string{"true"}, }) _, err := p.makeQuery(http.MethodPost, link) + return err } @@ -154,6 +160,7 @@ func (p *Polygon) EnablePoints(pID int) error { "enable": []string{"true"}, }) _, err := p.makeQuery(http.MethodPost, link) + return err } @@ -169,16 +176,19 @@ func NewTestRequest(pID int, index int) TestRequest { func (tr TestRequest) Group(group string) TestRequest { tr["testGroup"] = []string{group} + return tr } func (tr TestRequest) Points(points float32) TestRequest { tr["testPoints"] = []string{fmt.Sprint(points)} + return tr } func (p *Polygon) SaveTest(tReq TestRequest) error { link := p.buildURL("problem.saveTest", url.Values(tReq)) _, err := p.makeQuery(http.MethodPost, link) + return err } diff --git a/polygon/scoring.go b/polygon/scoring.go index 734363a..3136908 100644 --- a/polygon/scoring.go +++ b/polygon/scoring.go @@ -16,7 +16,9 @@ const ( ) var ( - ErrAllTestsAreSamples = fmt.Errorf("all tests are samples, try -s flag") + ErrAllTestsAreSamples = errors.New("all tests are samples, try -s flag") + ErrNoTestScore = errors.New("test_score is not supported yet") + ErrBadTestsOrder = errors.New("bad tests order, fix in polygon required") ) type Scoring struct { @@ -29,34 +31,35 @@ type Scoring struct { } func NewScoring(tests []TestAnswer, groups []GroupAnswer) (*Scoring, error) { - s := Scoring{ + scorer := Scoring{ score: map[string]int{}, count: map[string]int{}, first: map[string]int{}, last: map[string]int{}, dependencies: map[string][]string{}, } - for _, t := range tests { - s.score[t.Group] += int(t.Points) // TODO: ensure ejudge doesn't support float points - s.count[t.Group]++ - if val, ok := s.first[t.Group]; !ok || val > t.Index { - s.first[t.Group] = t.Index + for _, test := range tests { + scorer.score[test.Group] += int(test.Points) // TODO: ensure ejudge doesn't support float points + scorer.count[test.Group]++ + if val, ok := scorer.first[test.Group]; !ok || val > test.Index { + scorer.first[test.Group] = test.Index } - if val, ok := s.last[t.Group]; !ok || val < t.Index { - s.last[t.Group] = t.Index + if val, ok := scorer.last[test.Group]; !ok || val < test.Index { + scorer.last[test.Group] = test.Index } } - for _, g := range groups { - if g.PointsPolicy != "COMPLETE_GROUP" { - return nil, errors.New("test_score is not supported yet") + for _, group := range groups { + if group.PointsPolicy != "COMPLETE_GROUP" { + return nil, ErrNoTestScore } - if s.last[g.Name]-s.first[g.Name]+1 != s.count[g.Name] { - return nil, errors.New("bad tests order, fix in polygon required") + if scorer.last[group.Name]-scorer.first[group.Name]+1 != scorer.count[group.Name] { + return nil, ErrBadTestsOrder } - s.dependencies[g.Name] = g.Dependencies - s.groups = append(s.groups, g.Name) + scorer.dependencies[group.Name] = group.Dependencies + scorer.groups = append(scorer.groups, group.Name) } - return &s, nil + + return &scorer, nil } func (s *Scoring) buildValuer() string { @@ -70,6 +73,7 @@ func (s *Scoring) buildValuer() string { cur += "}\n" res = append(res, cur) } + return strings.Join(res, "\n") } @@ -84,7 +88,7 @@ func (s *Scoring) buildScoring() string { "\\textbf{Необходимые подзадачи}", "\\\\ \\hline", } - for i, g := range s.groups { + for i, group := range s.groups { var info string if i == 0 { info = "тесты из условия" @@ -92,9 +96,10 @@ func (s *Scoring) buildScoring() string { info = "---" } ans = append(ans, fmt.Sprintf("%s & %d & %s & %s \\\\ \\hline", - g, s.score[g], info, strings.Join(s.dependencies[g], ", "))) + group, s.score[group], info, strings.Join(s.dependencies[group], ", "))) } ans = append(ans, "\\end{tabular}", "\\end{center}") + return strings.Join(ans, "\n") } @@ -108,11 +113,11 @@ func (p *Polygon) InformaticsValuer(pID int, verbose bool) error { return err } - s, err := NewScoring(tests, groups) + scorer, err := NewScoring(tests, groups) if err != nil { return err } - valuer := s.buildValuer() + valuer := scorer.buildValuer() if verbose { logrus.Info("valuer.cfg\n" + valuer) } @@ -127,8 +132,9 @@ func (p *Polygon) InformaticsValuer(pID int, verbose bool) error { return err } - scoring := s.buildScoring() + scoring := scorer.buildScoring() fmt.Println(scoring) //nolint:forbidigo // Basic functionality. + return nil } @@ -143,45 +149,46 @@ func (p *Polygon) IncrementalScoring(pID int, samples bool) error { if err != nil { return err } - tc := 0 + testsCount := 0 for _, t := range tests { if t.UseInStatements && !samples { continue } - tc++ + testsCount++ } - if tc == 0 { + if testsCount == 0 { return ErrAllTestsAreSamples } - small := FullProblemScore / tc - smallCnt := tc - (FullProblemScore - small*tc) + small := FullProblemScore / testsCount + smallCnt := testsCount - (FullProblemScore - small*testsCount) logrus.WithFields(logrus.Fields{ - "zeroCount": len(tests) - tc, + "zeroCount": len(tests) - testsCount, "smallScore": small, "smallCount": smallCnt, "bigScore": small + 1, - "bigCount": tc - smallCnt, + "bigCount": testsCount - smallCnt, }).Info("points statistics") - for _, t := range tests { - var gr string - var pt int + for _, test := range tests { + var group string + var points int if smallCnt == 0 { //nolint:gocritic // It's smart piece of code. - gr = "2" - pt = small + 1 - } else if !t.UseInStatements || samples { - gr = "1" - pt = small + group = "2" + points = small + 1 + } else if !test.UseInStatements || samples { + group = "1" + points = small smallCnt-- } else { - gr = "0" - pt = 0 + group = "0" + points = 0 } - rt := NewTestRequest(pID, t.Index). - Group(gr). - Points(float32(pt)) + rt := NewTestRequest(pID, test.Index). + Group(group). + Points(float32(points)) if err := p.SaveTest(rt); err != nil { return err } } + return nil }