diff --git a/README.md b/README.md index c65b26c..86b3e1a 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ Extended release notes can be found at [chat](https://t.me/algolymp). | [ripper](#ripper) | change runs status | 🦍 | | ✅ | | [scalp](#scalp) | incremental scoring | | 🦍 | ✅ | | [valeria](#valeria) | valuer.cfg + tex scoring | | 🦍 | ✅ | +| [vydra](#vydra) | upload package | | 🦍 | 🧑‍💻 | | [wooda](#wooda) | glob problem files upload | | 🦍 | ✅ | | ⚙️ | move json config to ini | | | 🤔 | | 👻 | set good random group scores | | 🦍 | 🤔 | | 👻 | algolymp config manager | | | 🤔 | -| 👻 | upload package | | 🦍 | 🤔 | | 👻 | import polygon problem | 🦍 | 🦍 | 🤔 | | 👻 | autogen static problem | 🦍 | | 🤔 | | 👻 | zip extractor for websites | | | 🤔 | @@ -37,8 +37,10 @@ Extended release notes can be found at [chat](https://t.me/algolymp). - 🦍 Engines usage ## Build + +Download and install the latest version of [Go](https://go.dev/doc/install). + ```bash -sudo apt install go make export PATH=$(pwd)/bin:$PATH ``` @@ -316,6 +318,8 @@ Print `python` solution that outputs correct answer for each passed input file a Useful with Polygon to upload a problem without main correct solution. +**Make sure your input files has `\r\n` line endings (use `unix2dos`), because Polygon works in Windows.** + It's ready to work with any input/output files, encoding and escape sequences don't matter. Works great with [wooda](#wooda). @@ -450,6 +454,49 @@ valeria -i 285375 -t moscow -c n -c m -c k ![valeria logo](https://algolymp.ru/static/img/valeria.png) +## vydra +*Upload full problem package to Polygon using API.* + +### About + +**This tool is in beta right now.** + +This tool uses `problem.xml` for uploading all package content. + +Useful for migration between `polygon.lksh.ru` and `polygon.codeforces.com`. + +Designed to replace ~~legacy~~ [polygon-cli](https://github.com/kunyavskiy/polygon-cli) tool. + +**Ensure that the problem you are uploading the package into is empty.** + +### Known issues + +- If problem has testsets other than `tests`, you should create them manually, [issue](https://github.com/Codeforces/polygon-issue-tracking/issues/549); +- If problem is interactive, set `Is problem interactive` checkbox manually; +- If problem has statements resources, upload them manually; +- If problem has custom input/output, set it manually; +- If problem has [FreeMaker](https://freemarker.apache.org) generator, it will expand; +- If problem has stresses, unpload them manually; +- If checker is custom, it's recommended to set `Auto-update` checkbox for `testlib.h`. + +### Flags +- `-i` - problem id (required) +- `-p` - problem directory (default: `.`) + +### Config +- `polygon.url` +- `polygon.apiKey` +- `polygon.apiSecret` + +### Examples + +```bash +vydra --help +vydra -i 364022 +``` + +![vydra logo](https://algolymp.ru/static/img/vydra.png) + ## wooda *Upload problem files filtered by glob to Polygon using API.* diff --git a/cmd/vydra/main.go b/cmd/vydra/main.go new file mode 100644 index 0000000..5b251ab --- /dev/null +++ b/cmd/vydra/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + + "github.com/Gornak40/algolymp/config" + "github.com/Gornak40/algolymp/polygon" + "github.com/Gornak40/algolymp/polygon/vydra" + "github.com/akamensky/argparse" + "github.com/sirupsen/logrus" +) + +func main() { + parser := argparse.NewParser("vydra", "Upload package to Polygon.") + pID := parser.Int("i", "pid", &argparse.Options{ + Required: true, + Help: "Polygon problem ID", + }) + pDir := parser.String("p", "prob-dir", &argparse.Options{ + Required: false, + Default: ".", + Help: "Problem directory (with problem.xml)", + }) + + if err := parser.Parse(os.Args); err != nil { + logrus.WithError(err).Fatal("bad arguments") + } + if err := os.Chdir(*pDir); err != nil { + logrus.WithError(err).Fatal("bad problem directory") + } + + cfg := config.NewConfig() + pClient := polygon.NewPolygon(&cfg.Polygon) + + vyd := vydra.NewVydra(pClient, *pID) + errs := make(chan error) + go func() { + for err := range errs { + if err != nil { + logrus.WithError(err).Error("vydra error") + } + } + }() + if err := vyd.Upload(errs); err != nil { + logrus.WithError(err).Fatal("upload failed") + } +} diff --git a/go.mod b/go.mod index 799a1f3..a016d6f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/akamensky/argparse v1.4.0 + github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 ) @@ -12,7 +13,6 @@ require ( require ( github.com/andybalholm/cascadia v1.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect diff --git a/internal/natstream/natstream.go b/internal/natstream/natstream.go new file mode 100644 index 0000000..47482f5 --- /dev/null +++ b/internal/natstream/natstream.go @@ -0,0 +1,43 @@ +package natstream + +import ( + "errors" + "os" + "path/filepath" + + "github.com/facette/natsort" +) + +var ( + ErrEndStream = errors.New("no more files in natstream") +) + +type NatStream struct { + files []string + idx int +} + +func (ns *NatStream) Init(glob string) error { + files, err := filepath.Glob(glob) + if err != nil { + return err + } + ns.files = files + natsort.Sort(ns.files) + ns.idx = 0 + + return nil +} + +func (ns *NatStream) Next() (string, error) { + if ns.idx == len(ns.files) { + return "", ErrEndStream + } + data, err := os.ReadFile(ns.files[ns.idx]) + if err != nil { + return "", err + } + ns.idx++ + + return string(data), nil +} diff --git a/polygon/api.go b/polygon/api.go index e8d2df3..29ab8a6 100644 --- a/polygon/api.go +++ b/polygon/api.go @@ -22,15 +22,20 @@ import ( const ( sixSecretSymbols = "gorill" - defaultTestset = "tests" ) type SolutionTag string const ( - TagMain SolutionTag = "MA" - TagCorrect SolutionTag = "OK" - TagIncorrect SolutionTag = "RJ" + TagMain SolutionTag = "MA" + TagCorrect SolutionTag = "OK" + TagIncorrect SolutionTag = "RJ" + TagTimeLimit SolutionTag = "TL" + TagTLorOK SolutionTag = "TO" + TagWrongAnswer SolutionTag = "WA" + TagPresentationError SolutionTag = "PE" + TagMemoryLimit SolutionTag = "ML" + TagRuntimeError SolutionTag = "RE" ) type FileType string @@ -127,15 +132,31 @@ func (p *Polygon) makeQuery(method, link string, params url.Values) (*Answer, er } func (p *Polygon) skipEscape(params url.Values) string { - pairs := []string{} + type pair struct { + key string + value string + } + + var pairs []pair for k, vals := range params { for _, v := range vals { - pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) + pairs = append(pairs, pair{key: k, value: v}) } } - sort.Strings(pairs) + sort.Slice(pairs, func(i, j int) bool { + if pairs[i].key != pairs[j].key { + return pairs[i].key < pairs[j].key + } + + return pairs[i].value < pairs[j].value + }) + + pairs2 := make([]string, 0, len(pairs)) + for _, p := range pairs { + pairs2 = append(pairs2, fmt.Sprintf("%s=%s", p.key, p.value)) + } - return strings.Join(pairs, "&") + return strings.Join(pairs2, "&") } func (p *Polygon) buildURL(method string, params url.Values) (string, url.Values) { @@ -371,12 +392,57 @@ func (p *Polygon) SetInteractor(pID int, interactor string) error { return err } -func (p *Polygon) SaveSolution(pID int, name, data string, tag SolutionTag) error { - link, params := p.buildURL("problem.saveSolution", url.Values{ +func (p *Polygon) SaveScript(pID int, testset, source string) error { + link, params := p.buildURL("problem.saveScript", url.Values{ + "problemId": []string{strconv.Itoa(pID)}, + "testset": []string{testset}, + "source": []string{source}, + }) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveSolution(sr SolutionRequest) error { + link, params := p.buildURL("problem.saveSolution", url.Values(sr)) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveStatement(sr StatementRequest) error { + link, params := p.buildURL("problem.saveStatement", url.Values(sr)) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveValidatorTest(vtr ValidatorTestRequest) error { + link, params := p.buildURL("problem.saveValidatorTest", url.Values(vtr)) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveCheckerTest(ctr CheckerTestRequest) error { + link, params := p.buildURL("problem.saveCheckerTest", url.Values(ctr)) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveTestGroup(tgr TestGroupRequest) error { + link, params := p.buildURL("problem.saveTestGroup", url.Values(tgr)) + _, err := p.makeQuery(http.MethodPost, link, params) + + return err +} + +func (p *Polygon) SaveStatementResource(pID int, name, file string) error { + link, params := p.buildURL("problem.saveScript", url.Values{ "problemId": []string{strconv.Itoa(pID)}, "name": []string{name}, - "file": []string{data}, - "tag": []string{string(tag)}, + "file": []string{file}, }) _, err := p.makeQuery(http.MethodPost, link, params) diff --git a/polygon/requests.go b/polygon/requests.go index 55b9f28..c372dbb 100644 --- a/polygon/requests.go +++ b/polygon/requests.go @@ -4,6 +4,11 @@ import ( "fmt" "net/url" "strconv" + "strings" +) + +const ( + defaultTestset = "tests" ) type TestRequest url.Values @@ -16,6 +21,12 @@ func NewTestRequest(pID int, index int) TestRequest { } } +func (tr TestRequest) TestSet(testset string) TestRequest { + tr["testset"] = []string{testset} + + return tr +} + func (tr TestRequest) Group(group string) TestRequest { tr["testGroup"] = []string{group} @@ -95,7 +106,6 @@ func NewFileRequest(pID int, typ FileType, name, file string) FileRequest { } } -// TODO: fix it. func (fr FileRequest) CheckExisting(f bool) FileRequest { fr["checkExisting"] = []string{strconv.FormatBool(f)} @@ -109,3 +119,172 @@ func (fr FileRequest) SourceType(typ string) FileRequest { } // TODO: add other options + +type SolutionRequest url.Values + +func NewSolutionRequest(pID int, name, file string, tag SolutionTag) SolutionRequest { + return SolutionRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "name": []string{name}, + "file": []string{file}, + "tag": []string{string(tag)}, + } +} + +func (sr SolutionRequest) CheckExisting(f bool) SolutionRequest { + sr["checkExisting"] = []string{strconv.FormatBool(f)} + + return sr +} + +func (sr SolutionRequest) SourceType(typ string) SolutionRequest { + sr["sourceType"] = []string{typ} + + return sr +} + +type StatementRequest url.Values + +func NewStatementRequest(pID int, lang string) StatementRequest { + return StatementRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "lang": []string{lang}, + } +} + +func (sr StatementRequest) Encoding(enc string) StatementRequest { + sr["encoding"] = []string{enc} + + return sr +} + +func (sr StatementRequest) Name(name string) StatementRequest { + sr["name"] = []string{name} + + return sr +} + +func (sr StatementRequest) Legend(legend string) StatementRequest { + sr["legend"] = []string{legend} + + return sr +} + +func (sr StatementRequest) Input(input string) StatementRequest { + sr["input"] = []string{input} + + return sr +} + +func (sr StatementRequest) Output(output string) StatementRequest { + sr["output"] = []string{output} + + return sr +} + +func (sr StatementRequest) Scoring(scoring string) StatementRequest { + sr["scoring"] = []string{scoring} + + return sr +} + +func (sr StatementRequest) Interaction(interaction string) StatementRequest { + sr["interaction"] = []string{interaction} + + return sr +} + +func (sr StatementRequest) Notes(notes string) StatementRequest { + sr["notes"] = []string{notes} + + return sr +} + +func (sr StatementRequest) Tutorial(tutorial string) StatementRequest { + sr["tutorial"] = []string{tutorial} + + return sr +} + +type ValidatorTestRequest url.Values + +func NewValidatorTestRequest(pID, index int) ValidatorTestRequest { + return ValidatorTestRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "testIndex": []string{strconv.Itoa(index)}, + } +} + +func (vtr ValidatorTestRequest) Input(input string) ValidatorTestRequest { + vtr["testInput"] = []string{input} + + return vtr +} + +// VALID or INVALID. +func (vtr ValidatorTestRequest) Verdict(verdict string) ValidatorTestRequest { + vtr["testVerdict"] = []string{verdict} + + return vtr +} + +type CheckerTestRequest url.Values + +func NewCheckerTestRequest(pID, index int) CheckerTestRequest { + return CheckerTestRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "testIndex": []string{strconv.Itoa(index)}, + } +} + +func (ctr CheckerTestRequest) Input(input string) CheckerTestRequest { + ctr["testInput"] = []string{input} + + return ctr +} + +func (ctr CheckerTestRequest) Answer(answer string) CheckerTestRequest { + ctr["testAnswer"] = []string{answer} + + return ctr +} + +func (ctr CheckerTestRequest) Output(output string) CheckerTestRequest { + ctr["testOutput"] = []string{output} + + return ctr +} + +func (ctr CheckerTestRequest) Verdict(verdict string) CheckerTestRequest { + ctr["testVerdict"] = []string{verdict} + + return ctr +} + +type TestGroupRequest url.Values + +func NewTestGroupRequest(pID int, testset, group string) TestGroupRequest { + return TestGroupRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "testset": []string{testset}, + "group": []string{group}, + } +} + +func (tgr TestGroupRequest) PointsPolicy(policy string) TestGroupRequest { + tgr["pointsPolicy"] = []string{policy} + + return tgr +} + +func (tgr TestGroupRequest) FeedbackPolicy(feedback string) TestGroupRequest { + tgr["feedbackPolicy"] = []string{feedback} + + return tgr +} + +func (tgr TestGroupRequest) Dependencies(deps []string) TestGroupRequest { + tgr["dependencies"] = []string{strings.Join(deps, ",")} + + return tgr +} diff --git a/polygon/vydra/problemxml.go b/polygon/vydra/problemxml.go new file mode 100644 index 0000000..a074752 --- /dev/null +++ b/polygon/vydra/problemxml.go @@ -0,0 +1,123 @@ +package vydra + +type File struct { + Path string `xml:"path,attr"` + Type string `xml:"type,attr"` +} + +type Source struct { + Path string `xml:"path,attr"` + Type string `xml:"type,attr"` +} + +type Executable struct { + Source Source `xml:"source"` +} + +type Solution struct { + Tag string `xml:"tag,attr"` + Source Source `xml:"source"` +} + +type Statement struct { + Charset string `xml:"charset,attr"` + Language string `xml:"language,attr"` + Path string `xml:"path,attr"` + Type string `xml:"type,attr"` +} + +type Test struct { + Description string `xml:"description,attr"` + Method string `xml:"method,attr"` + Sample bool `xml:"sample,attr"` + Cmd string `xml:"cmd,attr"` + Verdict string `xml:"verdict,attr"` + Group string `xml:"group,attr"` + Points float32 `xml:"points,attr"` + FromFile string `xml:"from-file,attr"` +} + +type Group struct { + FeedbackPolicy string `xml:"feedback-policy,attr"` + PointsPolicy string `xml:"points-policy,attr"` + Name string `xml:"name,attr"` + Points float32 `xml:"points,attr"` + Dependencies struct { + Dependencies []struct { + Group string `xml:"group,attr"` + } `xml:"dependency"` + } `xml:"dependencies"` +} + +type TestSet struct { + Name string `xml:"name,attr"` + TimeLimit int `xml:"time-limit"` + MemoryLimit int `xml:"memory-limit"` + TestCount int `xml:"test-count"` + InputPathPattern string `xml:"input-path-pattern"` + OutputPathPattern string `xml:"output-path-pattern"` + AnswerPathPattern string `xml:"answer-path-pattern"` + Tests struct { + Tests []Test `xml:"test"` + } `xml:"tests"` + Groups struct { + Groups []Group `xml:"group"` + } `xml:"groups"` +} + +type Validator struct { + Source Source `xml:"source"` + TestSet TestSet `xml:"testset"` +} + +type Checker struct { + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Source Source `xml:"source"` + TestSet TestSet `xml:"testset"` +} + +type Assets struct { + Solutions struct { + Solutions []Solution `xml:"solution"` + } `xml:"solutions"` + Validators struct { + Validator *Validator `xml:"validator"` + } `xml:"validators"` + Checker *Checker `xml:"checker"` +} + +type Files struct { + Resources struct { + Files []File `xml:"file"` + } `xml:"resources"` + Executables struct { + Executables []Executable `xml:"executable"` + } `xml:"executables"` +} + +type Tag struct { + Value string `xml:"value,attr"` +} + +type Judging struct { + CPUName string `xml:"cpu-name,attr"` + CPUSpeed int `xml:"cpu-speed,attr"` + InputFile string `xml:"input-file,attr"` + OutputFile string `xml:"output-file,attr"` + TestSets []TestSet `xml:"testset"` +} + +type ProblemXML struct { + Revision int `xml:"revision,attr"` + ShortName string `xml:"short-name,attr"` + Assets Assets `xml:"assets"` + Files Files `xml:"files"` + Statements struct { + Statements []Statement `xml:"statement"` + } `xml:"statements"` + Judging Judging `xml:"judging"` + Tags struct { + Tags []Tag `xml:"tag"` + } `xml:"tags"` +} diff --git a/polygon/vydra/vydra.go b/polygon/vydra/vydra.go new file mode 100644 index 0000000..18543ee --- /dev/null +++ b/polygon/vydra/vydra.go @@ -0,0 +1,78 @@ +package vydra + +import ( + "encoding/xml" + "errors" + "os" + "strings" + + "github.com/Gornak40/algolymp/internal/natstream" + "github.com/Gornak40/algolymp/polygon" + "github.com/sirupsen/logrus" +) + +const ( + megabyte = 1024 * 1024 + + defaultTL = 1000 + defaultML = 256 + defaultInput = "stdin" + defaultOutput = "stdout" + + chkTests = "files/tests/checker-tests" +) + +var ( + ErrBadSolutionTag = errors.New("bad solution tag") +) + +type Vydra struct { + client *polygon.Polygon + pID int + prob ProblemXML + streamIn *natstream.NatStream + streamOut *natstream.NatStream + streamAns *natstream.NatStream +} + +func NewVydra(client *polygon.Polygon, pID int) *Vydra { + return &Vydra{ + client: client, + pID: pID, + streamIn: new(natstream.NatStream), + streamOut: new(natstream.NatStream), + streamAns: new(natstream.NatStream), + } +} + +// Convert string from .xml to API. +func convertString(s string) string { + return strings.ToUpper(strings.ReplaceAll(s, "-", "_")) // oh my God +} + +func (v *Vydra) readXML(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + if err := xml.Unmarshal(data, &v.prob); err != nil { + return err + } + logrus.WithFields(logrus.Fields{ + "revision": v.prob.Revision, "short-name": v.prob.ShortName, + }).Info("load problem.xml") + + return nil +} + +func (v *Vydra) Upload(errs chan error) error { + defer close(errs) + if err := v.readXML("problem.xml"); err != nil { + return err + } + v.batchInitial(errs) + v.batchValChk(errs) + v.batchJudging(errs) + + return nil +} diff --git a/polygon/vydra/vydrainit.go b/polygon/vydra/vydrainit.go new file mode 100644 index 0000000..2a49474 --- /dev/null +++ b/polygon/vydra/vydrainit.go @@ -0,0 +1,183 @@ +package vydra + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/Gornak40/algolymp/polygon" + "github.com/sirupsen/logrus" +) + +func (v *Vydra) initProblem(judge *Judging) error { + input := defaultInput + if judge.InputFile != "" { + input = judge.InputFile + } + output := defaultOutput + if judge.OutputFile != "" { + output = judge.OutputFile + } + tl := defaultTL + ml := defaultML + if len(judge.TestSets) != 0 { + tl = judge.TestSets[0].TimeLimit + ml = judge.TestSets[0].MemoryLimit / megabyte + } + logrus.WithFields(logrus.Fields{ + "input": input, "output": output, + "tl": tl, "ml": ml, + }).Info("init problem") + + pr := polygon.NewProblemRequest(v.pID). + InputFile(input).OutputFile(output). + TimeLimit(tl).MemoryLimit(ml) + + return v.client.UpdateInfo(pr) +} + +func (v *Vydra) uploadExecutable(exe *Executable) error { + logrus.WithFields(logrus.Fields{ + "path": exe.Source.Path, "type": exe.Source.Type, + }).Info("upload executable") + data, err := os.ReadFile(exe.Source.Path) + if err != nil { + return err + } + + fr := polygon.NewFileRequest(v.pID, polygon.TypeSource, filepath.Base(exe.Source.Path), string(data)). + SourceType(exe.Source.Type) + + return v.client.SaveFile(fr) +} + +func (v *Vydra) uploadSolution(sol *Solution) error { + logrus.WithFields(logrus.Fields{ + "path": sol.Source.Path, "type": sol.Source.Type, "tag": sol.Tag, + }).Info("upload solution") + data, err := os.ReadFile(sol.Source.Path) + if err != nil { + return err + } + + var tag polygon.SolutionTag + switch sol.Tag { // TODO: add other tags + case "main": + tag = polygon.TagMain + case "accepted": + tag = polygon.TagCorrect + case "rejected": + tag = polygon.TagIncorrect + case "time-limit-exceeded": + tag = polygon.TagTimeLimit + case "wrong-answer": + tag = polygon.TagWrongAnswer + case "time-limit-exceeded-or-accepted": + tag = polygon.TagTLorOK + case "presentation-error": + tag = polygon.TagPresentationError + case "memory-limit-exceeded": + tag = polygon.TagMemoryLimit + default: + return fmt.Errorf("%w: %s", ErrBadSolutionTag, sol.Tag) + } + + sr := polygon.NewSolutionRequest(v.pID, filepath.Base(sol.Source.Path), string(data), tag). + SourceType(sol.Source.Type) + + return v.client.SaveSolution(sr) +} + +func (v *Vydra) uploadResource(res *File) error { + logrus.WithFields(logrus.Fields{ + "path": res.Path, "type": res.Type, + }).Info("upload resource") + data, err := os.ReadFile(res.Path) + if err != nil { + return err + } + + fr := polygon.NewFileRequest(v.pID, polygon.TypeResource, filepath.Base(res.Path), string(data)) + + return v.client.SaveFile(fr) +} + +func (v *Vydra) uploadStatement(stat *Statement) error { + if stat.Type != "application/x-tex" { + return nil + } + logrus.WithFields(logrus.Fields{ + "language": stat.Language, "type": stat.Type, "charset": stat.Charset, + }).Info("upload statement") + dir := "statement-sections/" + stat.Language + + return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + sr := polygon.NewStatementRequest(v.pID, stat.Language). + Encoding(stat.Charset) + switch filepath.Base(path) { + case "input.tex": + sr.Input(string(data)) + case "output.tex": + sr.Output(string(data)) + case "legend.tex": + sr.Legend(string(data)) + case "name.tex": + sr.Name(string(data)) + case "notes.tex": + sr.Notes(string(data)) + case "tutorial.tex": + sr.Tutorial(string(data)) + case "interaction.tex": + sr.Interaction(string(data)) + case "scoring.tex": + sr.Scoring(string(data)) + default: + return nil + } + logrus.WithField("path", path).Info("upload statement section") + + return v.client.SaveStatement(sr) + }) +} + +func (v *Vydra) uploadTags(tags []Tag) error { + stags := make([]string, 0, len(tags)) + for _, t := range tags { + stags = append(stags, t.Value) + } + line := strings.Join(stags, ",") + logrus.WithField("tags", line).Info("upload tags") + + return v.client.SaveTags(v.pID, line) +} + +func (v *Vydra) batchInitial(errs chan error) { + errs <- v.initProblem(&v.prob.Judging) + if tags := v.prob.Tags.Tags; len(tags) != 0 { + errs <- v.uploadTags(tags) + } + for _, sol := range v.prob.Assets.Solutions.Solutions { + errs <- v.uploadSolution(&sol) + } + for _, res := range v.prob.Files.Resources.Files { + errs <- v.uploadResource(&res) + } + for _, exe := range v.prob.Files.Executables.Executables { + errs <- v.uploadExecutable(&exe) + } + for _, stat := range v.prob.Statements.Statements { + errs <- v.uploadStatement(&stat) + } +} diff --git a/polygon/vydra/vydrajudging.go b/polygon/vydra/vydrajudging.go new file mode 100644 index 0000000..1186791 --- /dev/null +++ b/polygon/vydra/vydrajudging.go @@ -0,0 +1,137 @@ +package vydra + +import ( + "fmt" + "path" + "strings" + + "github.com/Gornak40/algolymp/polygon" + "github.com/sirupsen/logrus" +) + +func (v *Vydra) uploadScript(testset *TestSet) error { + logrus.WithField("testset", testset.Name).Info("upload script") + gens := make([]string, 0, testset.TestCount) + for idx, test := range testset.Tests.Tests { // build script + if test.Method == "generated" { + line := fmt.Sprintf("%s > %d", test.Cmd, idx+1) + if test.FromFile != "" { // TODO: find better solution for `gen > {3-100}` + line = fmt.Sprintf("%s > {%d}", test.Cmd, idx+1) + } + gens = append(gens, line) + } + } + script := strings.Join(gens, "\n") + if script == "" { + return nil + } + + return v.client.SaveScript(v.pID, testset.Name, script) +} + +func (v *Vydra) uploadTest(testset string, idx int, test *Test) error { + // It's kind of experimental solution. + if (*test == Test{Cmd: test.Cmd, FromFile: test.FromFile, Method: "generated"}) { + return nil + } + logrus.WithFields(logrus.Fields{ + "testset": testset, "idx": idx, + "method": test.Method, "sample": test.Sample, + }).Info("upload test") + + tr := polygon.NewTestRequest(v.pID, idx). + TestSet(testset). + Description(test.Description). + UseInStatements(test.Sample) + if test.Group != "" { + tr.Group(test.Group) + } + if test.Points != 0 { + tr.Points(test.Points) + } + if test.Method == "manual" { + input, err := v.streamIn.Next() + if err != nil { + return err + } + tr.Input(input) + } + + return v.client.SaveTest(tr) +} + +func (v *Vydra) initGroups() error { + logrus.Info("init test groups") + + return v.client.EnableGroups(v.pID) +} + +func (v *Vydra) initPoints() error { + logrus.Info("init test points") + + return v.client.EnablePoints(v.pID) +} + +func (v *Vydra) uploadGroup(testset string, group *Group) error { + deps := make([]string, 0, len(group.Dependencies.Dependencies)) + for _, d := range group.Dependencies.Dependencies { + deps = append(deps, d.Group) + } + logrus.WithFields(logrus.Fields{ + "feedback": group.FeedbackPolicy, + "points": group.PointsPolicy, + "dependencies": deps, + }).Info("upload group") + + tgr := polygon.NewTestGroupRequest(v.pID, testset, group.Name). + FeedbackPolicy(convertString(group.FeedbackPolicy)). + PointsPolicy(convertString(group.PointsPolicy)). + Dependencies(deps) + + return v.client.SaveTestGroup(tgr) +} + +type testsMetaInfo struct { + enablePoints bool + enableGroups bool +} + +func getTestsMeta(tests []Test) testsMetaInfo { + var ans testsMetaInfo + for _, t := range tests { + if t.Group != "" { + ans.enableGroups = true + } + if t.Points != 0 { + ans.enablePoints = true + } + } + + return ans +} + +func (v *Vydra) batchJudging(errs chan error) { + for _, testset := range v.prob.Judging.TestSets { + errs <- v.uploadScript(&testset) + if err := v.streamIn.Init(path.Join(testset.Name, "*[^.a]")); err != nil { + errs <- err + + continue + } + meta := getTestsMeta(testset.Tests.Tests) + if meta.enableGroups { + errs <- v.initGroups() + } + if meta.enablePoints { + errs <- v.initPoints() + } + for idx, test := range testset.Tests.Tests { + errs <- v.uploadTest(testset.Name, idx+1, &test) + } + if grp := testset.Groups.Groups; len(grp) != 0 { + for _, g := range grp { + errs <- v.uploadGroup(testset.Name, &g) + } + } + } +} diff --git a/polygon/vydra/vydravalchk.go b/polygon/vydra/vydravalchk.go new file mode 100644 index 0000000..701cc01 --- /dev/null +++ b/polygon/vydra/vydravalchk.go @@ -0,0 +1,99 @@ +package vydra + +import ( + "path/filepath" + + "github.com/Gornak40/algolymp/polygon" + "github.com/sirupsen/logrus" +) + +func (v *Vydra) initValidator(val *Validator) error { + logrus.WithFields(logrus.Fields{ + "path": val.Source.Path, "type": val.Source.Type, + }).Info("init validator") + + return v.client.SetValidator(v.pID, filepath.Base(val.Source.Path)) +} + +func (v *Vydra) initChecker(chk *Checker) error { + path := chk.Name + if path == "" { + path = filepath.Base(chk.Source.Path) + } + logrus.WithFields(logrus.Fields{ + "path": path, "type": chk.Type, + }).Info("init checker") + + return v.client.SetChecker(v.pID, path) +} + +func (v *Vydra) uploadValidatorTest(idx int, test *Test) error { + logrus.WithFields(logrus.Fields{"idx": idx}).Info("upload validator test") + input, err := v.streamIn.Next() + if err != nil { + return err + } + + vtr := polygon.NewValidatorTestRequest(v.pID, idx). + Input(input).Verdict(convertString(test.Verdict)) + + return v.client.SaveValidatorTest(vtr) +} + +func (v *Vydra) uploadCheckerTest(idx int, test *Test) error { + logrus.WithFields(logrus.Fields{"idx": idx}).Info("upload checker test") + input, err := v.streamIn.Next() + if err != nil { + return err + } + output, err := v.streamOut.Next() + if err != nil { + return err + } + answer, err := v.streamAns.Next() + if err != nil { + return err + } + + ctr := polygon.NewCheckerTestRequest(v.pID, idx). + Input(input).Output(output).Answer(answer). + Verdict(convertString(test.Verdict)) + + return v.client.SaveCheckerTest(ctr) +} + +func (v *Vydra) batchValChk(errs chan error) { + if val := v.prob.Assets.Validators.Validator; val != nil { + errs <- v.initValidator(val) + if err := v.streamIn.Init("files/tests/validator-tests/*"); err != nil { + errs <- err + + goto checker + } + for idx, test := range val.TestSet.Tests.Tests { + errs <- v.uploadValidatorTest(idx+1, &test) + } + } +checker: + if chk := v.prob.Assets.Checker; chk != nil { + errs <- v.initChecker(chk) + if err := v.streamIn.Init(filepath.Join(chkTests, "*[^.ao]")); err != nil { + errs <- err + + return + } + if err := v.streamOut.Init(filepath.Join(chkTests, "*.o")); err != nil { + errs <- err + + return + } + if err := v.streamAns.Init(filepath.Join(chkTests, "*.a")); err != nil { + errs <- err + + return + } + for idx, test := range chk.TestSet.Tests.Tests { + errs <- v.uploadCheckerTest(idx+1, &test) + } + } +} diff --git a/polygon/wooda/wooda.go b/polygon/wooda/wooda.go index e0adde4..8c0ebf2 100644 --- a/polygon/wooda/wooda.go +++ b/polygon/wooda/wooda.go @@ -160,5 +160,7 @@ func (w *Wooda) resolveInteractor(path, data string) error { } func (w *Wooda) resolveSolution(path, data string, tag polygon.SolutionTag) error { - return w.client.SaveSolution(w.pID, filepath.Base(path), data, tag) + sr := polygon.NewSolutionRequest(w.pID, filepath.Base(path), data, tag) + + return w.client.SaveSolution(sr) }