diff --git a/common/db/models/models_test.go b/common/db/models/models_test.go index 92c6ad4..054dbb7 100644 --- a/common/db/models/models_test.go +++ b/common/db/models/models_test.go @@ -15,8 +15,8 @@ import ( func fixtureDb(t *testing.T) *gorm.DB { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - err := db.AutoMigrate(&Submission{}) - assert.NoError(t, err) + assert.NoError(t, db.AutoMigrate(&Submission{})) + assert.NoError(t, db.AutoMigrate(&Problem{})) return db } diff --git a/common/db/models/problem.go b/common/db/models/problem.go index 79031e8..2d1d378 100644 --- a/common/db/models/problem.go +++ b/common/db/models/problem.go @@ -1,17 +1,86 @@ package models import ( + "database/sql/driver" + "encoding/json" + "errors" "gorm.io/gorm" + "gorm.io/gorm/schema" "testing_system/lib/customfields" ) type ProblemType int const ( - ProblemType_ICPC ProblemType = iota + 1 - ProblemType_IOI + ProblemTypeICPC ProblemType = iota + 1 + ProblemTypeIOI ) +// TestGroupScoringType sets how should scheduler set points for a group +type TestGroupScoringType int + +const ( + // TestGroupScoringTypeComplete means that group costs TestGroup.GroupScore (all the tests should be OK) + TestGroupScoringTypeComplete TestGroupScoringType = iota + 1 + // TestGroupScoringTypeEachTest means that group score = TestGroup.TestScore * (number of tests with OK) + TestGroupScoringTypeEachTest + // TestGroupScoringTypeMin means that group score = min(checker's scores among all the tests) + TestGroupScoringTypeMin +) + +// TestGroupFeedbackType sets which info about tests in a group would be shown +type TestGroupFeedbackType int + +const ( + // TestGroupFeedbackTypeNone won't show anything + TestGroupFeedbackTypeNone TestGroupFeedbackType = iota + 1 + // TestGroupFeedbackTypePoints will show points only + TestGroupFeedbackTypePoints + // TestGroupFeedbackTypeICPC will show verdict, time and memory usage for the first test with no OK + TestGroupFeedbackTypeICPC + // TestGroupFeedbackTypeComplete same as TestGroupFeedbackTypeICPC, but for every test + TestGroupFeedbackTypeComplete + // TestGroupFeedbackTypeFull same as TestGroupFeedbackTypeComplete, but with input, output, stderr, etc. + TestGroupFeedbackTypeFull +) + +type TestGroup struct { + Name string `json:"name" yaml:"name"` + FirstTest uint64 `json:"FirstTest" yaml:"FirstTest"` + LastTest uint64 `json:"LastTest" yaml:"LastTest"` + // TestScore meaningful only in case of TestGroupScoringTypeEachTest + TestScore *float64 `json:"TestScore" yaml:"TestScore"` + // GroupScore meaningful only in case of TestGroupScoringTypeComplete + GroupScore *float64 `json:"GroupScore" yaml:"GroupScore"` + ScoringType TestGroupScoringType `json:"ScoringType" yaml:"ScoringType"` + FeedbackType TestGroupFeedbackType `json:"FeedbackType" yaml:"FeedbackType"` + RequiredGroupNames []string `json:"RequiredGroupNames" yaml:"RequiredGroupNames"` +} + +type TestGroups []TestGroup + +func (t TestGroups) Value() (driver.Value, error) { + return json.Marshal(t) +} + +func (t *TestGroups) Scan(value interface{}) error { + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed while scanning TestGroups") + } + return json.Unmarshal(bytes, t) +} + +func (t TestGroups) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch db.Dialector.Name() { + case "mysql", "sqlite": + return "JSON" + case "postgres": + return "JSONB" + } + return "" +} + type Problem struct { gorm.Model @@ -21,17 +90,19 @@ type Problem struct { MemoryLimit customfields.Memory `yaml:"MemoryLimit"` TestsNumber uint64 `yaml:"TestsNumber"` + // TestGroups ignored for ICPC problems + TestGroups TestGroups `yaml:"TestGroups"` // WallTimeLimit specifies maximum execution and wait time. // By default, it is max(5s, TimeLimit * 2) WallTimeLimit *customfields.Time `yaml:"WallTimeLimit,omitempty"` - // MaxOpenFiles specifies maximum number of files, opened by testing system. + // MaxOpenFiles specifies the maximum number of files, opened by testing system. // By default, it is 64 MaxOpenFiles *uint64 `yaml:"MaxOpenFiles,omitempty"` - // MaxThreads specifies maximum number of threads and/or processes - // By default, it is single thread + // MaxThreads specifies the maximum number of threads and/or processes; + // By default, it is a single thread // If MaxThreads equals to -1, any number of threads allowed MaxThreads *int64 `yaml:"MaxThreads,omitempty"` diff --git a/common/db/models/submission.go b/common/db/models/submission.go index c0182b2..cce1a34 100644 --- a/common/db/models/submission.go +++ b/common/db/models/submission.go @@ -16,6 +16,7 @@ type TestResult struct { Verdict verdict.Verdict `json:"Verdict" yaml:"Verdict"` Time customfields.Time `json:"Time" yaml:"Time"` Memory customfields.Memory `json:"Memory" yaml:"Memory"` + Error string `json:"Error,omitempty" yaml:"Error,omitempty"` } type TestResults []TestResult @@ -27,7 +28,7 @@ func (t TestResults) Value() (driver.Value, error) { func (t *TestResults) Scan(value interface{}) error { bytes, ok := value.([]byte) if !ok { - return errors.New("type assertion to []byte failed") + return errors.New("type assertion to []byte failed while scanning TestResults") } return json.Unmarshal(bytes, t) } @@ -42,12 +43,44 @@ func (t TestResults) GormDBDataType(db *gorm.DB, field *schema.Field) string { return "" } +type GroupResult struct { + GroupName string `json:"GroupName" yaml:"GroupName"` + Points float64 `json:"Points" yaml:"Points"` + Passed bool `json:"Passed" yaml:"Passed"` + // TODO maybe more fields +} + +type GroupResults []GroupResult + +func (t GroupResults) Value() (driver.Value, error) { + return json.Marshal(t) +} + +func (t *GroupResults) Scan(value interface{}) error { + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed while scanning GroupResults") + } + return json.Unmarshal(bytes, t) +} + +func (t GroupResults) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch db.Dialector.Name() { + case "mysql", "sqlite": + return "JSON" + case "postgres": + return "JSONB" + } + return "" +} + type Submission struct { gorm.Model ProblemID uint64 `json:"ProblemID" yaml:"ProblemID"` Language string `json:"Language" yaml:"Language"` - Score float64 `json:"Score" yaml:"Score"` - Verdict verdict.Verdict `json:"Verdict" yaml:"Verdict"` - TestResults TestResults `gorm:"type:jsonb" json:"TestResults" yaml:"TestResults"` + Score float64 `json:"Score" yaml:"Score"` + Verdict verdict.Verdict `json:"Verdict" yaml:"Verdict"` + TestResults TestResults `json:"TestResults" yaml:"TestResults"` + GroupResults GroupResults `json:"GroupResults" yaml:"GroupResults"` } diff --git a/go.mod b/go.mod index 9e5e4c3..e8e6404 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.4 github.com/xorcare/pointer v1.2.2 golang.org/x/net v0.39.0 gopkg.in/yaml.v3 v3.0.1 @@ -53,6 +52,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/swaggo/swag v1.16.4 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.16.0 // indirect diff --git a/master/queue/jobgenerators/common.go b/master/queue/jobgenerators/common.go new file mode 100644 index 0000000..ba7ecbf --- /dev/null +++ b/master/queue/jobgenerators/common.go @@ -0,0 +1,10 @@ +package jobgenerators + +// used in IOI and ICPC generators +type generatorState int + +const ( + compilationNotStarted generatorState = iota + compilationStarted + compilationFinished +) diff --git a/master/queue/jobgenerators/generator.go b/master/queue/jobgenerators/generator.go index f2e6514..dbcda13 100644 --- a/master/queue/jobgenerators/generator.go +++ b/master/queue/jobgenerators/generator.go @@ -21,8 +21,10 @@ type Generator interface { func NewGenerator(problem *models.Problem, submission *models.Submission) (Generator, error) { switch problem.ProblemType { - case models.ProblemType_ICPC: + case models.ProblemTypeICPC: return newICPCGenerator(problem, submission) + case models.ProblemTypeIOI: + return NewIOIGenerator(problem, submission) default: return nil, fmt.Errorf("unknown problem type %v", problem.ProblemType) } diff --git a/master/queue/jobgenerators/generator_test.go b/master/queue/jobgenerators/generator_test.go new file mode 100644 index 0000000..7646a41 --- /dev/null +++ b/master/queue/jobgenerators/generator_test.go @@ -0,0 +1,1095 @@ +package jobgenerators + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + "testing" + "testing_system/common/connectors/invokerconn" + "testing_system/common/connectors/masterconn" + "testing_system/common/constants/verdict" + "testing_system/common/db/models" +) + +func fixtureSubmission(ID uint) *models.Submission { + submission := &models.Submission{} + submission.ID = ID + return submission +} + +func nextJob(t *testing.T, g Generator, SubmitID uint, jobType invokerconn.JobType, test uint64) *invokerconn.Job { + job := g.NextJob() + require.NotNil(t, job) + require.Equal(t, jobType, job.Type) + require.Equal(t, SubmitID, job.SubmitID) + require.Equal(t, test, job.Test) + return job +} + +func noJobs(t *testing.T, g Generator) { + job := g.NextJob() + require.Nil(t, job) +} + +func TestICPCGenerator(t *testing.T) { + const fixtureICPCProblemTestsNumber = 10 + fixtureICPCProblem := func() *models.Problem { + return &models.Problem{ + ProblemType: models.ProblemTypeICPC, + TestsNumber: fixtureICPCProblemTestsNumber, + } + } + + t.Run("Fail compilation", func(t *testing.T) { + problem, submission := fixtureICPCProblem(), fixtureSubmission(1) + g, err := NewGenerator(problem, submission) + require.Nil(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CE, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.CE, sub.Verdict) + require.Equal(t, 0., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.SK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + }) + + t.Run("Straight tasks finishing", func(t *testing.T) { + problem := fixtureICPCProblem() + submission := fixtureSubmission(1) + generator, err := NewGenerator(problem, submission) + require.Nil(t, err) + job := nextJob(t, generator, 1, invokerconn.CompileJob, 0) + noJobs(t, generator) + sub, err := generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.Nil(t, err) + for i := range fixtureICPCProblemTestsNumber - 1 { + job = nextJob(t, generator, 1, invokerconn.TestJob, uint64(i)+1) + sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + } + job = nextJob(t, generator, 1, invokerconn.TestJob, fixtureICPCProblemTestsNumber) + sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.OK, sub.Verdict) + require.Equal(t, 1., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.OK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + }) + + t.Run("Tasks finishing", func(t *testing.T) { + prepare := func() (Generator, []string) { + problem := fixtureICPCProblem() + submission := fixtureSubmission(1) + g, err := NewGenerator(problem, submission) + require.Nil(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.Nil(t, err) + firstTwoJobIDs := make([]string, 0) + for i := range 2 { + job = nextJob(t, g, 1, invokerconn.TestJob, uint64(i)+1) + firstTwoJobIDs = append(firstTwoJobIDs, job.ID) + } + return g, firstTwoJobIDs + } + finishOtherTests := func(g Generator) { + for i := 2; i < fixtureICPCProblemTestsNumber-1; i++ { + job := nextJob(t, g, 1, invokerconn.TestJob, uint64(i)+1) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + } + job := nextJob(t, g, 1, invokerconn.TestJob, 10) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.OK, sub.Verdict) + require.Equal(t, 1., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.OK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + } + + t.Run("right order", func(t *testing.T) { + g, firstTwoJobIDs := prepare() + for _, id := range firstTwoJobIDs { + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: id, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + } + finishOtherTests(g) + }) + + t.Run("wrong order + both ok", func(t *testing.T) { + g, firstTwoJobIDs := prepare() + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[1], + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[0], + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + + finishOtherTests(g) + }) + + t.Run("wrong order + 2nd fail", func(t *testing.T) { + g, firstTwoJobIDs := prepare() + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[1], + Verdict: verdict.WA, + }) + require.Nil(t, sub) + require.Nil(t, err) + + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[0], + Verdict: verdict.OK, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.WA, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) + for i, result := range sub.TestResults[2:] { + require.Equal(t, verdict.SK, result.Verdict) + require.Equal(t, uint64(i)+3, result.TestNumber) + } + }) + + t.Run("wrong order + 1st fail", func(t *testing.T) { + g, firstTwoJobIDs := prepare() + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[1], + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: firstTwoJobIDs[0], + Verdict: verdict.WA, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.WA, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) + for i, result := range sub.TestResults[2:] { + require.Equal(t, verdict.SK, result.Verdict) + require.Equal(t, uint64(i)+3, result.TestNumber) + } + }) + }) + + t.Run("Finish same job twice", func(t *testing.T) { + problem, submission := fixtureICPCProblem(), fixtureSubmission(1) + g, err := NewGenerator(problem, submission) + require.Nil(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CE, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.CE, sub.Verdict) + require.Equal(t, 0., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.SK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CE, + }) + require.Nil(t, sub) + require.NotNil(t, err) + }) +} + +func TestIOIGenerator(t *testing.T) { + t.Run("Bad problem configuration", func(t *testing.T) { + badProblems := []models.Problem{ + // type EachTest, but TestScore is nil + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 1, + TestScore: nil, + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 1, + }, + // type Complete, but GroupScore is nil + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 1, + GroupScore: nil, + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 1, + }, + // type Min, but GroupScore is nil + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 1, + GroupScore: nil, + ScoringType: models.TestGroupScoringTypeMin, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 1, + }, + // cyclic groups + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name1", + FirstTest: 1, + LastTest: 1, + GroupScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeMin, + RequiredGroupNames: []string{"name2"}, + }, + { + Name: "name2", + FirstTest: 2, + LastTest: 2, + GroupScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeMin, + RequiredGroupNames: []string{"name1"}, + }, + }, + }, + // test is not covered + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 1, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 2, + }, + // test in several groups + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 2, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + { + Name: "name1", + FirstTest: 2, + LastTest: 3, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 3, + }, + // the same group name + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name", + FirstTest: 1, + LastTest: 2, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + { + Name: "name", + FirstTest: 3, + LastTest: 3, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + TestsNumber: 3, + }, + // first test > last test + { + ProblemType: models.ProblemTypeIOI, + TestGroups: []models.TestGroup{ + { + Name: "name1", + FirstTest: 1, + LastTest: 2, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + { + Name: "name2", + FirstTest: 2, + LastTest: 1, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + { + Name: "name3", + FirstTest: 3, + LastTest: 3, + TestScore: pointer.Float64(1.0), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + }, + } + for _, problem := range badProblems { + _, err := NewIOIGenerator(&problem, &models.Submission{}) + require.Error(t, err) + } + }) + + problemWithOneGroup := models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 10, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 10, + TestScore: nil, + GroupScore: pointer.Float64(100), + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: make([]string, 0), + }, + }, + } + + t.Run("Fail compilation", func(t *testing.T) { + submission := fixtureSubmission(1) + wasProblem := problemWithOneGroup + g, err := NewGenerator(&problemWithOneGroup, submission) + require.Nil(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CE, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.CE, sub.Verdict) + require.Equal(t, 0., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.SK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + require.Equal(t, wasProblem, problemWithOneGroup) + require.Equal(t, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + }, sub.GroupResults) + }) + + t.Run("Straight task finishing", func(t *testing.T) { + wasProblem := problemWithOneGroup + submission := fixtureSubmission(1) + generator, err := NewGenerator(&problemWithOneGroup, submission) + require.Nil(t, err) + job := nextJob(t, generator, 1, invokerconn.CompileJob, 0) + noJobs(t, generator) + sub, err := generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.Nil(t, err) + for i := range 9 { + job = nextJob(t, generator, 1, invokerconn.TestJob, uint64(i)+1) + sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.Nil(t, err) + } + job = nextJob(t, generator, 1, invokerconn.TestJob, 10) + sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + require.NotNil(t, sub) + require.Nil(t, err) + + require.Equal(t, verdict.OK, sub.Verdict) + require.Equal(t, 100., sub.Score) + for i, result := range sub.TestResults { + require.Equal(t, verdict.OK, result.Verdict) + require.Equal(t, uint64(i)+1, result.TestNumber) + } + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 100., + Passed: true, + }, + }) + + require.Equal(t, wasProblem, problemWithOneGroup) + }) + + t.Run("Fails in TestGroupScoringTypeComplete", func(t *testing.T) { + prepare := func() Generator { + problem := models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 2, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 1, + TestScore: nil, + GroupScore: pointer.Float64(50), + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: make([]string, 0), + }, + { + Name: "group2", + FirstTest: 2, + LastTest: 2, + TestScore: nil, + GroupScore: pointer.Float64(50), + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: []string{"group1"}, + }, + }, + } + gen, err := NewGenerator(&problem, &models.Submission{}) + require.NoError(t, err) + job := nextJob(t, gen, 0, invokerconn.CompileJob, 0) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.NoError(t, err) + return gen + } + + t.Run("WA 1st, OK 2nd", func(t *testing.T) { + gen := prepare() + job1 := nextJob(t, gen, 0, invokerconn.TestJob, 1) + job2 := nextJob(t, gen, 0, invokerconn.TestJob, 2) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job1.ID, + Verdict: verdict.WA, + }) + require.Nil(t, sub) + require.NoError(t, err) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.OK, + }) + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + { + GroupName: "group2", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("OK 2nd, WA 1st", func(t *testing.T) { + gen := prepare() + job1 := nextJob(t, gen, 0, invokerconn.TestJob, 1) + job2 := nextJob(t, gen, 0, invokerconn.TestJob, 2) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.NoError(t, err) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job1.ID, + Verdict: verdict.WA, + }) + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + { + GroupName: "group2", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("OK 1st, WA 2nd", func(t *testing.T) { + gen := prepare() + job1 := nextJob(t, gen, 0, invokerconn.TestJob, 1) + job2 := nextJob(t, gen, 0, invokerconn.TestJob, 2) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job1.ID, + Verdict: verdict.OK, + }) + require.Nil(t, sub) + require.NoError(t, err) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.WA, + }) + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 50., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 50., + Passed: true, + }, + { + GroupName: "group2", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("WA 2nd, OK 1st", func(t *testing.T) { + gen := prepare() + job1 := nextJob(t, gen, 0, invokerconn.TestJob, 1) + job2 := nextJob(t, gen, 0, invokerconn.TestJob, 2) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.WA, + }) + require.Nil(t, sub) + require.NoError(t, err) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job1.ID, + Verdict: verdict.OK, + }) + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 50., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 50., + Passed: true, + }, + { + GroupName: "group2", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("WA 1st, then take 2nd", func(t *testing.T) { + gen := prepare() + job := nextJob(t, gen, 0, invokerconn.TestJob, 1) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.WA, + }) + require.NotNil(t, sub) + require.NoError(t, err) + assert.Nil(t, gen.NextJob()) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + { + GroupName: "group2", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("Fail in group with >1 tests", func(t *testing.T) { + problem := models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 2, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 2, + TestScore: nil, + GroupScore: pointer.Float64(100), + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: make([]string, 0), + }, + }, + } + gen, err := NewGenerator(&problem, &models.Submission{}) + require.NoError(t, err) + job := nextJob(t, gen, 0, invokerconn.CompileJob, 0) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.NoError(t, err) + job = nextJob(t, gen, 0, invokerconn.TestJob, 1) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.WA, + }) + require.Nil(t, gen.NextJob()) + require.NotNil(t, sub) + require.NoError(t, err) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("OK, run, FAIL, get", func(t *testing.T) { + baseStat := &masterconn.JobResultStatistics{ + Time: 100, + Memory: 100, + WallTime: 100, + ExitCode: 0, + } + problem := models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 4, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 4, + TestScore: nil, + GroupScore: pointer.Float64(100), + ScoringType: models.TestGroupScoringTypeComplete, + RequiredGroupNames: make([]string, 0), + }, + }, + } + gen, err := NewGenerator(&problem, fixtureSubmission(1)) + require.NoError(t, err) + job := nextJob(t, gen, 1, invokerconn.CompileJob, 0) + sub, err := gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.Nil(t, sub) + require.NoError(t, err) + job1 := nextJob(t, gen, 1, invokerconn.TestJob, 1) + job2 := nextJob(t, gen, 1, invokerconn.TestJob, 2) + job3 := nextJob(t, gen, 1, invokerconn.TestJob, 3) + // now finish 1 and 3 + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job1.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.Nil(t, sub) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job3.ID, + Verdict: verdict.WA, + Statistics: baseStat, + }) + // this group is already failed, so the generator should not return any job + require.Nil(t, gen.NextJob()) + sub, err = gen.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.TL, + Statistics: baseStat, + }) + require.NoError(t, err) + require.Nil(t, gen.NextJob()) + require.NotNil(t, sub) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + }) + require.Equal(t, models.TestResult{ + TestNumber: 1, + Points: nil, + Verdict: verdict.OK, + }, sub.TestResults[0]) + require.Equal(t, models.TestResult{ + TestNumber: 2, + Points: nil, + Verdict: verdict.TL, + Time: 100, + Memory: 100, + }, sub.TestResults[1]) + require.Equal(t, models.TestResult{ + TestNumber: 3, + Points: nil, + Verdict: verdict.SK, + }, sub.TestResults[2]) + require.Equal(t, models.TestResult{ + TestNumber: 4, + Points: nil, + Verdict: verdict.SK, + }, sub.TestResults[3]) + }) + }) + + t.Run("Fails in TestGroupScoringTypeEachTest", func(t *testing.T) { + t.Run("WA in the middle of the group", func(t *testing.T) { + problem := models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 3, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 3, + TestScore: pointer.Float64(20), + ScoringType: models.TestGroupScoringTypeEachTest, + RequiredGroupNames: make([]string, 0), + }, + }, + } + g, err := NewIOIGenerator(&problem, fixtureSubmission(1)) + require.NoError(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + require.NotNil(t, job) + require.Nil(t, g.NextJob()) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.NoError(t, err) + require.Nil(t, sub) + // test + job = nextJob(t, g, 1, invokerconn.TestJob, 1) + require.NotNil(t, job) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + job2 := nextJob(t, g, 1, invokerconn.TestJob, 2) + require.NotNil(t, job2) + job3 := nextJob(t, g, 1, invokerconn.TestJob, 3) + require.NotNil(t, job3) + require.Nil(t, g.NextJob()) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.WA, + }) + require.NoError(t, err) + require.Nil(t, sub) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job3.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.NotNil(t, sub) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 40., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) + require.Equal(t, verdict.OK, sub.TestResults[2].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 40., + Passed: false, + }, + }) + }) + }) + + t.Run("TestGroupScoringTypeMin", func(t *testing.T) { + prepare := func() models.Problem { + return models.Problem{ + ProblemType: models.ProblemTypeIOI, + TestsNumber: 3, + TestGroups: []models.TestGroup{ + { + Name: "group1", + FirstTest: 1, + LastTest: 3, + GroupScore: pointer.Float64(20), + ScoringType: models.TestGroupScoringTypeMin, + RequiredGroupNames: make([]string, 0), + }, + }, + } + } + + t.Run("WA in the middle of the group", func(t *testing.T) { + problem := prepare() + g, err := NewIOIGenerator(&problem, fixtureSubmission(1)) + require.NoError(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + require.NotNil(t, job) + require.Nil(t, g.NextJob()) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.NoError(t, err) + require.Nil(t, sub) + // test + job = nextJob(t, g, 1, invokerconn.TestJob, 1) + require.NotNil(t, job) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + job2 := nextJob(t, g, 1, invokerconn.TestJob, 2) + require.NotNil(t, job2) + job3 := nextJob(t, g, 1, invokerconn.TestJob, 3) + require.NotNil(t, job3) + require.Nil(t, g.NextJob()) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.WA, + }) + require.NoError(t, err) + require.Nil(t, sub) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job3.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.NotNil(t, sub) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 0., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) + require.Equal(t, verdict.SK, sub.TestResults[2].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 0., + Passed: false, + }, + }) + }) + + t.Run("PT in the middle of the group", func(t *testing.T) { + problem := prepare() + g, err := NewIOIGenerator(&problem, fixtureSubmission(1)) + require.NoError(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + require.NotNil(t, job) + require.Nil(t, g.NextJob()) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.NoError(t, err) + require.Nil(t, sub) + // test + job = nextJob(t, g, 1, invokerconn.TestJob, 1) + require.NotNil(t, job) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + job2 := nextJob(t, g, 1, invokerconn.TestJob, 2) + require.NotNil(t, job2) + job3 := nextJob(t, g, 1, invokerconn.TestJob, 3) + require.NotNil(t, job3) + require.Nil(t, g.NextJob()) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.PT, + Points: pointer.Float64(10), + }) + require.NoError(t, err) + require.Nil(t, sub) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job3.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.NotNil(t, sub) + + require.Equal(t, verdict.PT, sub.Verdict) + require.Equal(t, 10., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.PT, sub.TestResults[1].Verdict) + require.Equal(t, verdict.OK, sub.TestResults[2].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 10., + Passed: false, + }, + }) + }) + + t.Run("no fails", func(t *testing.T) { + problem := prepare() + g, err := NewIOIGenerator(&problem, fixtureSubmission(1)) + require.NoError(t, err) + job := nextJob(t, g, 1, invokerconn.CompileJob, 0) + require.NotNil(t, job) + require.Nil(t, g.NextJob()) + sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.CD, + }) + require.NoError(t, err) + require.Nil(t, sub) + // test + job = nextJob(t, g, 1, invokerconn.TestJob, 1) + require.NotNil(t, job) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job.ID, + Verdict: verdict.OK, + }) + job2 := nextJob(t, g, 1, invokerconn.TestJob, 2) + require.NotNil(t, job2) + job3 := nextJob(t, g, 1, invokerconn.TestJob, 3) + require.NotNil(t, job3) + require.Nil(t, g.NextJob()) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job2.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.Nil(t, sub) + sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ + JobID: job3.ID, + Verdict: verdict.OK, + }) + require.NoError(t, err) + require.NotNil(t, sub) + + require.Equal(t, verdict.OK, sub.Verdict) + require.Equal(t, 20., sub.Score) + require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) + require.Equal(t, verdict.OK, sub.TestResults[1].Verdict) + require.Equal(t, verdict.OK, sub.TestResults[2].Verdict) + require.Equal(t, sub.GroupResults, models.GroupResults{ + { + GroupName: "group1", + Points: 20., + Passed: true, + }, + }) + }) + }) +} diff --git a/master/queue/jobgenerators/icpc_generator.go b/master/queue/jobgenerators/icpc_generator.go index e99e759..005d89c 100644 --- a/master/queue/jobgenerators/icpc_generator.go +++ b/master/queue/jobgenerators/icpc_generator.go @@ -11,20 +11,12 @@ import ( "testing_system/lib/logger" ) -type state int - -const ( - compilationNotStarted state = iota - compilationStarted - compilationFinished -) - type ICPCGenerator struct { id string mutex sync.Mutex submission *models.Submission problem *models.Problem - state state + state generatorState hasFails bool givenJobs map[string]*invokerconn.Job futureTests []uint64 @@ -130,7 +122,7 @@ func (i *ICPCGenerator) JobCompleted(result *masterconn.InvokerJobResult) (*mode defer i.mutex.Unlock() job, ok := i.givenJobs[result.JobID] if !ok { - return nil, fmt.Errorf("job not found") + return nil, fmt.Errorf("job %s does not exist", result.JobID) } delete(i.givenJobs, result.JobID) @@ -150,7 +142,7 @@ func newICPCGenerator(problem *models.Problem, submission *models.Submission) (G logger.Panic("Can't generate generator id: %w", err) } - if problem.ProblemType != models.ProblemType_ICPC { + if problem.ProblemType != models.ProblemTypeICPC { return nil, fmt.Errorf("problem %v is not ICPC", problem.ID) } futureTests := make([]uint64, 0, problem.TestsNumber) @@ -170,6 +162,7 @@ func newICPCGenerator(problem *models.Problem, submission *models.Submission) (G return &ICPCGenerator{ id: id.String(), submission: submission, + state: compilationNotStarted, problem: problem, givenJobs: make(map[string]*invokerconn.Job), futureTests: futureTests, diff --git a/master/queue/jobgenerators/icpc_generator_test.go b/master/queue/jobgenerators/icpc_generator_test.go deleted file mode 100644 index 4115e50..0000000 --- a/master/queue/jobgenerators/icpc_generator_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package jobgenerators - -import ( - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" - "testing_system/common/connectors/invokerconn" - "testing_system/common/connectors/masterconn" - "testing_system/common/constants/verdict" - "testing_system/common/db/models" -) - -func fixtureProblem() *models.Problem { - return &models.Problem{ - ProblemType: models.ProblemType_ICPC, - TestsNumber: 10, - } -} - -func fixtureSubmission() *models.Submission { - submission := &models.Submission{} - submission.ID = 1 - return submission -} - -func nextJob(t *testing.T, g Generator, SubmitID uint, jobType invokerconn.JobType, test uint64) *invokerconn.Job { - job := g.NextJob() - assert.NotNil(t, job) - assert.Equal(t, job.Type, jobType) - assert.Equal(t, SubmitID, job.SubmitID) - assert.Equal(t, test, job.Test) - return job -} - -func noJobs(t *testing.T, g Generator) { - job := g.NextJob() - assert.Nil(t, job) -} - -func TestStraightTasksFinishing(t *testing.T) { - problem := fixtureProblem() - submission := fixtureSubmission() - generator, err := NewGenerator(problem, submission) - require.Nil(t, err) - job := nextJob(t, generator, 1, invokerconn.CompileJob, 0) - noJobs(t, generator) - sub, err := generator.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.CD, - }) - require.Nil(t, sub) - require.Nil(t, err) - for i := range 9 { - job = nextJob(t, generator, 1, invokerconn.TestJob, uint64(i)+1) - sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - } - job = nextJob(t, generator, 1, invokerconn.TestJob, 10) - sub, err = generator.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.OK, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.OK, sub.Verdict) - require.Equal(t, 1., sub.Score) - for i, result := range sub.TestResults { - require.Equal(t, verdict.OK, result.Verdict) - require.Equal(t, uint64(i)+1, result.TestNumber) - } -} - -func TestTasksFinishing(t *testing.T) { - prepare := func() (Generator, []string) { - problem := fixtureProblem() - submission := fixtureSubmission() - g, err := NewGenerator(problem, submission) - require.Nil(t, err) - job := nextJob(t, g, 1, invokerconn.CompileJob, 0) - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.CD, - }) - require.Nil(t, sub) - require.Nil(t, err) - firstTwoJobIDs := make([]string, 0) - for i := range 2 { - job = nextJob(t, g, 1, invokerconn.TestJob, uint64(i)+1) - firstTwoJobIDs = append(firstTwoJobIDs, job.ID) - } - return g, firstTwoJobIDs - } - finishOtherTests := func(g Generator) { - for i := 2; i < 9; i++ { - job := nextJob(t, g, 1, invokerconn.TestJob, uint64(i)+1) - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - } - job := nextJob(t, g, 1, invokerconn.TestJob, 10) - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.OK, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.OK, sub.Verdict) - require.Equal(t, 1., sub.Score) - for i, result := range sub.TestResults { - require.Equal(t, verdict.OK, result.Verdict) - require.Equal(t, uint64(i)+1, result.TestNumber) - } - } - - t.Run("right order", func(t *testing.T) { - g, firstTwoJobIDs := prepare() - for _, id := range firstTwoJobIDs { - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: id, - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - } - finishOtherTests(g) - }) - - t.Run("wrong order + both ok", func(t *testing.T) { - g, firstTwoJobIDs := prepare() - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[1], - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - - sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[0], - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - - finishOtherTests(g) - }) - - t.Run("wrong order + 2nd fail", func(t *testing.T) { - g, firstTwoJobIDs := prepare() - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[1], - Verdict: verdict.WA, - }) - require.Nil(t, sub) - require.Nil(t, err) - - sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[0], - Verdict: verdict.OK, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.WA, sub.Verdict) - require.Equal(t, 0., sub.Score) - require.Equal(t, verdict.OK, sub.TestResults[0].Verdict) - require.Equal(t, verdict.WA, sub.TestResults[1].Verdict) - for i, result := range sub.TestResults[2:] { - require.Equal(t, verdict.SK, result.Verdict) - require.Equal(t, uint64(i)+3, result.TestNumber) - } - }) - - t.Run("wrong order + 1st fail", func(t *testing.T) { - g, firstTwoJobIDs := prepare() - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[1], - Verdict: verdict.OK, - }) - require.Nil(t, sub) - require.Nil(t, err) - - sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: firstTwoJobIDs[0], - Verdict: verdict.WA, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.WA, sub.Verdict) - require.Equal(t, 0., sub.Score) - require.Equal(t, verdict.WA, sub.TestResults[0].Verdict) - require.Equal(t, verdict.SK, sub.TestResults[1].Verdict) - for i, result := range sub.TestResults[2:] { - require.Equal(t, verdict.SK, result.Verdict) - require.Equal(t, uint64(i)+3, result.TestNumber) - } - }) -} - -func TestFailedCompilation(t *testing.T) { - problem, submission := fixtureProblem(), fixtureSubmission() - g, err := NewGenerator(problem, submission) - require.Nil(t, err) - job := nextJob(t, g, 1, invokerconn.CompileJob, 0) - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.CE, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.CE, sub.Verdict) - require.Equal(t, 0., sub.Score) - for i, result := range sub.TestResults { - require.Equal(t, verdict.SK, result.Verdict) - require.Equal(t, uint64(i)+1, result.TestNumber) - } -} - -func TestFinishSameJobTwice(t *testing.T) { - problem, submission := fixtureProblem(), fixtureSubmission() - g, err := NewGenerator(problem, submission) - require.Nil(t, err) - job := nextJob(t, g, 1, invokerconn.CompileJob, 0) - sub, err := g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.CE, - }) - require.NotNil(t, sub) - require.Nil(t, err) - - require.Equal(t, verdict.CE, sub.Verdict) - require.Equal(t, 0., sub.Score) - for i, result := range sub.TestResults { - require.Equal(t, verdict.SK, result.Verdict) - require.Equal(t, uint64(i)+1, result.TestNumber) - } - - sub, err = g.JobCompleted(&masterconn.InvokerJobResult{ - JobID: job.ID, - Verdict: verdict.CE, - }) - require.Nil(t, sub) - require.NotNil(t, err) -} diff --git a/master/queue/jobgenerators/ioi_generator.go b/master/queue/jobgenerators/ioi_generator.go new file mode 100644 index 0000000..f4049fb --- /dev/null +++ b/master/queue/jobgenerators/ioi_generator.go @@ -0,0 +1,487 @@ +package jobgenerators + +import ( + "fmt" + "github.com/google/uuid" + "github.com/xorcare/pointer" + "slices" + "sync" + "testing_system/common/connectors/invokerconn" + "testing_system/common/connectors/masterconn" + "testing_system/common/constants/verdict" + "testing_system/common/db/models" + "testing_system/lib/logger" +) + +type IOIGenerator struct { + id string + mutex sync.Mutex + submission *models.Submission + problem *models.Problem + state generatorState + givenJobs map[string]*invokerconn.Job + groupNameToGroupInfo map[string]models.TestGroup + groupNameToInternalInfo map[string]*internalGroupInfo + testNumberToGroupName map[uint64]string + internalTestResults []*internalTestResult + + // firstNotCompletedTest = the longest prefix of the tests, for which we know verdict; 1-based indexing + firstNotCompletedTest uint64 + // firstNotCompletedGroup = the longest prefix of the groups, for which we know verdict; 1-based indexing + firstNotCompletedGroup uint64 + // firstNotGivenTest = first test with internalTestState = testNotGiven; 1-based indexing + firstNotGivenTest uint64 +} + +type internalTestState int + +const ( + testNotGiven internalTestState = iota + testRunning + testFinished +) + +type internalGroupInfo struct { + shouldGiveNewJobs bool + shouldMarkFinalTestsSkipped bool +} + +type internalTestResult struct { + result *models.TestResult + state internalTestState +} + +func (i *IOIGenerator) checkIfGroupsDependenciesOK(problem *models.Problem) error { + if !slices.IsSortedFunc(problem.TestGroups, func(a, b models.TestGroup) int { + return int(a.FirstTest) - int(b.FirstTest) + }) { + return fmt.Errorf("groups are not sorted by tests") + } + // each test should be in exactly one group, and each group should depend on the earlier ones + lastTest := uint64(0) + encounteredGroups := make(map[string]struct{}) + for _, group := range problem.TestGroups { + if group.FirstTest != lastTest+1 { + return fmt.Errorf("at least one test is not in exactly one group") + } + lastTest = group.LastTest + if _, ok := encounteredGroups[group.Name]; ok { + return fmt.Errorf("duplicate test group: %s", group.Name) + } + for _, requiredGroupName := range group.RequiredGroupNames { + if _, ok := encounteredGroups[requiredGroupName]; !ok { + return fmt.Errorf("missing required group (maybe it is later) %v for group %v", + requiredGroupName, group.Name) + } + requiredGroupInfo := i.groupNameToGroupInfo[requiredGroupName] + if requiredGroupInfo.ScoringType != models.TestGroupScoringTypeComplete { + return fmt.Errorf("group %v depends on group %v with scoringType=%v", + group.Name, requiredGroupInfo.Name, requiredGroupInfo.ScoringType) + } + } + encounteredGroups[group.Name] = struct{}{} + } + if lastTest != problem.TestsNumber { + return fmt.Errorf("at least one test is not in exactly one group") + } + return nil +} + +func (i *IOIGenerator) prepareGenerator() error { + problem := i.problem + if problem.ProblemType != models.ProblemTypeIOI { + return fmt.Errorf("problem %v is not an IOI problem", problem.ID) + } + // each group with TestGroupScoringTypeEachTest must have TestScore + for _, group := range problem.TestGroups { + switch group.ScoringType { + case models.TestGroupScoringTypeComplete, models.TestGroupScoringTypeMin: + if group.GroupScore == nil { + return fmt.Errorf("group %v in problem %v doesn't have GroupScore", group.Name, problem.ID) + } + case models.TestGroupScoringTypeEachTest: + if group.TestScore == nil { + return fmt.Errorf("group %v in problem %v doesn't have TestScore", group.Name, problem.ID) + } + default: + return fmt.Errorf("unknown TestGroupScoringType %v", group.ScoringType) + } + if group.FirstTest > group.LastTest { + return fmt.Errorf("group %v has FirstTest > LastTest", group.Name) + } + for testNumber := group.FirstTest; testNumber <= group.LastTest; testNumber++ { + i.testNumberToGroupName[testNumber-1] = group.Name + i.internalTestResults = append(i.internalTestResults, &internalTestResult{ + result: &models.TestResult{ + TestNumber: testNumber, + Verdict: verdict.RU, + }, + state: testNotGiven, + }) + } + i.groupNameToGroupInfo[group.Name] = group + i.groupNameToInternalInfo[group.Name] = &internalGroupInfo{ + shouldGiveNewJobs: true, + shouldMarkFinalTestsSkipped: false, + } + } + return i.checkIfGroupsDependenciesOK(problem) +} + +func (i *IOIGenerator) ID() string { + return i.id +} + +func (i *IOIGenerator) NextJob() *invokerconn.Job { + i.mutex.Lock() + defer i.mutex.Unlock() + if i.state == compilationFinished && i.firstNotGivenTest > i.problem.TestsNumber { + return nil + } + if i.state == compilationStarted { + return nil + } + id, err := uuid.NewV7() + if err != nil { + logger.Panic("Can't generate id for job: %w", err) + } + job := &invokerconn.Job{ + ID: id.String(), + SubmitID: i.submission.ID, + } + if i.state == compilationNotStarted { + job.Type = invokerconn.CompileJob + i.state = compilationStarted + i.givenJobs[job.ID] = job + return job + } + job.Type = invokerconn.TestJob + for i.firstNotGivenTest <= i.problem.TestsNumber { + groupName := i.testNumberToGroupName[i.firstNotGivenTest-1] + groupInfo := i.groupNameToInternalInfo[groupName] + if !groupInfo.shouldGiveNewJobs { + i.internalTestResults[i.firstNotGivenTest-1].state = testFinished + i.firstNotGivenTest++ + continue + } + i.internalTestResults[i.firstNotGivenTest-1].state = testRunning + job.Test = i.firstNotGivenTest + i.givenJobs[job.ID] = job + i.firstNotGivenTest++ + return job + } + return nil +} + +func doesTestPreventTestingGroup(testGroupInfo models.TestGroup, testVerdict verdict.Verdict) bool { + switch testGroupInfo.ScoringType { + case models.TestGroupScoringTypeComplete: + return testVerdict != verdict.OK + case models.TestGroupScoringTypeEachTest: + return false + case models.TestGroupScoringTypeMin: + return testVerdict != verdict.OK && testVerdict != verdict.PT + default: + logger.Panic("unknown testGroupInfo.ScoringType %v", testGroupInfo.ScoringType) + } + return false +} + +func (i *IOIGenerator) calcGroupVerdict(groupInfo models.TestGroup) verdict.Verdict { + firstTest, lastTest := groupInfo.FirstTest, groupInfo.LastTest + if slices.IndexFunc(i.submission.TestResults[firstTest-1:lastTest], func(result models.TestResult) bool { + return result.Verdict != verdict.OK + }) != -1 { + return verdict.PT + } else { + return verdict.OK + } +} + +func (i *IOIGenerator) completeGroupTesting(groupInfo models.TestGroup) { + if i.firstNotCompletedTest != groupInfo.LastTest { + logger.Panic("completeGroupTesting called, but wasn't finished right now") + } + score := 0.0 + groupVerdict := i.calcGroupVerdict(groupInfo) + switch groupInfo.ScoringType { + case models.TestGroupScoringTypeComplete: + if groupVerdict == verdict.OK { + score = *groupInfo.GroupScore + } + case models.TestGroupScoringTypeEachTest: + for testNumber := groupInfo.FirstTest; testNumber <= groupInfo.LastTest; testNumber++ { + testScore := i.submission.TestResults[testNumber-1].Points + if testScore == nil { + logger.Panic("test %v has points in group %v", testNumber, groupInfo.Name) + } else { + score += *testScore + } + } + case models.TestGroupScoringTypeMin: + score = *groupInfo.GroupScore + for testNumber := groupInfo.FirstTest; testNumber <= groupInfo.LastTest; testNumber++ { + testScore := i.submission.TestResults[testNumber-1].Points + if testScore == nil { + if i.submission.TestResults[testNumber-1].Verdict != verdict.SK { + logger.Panic("test %v has points in group %v", testNumber, groupInfo.Name) + } else { + score = 0 + } + } else { + score = min(score, *testScore) + } + } + } + i.submission.GroupResults = append(i.submission.GroupResults, models.GroupResult{ + GroupName: groupInfo.Name, + Points: score, + Passed: groupVerdict == verdict.OK, + }) + i.firstNotCompletedGroup++ +} + +// updateSubmissionResult must be done with acquired mutex +func (i *IOIGenerator) updateSubmissionResult() (*models.Submission, error) { +TestsLoop: + for i.firstNotCompletedTest <= i.problem.TestsNumber { + testGroupName := i.testNumberToGroupName[i.firstNotCompletedTest-1] + testGroupInfo := i.groupNameToGroupInfo[testGroupName] + testInternalGroupInfo := i.groupNameToInternalInfo[testGroupName] + switch i.internalTestResults[i.firstNotCompletedTest-1].state { + case testNotGiven: + if !testInternalGroupInfo.shouldMarkFinalTestsSkipped { + break TestsLoop + } + case testRunning: + break TestsLoop + case testFinished: + break + } + + if testInternalGroupInfo.shouldMarkFinalTestsSkipped { + i.submission.TestResults = append(i.submission.TestResults, + models.TestResult{ + TestNumber: i.internalTestResults[i.firstNotCompletedTest-1].result.TestNumber, + Verdict: verdict.SK, + }) + } else { + i.submission.TestResults = append(i.submission.TestResults, + *i.internalTestResults[i.firstNotCompletedTest-1].result) + testVerdict := i.internalTestResults[i.firstNotCompletedTest-1].result.Verdict + if doesTestPreventTestingGroup(testGroupInfo, testVerdict) { + testInternalGroupInfo.shouldMarkFinalTestsSkipped = true + for _, group := range i.problem.TestGroups { + for _, requiredGroupName := range group.RequiredGroupNames { + if i.groupNameToInternalInfo[requiredGroupName].shouldMarkFinalTestsSkipped { + i.groupNameToInternalInfo[group.Name].shouldMarkFinalTestsSkipped = true + } + } + } + } + } + + if i.firstNotCompletedTest == testGroupInfo.LastTest { + i.completeGroupTesting(testGroupInfo) + } + i.firstNotCompletedTest++ + } + if i.firstNotCompletedTest > i.problem.TestsNumber && len(i.givenJobs) == 0 { + i.setFinalScoreAndVerdict() + if int(i.firstNotCompletedGroup) <= len(i.problem.TestGroups) { + logger.Panic("not all the groups were filled, but the problem is considered tested") + } + return i.submission, nil + } + return nil, nil +} + +// compileJobCompleted must be done with acquired mutex +func (i *IOIGenerator) compileJobCompleted( + job *invokerconn.Job, + result *masterconn.InvokerJobResult, +) { + if job.Type != invokerconn.CompileJob { + logger.Warn("job type %s is %v; treating is as a compile job", job.ID, job.Type) + } + switch result.Verdict { + case verdict.CD: + i.state = compilationFinished + case verdict.CE: + i.submission.Verdict = verdict.CE + for _, group := range i.problem.TestGroups { + i.groupNameToInternalInfo[group.Name].shouldMarkFinalTestsSkipped = true + } + default: + logger.Panic("unknown verdict for compilation completed: %v", result.Verdict) + } +} + +func populateTestJobResult(groupInfo models.TestGroup, result *masterconn.InvokerJobResult) error { + switch groupInfo.ScoringType { + case models.TestGroupScoringTypeComplete: + break + case models.TestGroupScoringTypeEachTest: + if result.Points == nil { + if result.Verdict == verdict.OK { + result.Points = groupInfo.TestScore + } else if result.Verdict == verdict.PT { + return fmt.Errorf("checker returned nil points, but verdict=%v", result.Verdict) + } else { + result.Points = pointer.Float64(0) + } + } + case models.TestGroupScoringTypeMin: + if result.Points == nil { + if result.Verdict == verdict.OK { + result.Points = groupInfo.GroupScore + return nil + } else if result.Verdict == verdict.PT { + return fmt.Errorf("checker returned nil points, but verdict=%v", result.Verdict) + } else { + result.Points = pointer.Float64(0) + } + } + default: + logger.Panic("unknown group scoring type: %v", groupInfo.ScoringType) + } + return nil +} + +func (i *IOIGenerator) stopGivingNewTestsIfNeeded( + testInternalGroupInfo *internalGroupInfo, + testGroupInfo models.TestGroup, + testVerdict verdict.Verdict, +) { + if !doesTestPreventTestingGroup(testGroupInfo, testVerdict) { + return + } + + testInternalGroupInfo.shouldGiveNewJobs = false + for _, group := range i.problem.TestGroups { + for _, requiredGroupName := range group.RequiredGroupNames { + if !i.groupNameToInternalInfo[requiredGroupName].shouldGiveNewJobs { + i.groupNameToInternalInfo[group.Name].shouldGiveNewJobs = false + break + } + } + } +} + +func (i *IOIGenerator) setFinalScoreAndVerdict() { + if len(i.givenJobs) != 0 { + logger.Panic("setFinalScoreAndVerdict called, but there are some jobs still") + } + if i.firstNotCompletedTest != i.problem.TestsNumber+1 { + logger.Panic("setFinalScoreAndVerdict called, but not all the tests were completed") + } + if len(i.submission.GroupResults) != len(i.problem.TestGroups) { + logger.Panic("for some reason \"len(i.submission.GroupResults) != len(i.problem.TestGroups)\"") + } + if len(i.submission.TestResults) != int(i.problem.TestsNumber) { + logger.Panic("for some reason \"len(i.submission.TestResults) != int(i.problem.TestsNumber)\"") + } + for _, groupResult := range i.submission.GroupResults { + i.submission.Score += groupResult.Points + } + if i.submission.Verdict != verdict.RU { + logger.Trace("submission %v already has verdict %v", i.submission, i.submission.Verdict) + return + } + + hasSkipped := false + hasVerdict := false + for _, testResult := range i.submission.TestResults { + if testResult.Verdict != verdict.OK && testResult.Verdict != verdict.SK { + i.submission.Verdict = verdict.PT + hasVerdict = true + } else if testResult.Verdict == verdict.SK { + hasSkipped = true + } + } + if !hasVerdict { + if hasSkipped { + logger.Panic("problem does not have verdict, but have skipped tests") + } + i.submission.Verdict = verdict.OK + } + return +} + +// testJobCompleted must be done with acquired mutex +func (i *IOIGenerator) testJobCompleted( + job *invokerconn.Job, + result *masterconn.InvokerJobResult, +) { + if job.Type != invokerconn.TestJob { + logger.Warn("job type %s is %v; treating is as a testing job", job.ID, job.Type) + } + testGroupName := i.testNumberToGroupName[job.Test-1] + testGroupInfo := i.groupNameToGroupInfo[testGroupName] + testInternalGroupInfo := i.groupNameToInternalInfo[testGroupName] + // move info from result to internal state + if err := populateTestJobResult(testGroupInfo, result); err != nil { + result.Verdict = verdict.CF + if result.Points != nil { + result.Points = pointer.Float64(0) + } + if result.Error != "" { + result.Error += "; " + } + result.Error += err.Error() + } + i.internalTestResults[job.Test-1].result.Points = result.Points + i.internalTestResults[job.Test-1].result.Verdict = result.Verdict + i.internalTestResults[job.Test-1].result.Error = result.Error + if result.Statistics != nil { + i.internalTestResults[job.Test-1].result.Time = result.Statistics.Time + i.internalTestResults[job.Test-1].result.Memory = result.Statistics.Memory + } + i.internalTestResults[job.Test-1].state = testFinished + i.stopGivingNewTestsIfNeeded(testInternalGroupInfo, testGroupInfo, result.Verdict) +} + +func (i *IOIGenerator) JobCompleted(jobResult *masterconn.InvokerJobResult) (*models.Submission, error) { + i.mutex.Lock() + defer i.mutex.Unlock() + job, ok := i.givenJobs[jobResult.JobID] + if !ok { + return nil, fmt.Errorf("job %s does not exist", jobResult.JobID) + } + delete(i.givenJobs, jobResult.JobID) + switch job.Type { + case invokerconn.CompileJob: + i.compileJobCompleted(job, jobResult) + case invokerconn.TestJob: + i.testJobCompleted(job, jobResult) + default: + return nil, fmt.Errorf("unknown job type for IOI problem: %v", job.Type) + } + return i.updateSubmissionResult() +} + +func NewIOIGenerator(problem *models.Problem, submission *models.Submission) (Generator, error) { + id, err := uuid.NewV7() + if err != nil { + logger.Panic("Can't generate generator id: %w", err) + } + generator := &IOIGenerator{ + id: id.String(), + submission: submission, + problem: problem, + state: compilationNotStarted, + givenJobs: make(map[string]*invokerconn.Job), + groupNameToGroupInfo: make(map[string]models.TestGroup), + groupNameToInternalInfo: make(map[string]*internalGroupInfo), + testNumberToGroupName: make(map[uint64]string), + internalTestResults: make([]*internalTestResult, 0), + firstNotCompletedTest: 1, + firstNotCompletedGroup: 1, + firstNotGivenTest: 1, + } + generator.submission.Verdict = verdict.RU + if err = generator.prepareGenerator(); err != nil { + return nil, err + } + return generator, nil +} diff --git a/master/queue/queue_test.go b/master/queue/queue_test.go index 9d3e3ef..534e2a2 100644 --- a/master/queue/queue_test.go +++ b/master/queue/queue_test.go @@ -54,11 +54,11 @@ func TestQueueWork(t *testing.T) { q := NewQueue(nil).(*Queue) problem1 := models.Problem{ TestsNumber: 2, - ProblemType: models.ProblemType_ICPC, + ProblemType: models.ProblemTypeICPC, } problem2 := models.Problem{ TestsNumber: 2, - ProblemType: models.ProblemType_ICPC, + ProblemType: models.ProblemTypeICPC, } problem1.ID, problem2.ID = 1, 2 submission1 := models.Submission{} @@ -77,11 +77,11 @@ func TestQueueFairness(t *testing.T) { q := NewQueue(nil).(*Queue) problem1 := models.Problem{ TestsNumber: 500, - ProblemType: models.ProblemType_ICPC, + ProblemType: models.ProblemTypeICPC, } problem2 := models.Problem{ TestsNumber: 10, - ProblemType: models.ProblemType_ICPC, + ProblemType: models.ProblemTypeICPC, } problem1.ID, problem2.ID = 1, 2 submission1 := models.Submission{} @@ -104,7 +104,7 @@ func TestQueue_RescheduleJob(t *testing.T) { require.True(t, isQueueEmpty(q)) problem1 := models.Problem{ TestsNumber: 1, - ProblemType: models.ProblemType_ICPC, + ProblemType: models.ProblemTypeICPC, } problem2 := problem1 submission1 := models.Submission{}