diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 9a10d3d..d3208a7 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -19,7 +19,7 @@ jobs: go-version: stable - name: Install dependency tools - run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest | go install golang.org/x/tools/cmd/goimports@latest + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 | go install golang.org/x/tools/cmd/goimports@latest - name: Set up pre-commit Cache uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94c5ce8..d355606 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,4 +13,6 @@ repos: - id: go-fmt - id: go-imports - id: go-unit-tests + pass_filenames: false - id: golangci-lint + pass_filenames: false diff --git a/docker-compose.yaml b/docker-compose.yaml index 4eea310..7521f63 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,15 +20,22 @@ services: - RABBITMQ_USER=guest - RABBITMQ_PASSWORD=guest - RABBITMQ_PORT=5672 - - FILESTORAGE_HOST=file-storage - - FILESTORAGE_PORT=8081 + - STORAGE_HOST=file-storage + - STORAGE_PORT=8081 - DOCKER_HOST=unix:///var/run/docker.sock + - WORKER_QUEUE_NAME=worker_queue + - MAX_WORKERS=10 file-storage: - image: maxit/file-storage - pull_policy: never + image: ghcr.io/mini-maxit/file-storage:latest container_name: file-storage + # Internal server (writes + /sign) is network-only — never host-published. + # Public server (signed GET/HEAD) is exposed for local testing. ports: - - "8081:8081" + - "8888:8888" + environment: + - PUBLIC_SERVER_PORT=8888 + - INTERNAL_SERVER_PORT=8081 + - SIGNING_SECRET=dev-secret volumes: rabbitmq_data: diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 9915786..1c18ce3 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -3,6 +3,7 @@ package pipeline import ( "fmt" "os" + "sync" "github.com/mini-maxit/worker/internal/logger" "github.com/mini-maxit/worker/internal/rabbitmq/responder" @@ -33,6 +34,7 @@ type WorkerState struct { type worker struct { id int + mu sync.RWMutex state WorkerState responseQueue string packager packager.Packager @@ -67,25 +69,48 @@ func (ws *worker) GetId() int { } func (ws *worker) GetState() WorkerState { + ws.mu.RLock() + defer ws.mu.RUnlock() return ws.state } func (ws *worker) UpdateStatus(status constants.WorkerStatus) { + ws.mu.Lock() + defer ws.mu.Unlock() ws.state.Status = status } func (ws *worker) GetProcessingMessageID() string { + ws.mu.RLock() + defer ws.mu.RUnlock() return ws.state.ProcessingMessageID } +func (ws *worker) setProcessing(messageID, responseQueue string) { + ws.mu.Lock() + defer ws.mu.Unlock() + ws.state.ProcessingMessageID = messageID + ws.responseQueue = responseQueue +} + +func (ws *worker) clearProcessing() { + ws.mu.Lock() + defer ws.mu.Unlock() + ws.state.ProcessingMessageID = "" + ws.responseQueue = "" +} + func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.TaskQueueMessage) { + ws.logger.Infof("Processing task [MsgID: %s]", messageID) + ws.setProcessing(messageID, responseQueue) defer func() { + ws.clearProcessing() if r := recover(); r != nil { if err, ok := r.(error); ok { ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, err, ) } else { @@ -94,21 +119,13 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta } }() - ws.logger.Infof("Processing task [MsgID: %s]", messageID) - ws.state.ProcessingMessageID = messageID - ws.responseQueue = responseQueue - defer func() { - ws.state.ProcessingMessageID = "" - ws.responseQueue = "" - }() - langType, err := languages.ParseLanguageType(task.LanguageType) if err != nil { ws.logger.Errorf("Invalid language type %s: %s", task.LanguageType, err) ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, err, ) return @@ -118,8 +135,8 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta if err != nil { ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, err, ) return @@ -155,8 +172,8 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta if err != nil { ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, err, ) return @@ -167,7 +184,7 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta fileInfo, statErr := os.Stat(dc.CompileErrFilePath) if statErr == nil && fileInfo.Size() > 0 { - ws.publishCompilationError(dc, task.TestCases) + ws.publishCompilationError(dc, task.TestCases, messageID, responseQueue) return } } @@ -178,8 +195,8 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta if err != nil { ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, err, ) return @@ -187,21 +204,25 @@ func (ws *worker) ProcessTask(messageID, responseQueue string, task *messages.Ta ws.responder.PublishPayloadTaskRespond( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, solutionResult, ) ws.logger.Infof("Finished processing task [MsgID: %s]", messageID) } -func (ws *worker) publishCompilationError(dirConfig *packager.TaskDirConfig, testCases []messages.TestCase) { - ws.logger.Infof("Compilation error occurred for message ID: %s", ws.state.ProcessingMessageID) - sendErr := ws.packager.SendSolutionPackage(dirConfig, testCases, true, ws.state.ProcessingMessageID) +func (ws *worker) publishCompilationError( + dirConfig *packager.TaskDirConfig, + testCases []messages.TestCase, + messageID, responseQueue string, +) { + ws.logger.Infof("Compilation error occurred for message ID: %s", messageID) + sendErr := ws.packager.SendSolutionPackage(dirConfig, testCases, true, messageID) if sendErr != nil { ws.responder.PublishTaskErrorToResponseQueue( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, sendErr, ) return @@ -213,8 +234,8 @@ func (ws *worker) publishCompilationError(dirConfig *packager.TaskDirConfig, tes } ws.responder.PublishPayloadTaskRespond( constants.QueueMessageTypeTask, - ws.state.ProcessingMessageID, - ws.responseQueue, + messageID, + responseQueue, solutionResult, ) } diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 6033a6d..bfe1137 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -15,6 +15,12 @@ import ( "go.uber.org/mock/gomock" ) +const ( + testUserSolutionPath = "src" + testUserExecFilePath = "exec" + testLanguageType = "cpp" +) + // setupSuccessfulPipelineMocks configures mocks for a successful task processing flow. func setupSuccessfulPipelineMocks( t *testing.T, @@ -26,8 +32,8 @@ func setupSuccessfulPipelineMocks( tmpDir := t.TempDir() dir := &packager.TaskDirConfig{ PackageDirPath: tmpDir, - UserSolutionPath: "src", - UserExecFilePath: "exec", + UserSolutionPath: testUserSolutionPath, + UserExecFilePath: testUserExecFilePath, CompileErrFilePath: "compile.err", } mockPackager.EXPECT().PrepareSolutionPackage(gomock.Any(), gomock.Any(), gomock.Any()).Return(dir, nil) @@ -57,7 +63,7 @@ func TestProcessTask_SuccessFlow(t *testing.T) { w := pipeline.NewWorker(1, mockPackager, mockExecutor, mockVerifier, mockResponder) task := &messages.TaskQueueMessage{ - LanguageType: "cpp", + LanguageType: testLanguageType, LanguageVersion: "11", TestCases: []messages.TestCase{ {TimeLimitMs: 100, MemoryLimitKB: 65536}, @@ -82,8 +88,8 @@ func TestProcessTask_CompilationErrorFlow(t *testing.T) { tmpDir := t.TempDir() dir := &packager.TaskDirConfig{ PackageDirPath: tmpDir, - UserSolutionPath: "src", - UserExecFilePath: "exec", + UserSolutionPath: testUserSolutionPath, + UserExecFilePath: testUserExecFilePath, CompileErrFilePath: tmpDir + "/compile.err", } mockPackager.EXPECT().PrepareSolutionPackage(gomock.Any(), gomock.Any(), gomock.Any()).Return(dir, nil) @@ -115,7 +121,7 @@ func TestProcessTask_CompilationErrorFlow(t *testing.T) { ) w := pipeline.NewWorker(2, mockPackager, mockExecutor, mockVerifier, mockResponder) - task := &messages.TaskQueueMessage{LanguageType: "cpp", LanguageVersion: "11", TestCases: nil} + task := &messages.TaskQueueMessage{LanguageType: testLanguageType, LanguageVersion: "11", TestCases: nil} w.ProcessTask("msg-compile", "respQ", task) } @@ -136,7 +142,7 @@ func TestProcessTask_PreparePackageFails(t *testing.T) { mockResponder.EXPECT().PublishTaskErrorToResponseQueue(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) w := pipeline.NewWorker(3, mockPackager, mockExecutor, mockVerifier, mockResponder) - task := &messages.TaskQueueMessage{LanguageType: "cpp"} + task := &messages.TaskQueueMessage{LanguageType: testLanguageType} w.ProcessTask("msg-dl", "respQ", task) } @@ -152,8 +158,8 @@ func TestProcessTask_SendPackageFailsAfterRun(t *testing.T) { tmpDir := t.TempDir() dir := &packager.TaskDirConfig{ PackageDirPath: tmpDir, - UserSolutionPath: "src", - UserExecFilePath: "exec", + UserSolutionPath: testUserSolutionPath, + UserExecFilePath: testUserExecFilePath, CompileErrFilePath: tmpDir + "/compile.err", } mockPackager.EXPECT().PrepareSolutionPackage(gomock.Any(), gomock.Any(), gomock.Any()).Return(dir, nil) @@ -170,7 +176,7 @@ func TestProcessTask_SendPackageFailsAfterRun(t *testing.T) { mockResponder.EXPECT().PublishTaskErrorToResponseQueue(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) w := pipeline.NewWorker(4, mockPackager, mockExecutor, mockVerifier, mockResponder) - task := &messages.TaskQueueMessage{LanguageType: "cpp"} + task := &messages.TaskQueueMessage{LanguageType: testLanguageType} w.ProcessTask("msg-upload", "respQ", task) } @@ -186,8 +192,8 @@ func TestProcessTask_VerifierPanicRecovered(t *testing.T) { tmpDir := t.TempDir() dir := &packager.TaskDirConfig{ PackageDirPath: tmpDir, - UserSolutionPath: "src", - UserExecFilePath: "exec", + UserSolutionPath: testUserSolutionPath, + UserExecFilePath: testUserExecFilePath, CompileErrFilePath: tmpDir + "/compile.err", } mockPackager.EXPECT().PrepareSolutionPackage(gomock.Any(), gomock.Any(), gomock.Any()).Return(dir, nil) @@ -203,7 +209,7 @@ func TestProcessTask_VerifierPanicRecovered(t *testing.T) { mockResponder.EXPECT().PublishTaskErrorToResponseQueue(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) w := pipeline.NewWorker(5, mockPackager, mockExecutor, mockVerifier, mockResponder) - task := &messages.TaskQueueMessage{LanguageType: "cpp"} + task := &messages.TaskQueueMessage{LanguageType: testLanguageType} w.ProcessTask("msg-panic", "respQ", task) } @@ -220,7 +226,7 @@ func TestProcessTask_PublishPayloadFails(t *testing.T) { w := pipeline.NewWorker(6, mockPackager, mockExecutor, mockVerifier, mockResponder) task := &messages.TaskQueueMessage{ - LanguageType: "cpp", + LanguageType: testLanguageType, LanguageVersion: "11", TestCases: []messages.TestCase{ {TimeLimitMs: 100, MemoryLimitKB: 65536}, @@ -280,8 +286,8 @@ func TestGetProcessingMessageID(t *testing.T) { done := make(chan struct{}) dir := &packager.TaskDirConfig{ PackageDirPath: t.TempDir(), - UserSolutionPath: "src", - UserExecFilePath: "exec", + UserSolutionPath: testUserSolutionPath, + UserExecFilePath: testUserExecFilePath, CompileErrFilePath: "compile.err", } @@ -308,7 +314,7 @@ func TestGetProcessingMessageID(t *testing.T) { mockResponder.EXPECT(). PublishPayloadTaskRespond(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) go func() { - w.ProcessTask("msg-123", "respQ", &messages.TaskQueueMessage{LanguageType: "cpp"}) + w.ProcessTask("msg-123", "respQ", &messages.TaskQueueMessage{LanguageType: testLanguageType}) }() // Wait until PrepareSolutionPackage is invoked (worker has started) @@ -339,6 +345,69 @@ func TestGetProcessingMessageID(t *testing.T) { } } +func TestWorkerStateConcurrentAccess(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockPackager := mocks.NewMockPackager(ctrl) + mockExecutor := mocks.NewMockExecutor(ctrl) + mockVerifier := mocks.NewMockVerifier(ctrl) + mockResponder := mocks.NewMockResponder(ctrl) + + w := pipeline.NewWorker(9, mockPackager, mockExecutor, mockVerifier, mockResponder) + + dir := &packager.TaskDirConfig{PackageDirPath: t.TempDir()} + started := make(chan struct{}) + done := make(chan struct{}) + + mockPackager.EXPECT().PrepareSolutionPackage(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ interface{}, _ interface{}, _ interface{}) (*packager.TaskDirConfig, error) { + close(started) + <-done + return dir, nil + }, + ) + mockPackager.EXPECT().SendSolutionPackage(dir, gomock.Any(), false, gomock.Any()).Return(nil) + mockExecutor.EXPECT().ExecuteCommand(gomock.Any()).Return(nil) + mockVerifier.EXPECT().EvaluateAllTestCases(dir, gomock.Any(), gomock.Any(), gomock.Any()). + Return(solution.Result{StatusCode: solution.Success, Message: "OK"}) + mockResponder.EXPECT().PublishPayloadTaskRespond(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + + go func() { + w.ProcessTask("concurrent-msg", "respQ", &messages.TaskQueueMessage{LanguageType: testLanguageType}) + }() + + <-started + + // Hammer state readers concurrently while the worker goroutine mutates state. + readersDone := make(chan struct{}) + go func() { + for { + select { + case <-readersDone: + return + default: + w.GetProcessingMessageID() + w.GetState() + w.UpdateStatus(constants.WorkerStatusIdle) + } + } + }() + + close(done) + + deadline := time.After(2 * time.Second) + for w.GetProcessingMessageID() != "" { + select { + case <-deadline: + t.Fatalf("timeout waiting for worker to finish") + default: + time.Sleep(5 * time.Millisecond) + } + } + close(readersDone) +} + // TestProcessTask_ContainerCompilationSuccess tests compilation happening inside container. func TestProcessTask_ContainerCompilationSuccess(t *testing.T) { ctrl := gomock.NewController(t) @@ -384,7 +453,7 @@ func TestProcessTask_ContainerCompilationSuccess(t *testing.T) { w := pipeline.NewWorker(9, mockPackager, mockExecutor, mockVerifier, mockResponder) task := &messages.TaskQueueMessage{ - LanguageType: "cpp", + LanguageType: testLanguageType, LanguageVersion: "11", TestCases: []messages.TestCase{ {TimeLimitMs: 100, MemoryLimitKB: 65536}, @@ -444,7 +513,7 @@ func TestProcessTask_ContainerCompilationErrorDetection(t *testing.T) { w := pipeline.NewWorker(10, mockPackager, mockExecutor, mockVerifier, mockResponder) task := &messages.TaskQueueMessage{ - LanguageType: "cpp", + LanguageType: testLanguageType, LanguageVersion: "11", TestCases: []messages.TestCase{{TimeLimitMs: 100, MemoryLimitKB: 65536}}, } diff --git a/internal/rabbitmq/consumer/consumer_test.go b/internal/rabbitmq/consumer/consumer_test.go index 44323fa..0220d5f 100644 --- a/internal/rabbitmq/consumer/consumer_test.go +++ b/internal/rabbitmq/consumer/consumer_test.go @@ -19,7 +19,11 @@ import ( "github.com/mini-maxit/worker/pkg/messages" ) -const workerQueue = "worker_queue_test" +const ( + workerQueue = "worker_queue_test" + testReplyTo = "reply" + testLang = "CPP" +) func TestProcessMessage(t *testing.T) { ctrl := gomock.NewController(t) @@ -32,9 +36,9 @@ func TestProcessMessage(t *testing.T) { t.Run("invalid json", func(t *testing.T) { // Expect responder.PublishErrorToResponseQueue called with empty type/messageID and the replyTo - mockResponder.EXPECT().PublishErrorToResponseQueue("", "", "reply", gomock.Any()).Times(1) + mockResponder.EXPECT().PublishErrorToResponseQueue("", "", testReplyTo, gomock.Any()).Times(1) - msg := amqp.Delivery{Body: []byte("not json"), ReplyTo: "reply"} + msg := amqp.Delivery{Body: []byte("not json"), ReplyTo: testReplyTo} c.ProcessMessage(msg) }) @@ -42,35 +46,38 @@ func TestProcessMessage(t *testing.T) { qm := messages.QueueMessage{Type: "foo", MessageID: "mid", Payload: nil} b, _ := json.Marshal(qm) - mockResponder.EXPECT().PublishErrorToResponseQueue("foo", "mid", "reply", pkgerrors.ErrUnknownMessageType).Times(1) + mockResponder. + EXPECT(). + PublishErrorToResponseQueue("foo", "mid", testReplyTo, pkgerrors.ErrUnknownMessageType). + Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) t.Run("task success", func(t *testing.T) { - task := messages.TaskQueueMessage{LanguageType: "CPP", LanguageVersion: "17"} + task := messages.TaskQueueMessage{LanguageType: testLang, LanguageVersion: "17"} taskB, _ := json.Marshal(&task) qm := messages.QueueMessage{Type: constants.QueueMessageTypeTask, MessageID: "task-id-1", Payload: taskB} b, _ := json.Marshal(qm) mockScheduler.EXPECT().ProcessTask( - "reply", "task-id-1", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), + testReplyTo, "task-id-1", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), ).Return(nil).Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) t.Run("task requeue when no worker", func(t *testing.T) { - task := messages.TaskQueueMessage{LanguageType: "CPP", LanguageVersion: "17"} + task := messages.TaskQueueMessage{LanguageType: testLang, LanguageVersion: "17"} taskB, _ := json.Marshal(&task) qm := messages.QueueMessage{Type: constants.QueueMessageTypeTask, MessageID: "task-id-2", Payload: taskB} b, _ := json.Marshal(qm) // Scheduler returns ErrFailedToGetFreeWorker mockScheduler.EXPECT().ProcessTask( - "reply", "task-id-2", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), + testReplyTo, "task-id-2", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), ).Return(pkgerrors.ErrFailedToGetFreeWorker).Times(1) // Expect responder.Publish called to requeue with higher priority. Inspect publishing in Do. @@ -82,7 +89,7 @@ func TestProcessMessage(t *testing.T) { } }).Return(nil).Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) @@ -97,10 +104,10 @@ func TestProcessMessage(t *testing.T) { mockScheduler.EXPECT().GetWorkersStatus().Return(status).Times(1) mockResponder.EXPECT().PublishSuccessStatusRespond( - constants.QueueMessageTypeStatus, "status-id", "reply", status, + constants.QueueMessageTypeStatus, "status-id", testReplyTo, status, ).Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) @@ -112,7 +119,7 @@ func TestProcessMessage(t *testing.T) { mockResponder.EXPECT().PublishSuccessHandshakeRespond( constants.QueueMessageTypeHandshake, "hs-id", - "reply", + testReplyTo, gomock.AssignableToTypeOf(messages.ResponseHandshakePayload{}), ).Do(func(_ string, _ string, _ string, langs messages.ResponseHandshakePayload) { if len(langs.Languages) == 0 { @@ -120,21 +127,21 @@ func TestProcessMessage(t *testing.T) { } }).Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) t.Run("task success", func(t *testing.T) { - task := messages.TaskQueueMessage{LanguageType: "CPP", LanguageVersion: "17"} + task := messages.TaskQueueMessage{LanguageType: testLang, LanguageVersion: "17"} taskB, _ := json.Marshal(&task) qm := messages.QueueMessage{Type: constants.QueueMessageTypeTask, MessageID: "task-id-1", Payload: taskB} b, _ := json.Marshal(qm) mockScheduler.EXPECT().ProcessTask( - "reply", "task-id-1", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), + testReplyTo, "task-id-1", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), ).Return(nil).Times(1) - msg := amqp.Delivery{Body: b, ReplyTo: "reply"} + msg := amqp.Delivery{Body: b, ReplyTo: testReplyTo} c.ProcessMessage(msg) }) } @@ -174,7 +181,7 @@ func TestListen_ProcessTaskMessage(t *testing.T) { // When the scheduler's ProcessTask is called, signal completion done := make(chan struct{}, 1) mockScheduler.EXPECT().ProcessTask( - "reply", "task-id-listen", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), + testReplyTo, "task-id-listen", gomock.AssignableToTypeOf(&messages.TaskQueueMessage{}), ).Do( func(_ string, _ string, _ *messages.TaskQueueMessage) { done <- struct{}{} @@ -199,12 +206,12 @@ func TestListen_ProcessTaskMessage(t *testing.T) { } // Build a task message and send it - task := messages.TaskQueueMessage{LanguageType: "CPP", LanguageVersion: "17"} + task := messages.TaskQueueMessage{LanguageType: testLang, LanguageVersion: "17"} taskB, _ := json.Marshal(&task) qm := messages.QueueMessage{Type: constants.QueueMessageTypeTask, MessageID: "task-id-listen", Payload: taskB} b, _ := json.Marshal(qm) - deliveries <- amqp.Delivery{Body: b, ReplyTo: "reply"} + deliveries <- amqp.Delivery{Body: b, ReplyTo: testReplyTo} // Wait for process to be invoked select { diff --git a/internal/stages/packager/packager_test.go b/internal/stages/packager/packager_test.go index 9a13ddb..6bda0d0 100644 --- a/internal/stages/packager/packager_test.go +++ b/internal/stages/packager/packager_test.go @@ -16,6 +16,28 @@ import ( gomock "go.uber.org/mock/gomock" ) +const ( + testMainCppPath = "solutions/1/main.cpp" + testSubmissionBucket = "sub" + testInputsBucket = "inputs" + testInputPath = "inputs/1/in.txt" + testOutputsBucket = "outputs" + testOutputPath = "outputs/1/out.txt" + testResultsBucket = "results" + testOutResultPath = "results/1/out.result" + testErrResultPath = "results/1/err.result" + testDiffResultPath = "results/1/diff.result" +) + +// uniqueMsgID returns a filesystem-safe unique message ID for a test. +// t.TempDir() yields //001 — the parent basename is +// unique per invocation, so it is used as the msgID to prevent concurrent +// runs of the same test from colliding on the shared /tmp/ dir. +func uniqueMsgID(t *testing.T) string { + t.Helper() + return filepath.Base(filepath.Dir(t.TempDir())) +} + func TestPrepareSolutionPackage_Success(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -23,17 +45,17 @@ func TestPrepareSolutionPackage_Success(t *testing.T) { mockStorage := mocks.NewMockStorage(ctrl) mockFileCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{SubmissionFile: submission, TestCases: []messages.TestCase{tc}} - msgID := filepath.Base(t.TempDir()) + msgID := uniqueMsgID(t) // expect DownloadFile for submission and test case files; destination path can be any mockStorage.EXPECT().DownloadFile(submission, gomock.Any()).Return(nil) @@ -131,8 +153,8 @@ func TestSendSolutionPackage_WithCompilationError_Uploads(t *testing.T) { // test case describing where to upload tc := messages.TestCase{StdErrResult: messages.FileLocation{Bucket: "res-bucket", Path: "some/path/compile.err"}} - // expect UploadFile with objPath equal to parent dir of Path - mockStorage.EXPECT().UploadFile(compErrPath, "res-bucket", "some/path").Return(nil) + // expect UploadFile with the full Path as the object key + mockStorage.EXPECT().UploadFile(compErrPath, "res-bucket", "some/path/compile.err").Return(nil) p := packager.NewPackager(mockStorage, mockFileCache) @@ -184,10 +206,10 @@ func TestSendSolutionPackage_NoCompilation_UploadsNonEmptyFiles(t *testing.T) { t.Fatalf("failed to write userDiffPath: %v", err) } - // expect UploadFile for each non-empty file, with objPath equal to parent dir of Path - mockStorage.EXPECT().UploadFile(userOutPath, "b", "outputs/task1").Return(nil) - mockStorage.EXPECT().UploadFile(userErrPath, "b", "errors/task1").Return(nil) - mockStorage.EXPECT().UploadFile(userDiffPath, "b", "diffs/task1").Return(nil) + // expect UploadFile for each non-empty file, with the full Path as the object key + mockStorage.EXPECT().UploadFile(userOutPath, "b", "outputs/task1/out.txt").Return(nil) + mockStorage.EXPECT().UploadFile(userErrPath, "b", "errors/task1/err.txt").Return(nil) + mockStorage.EXPECT().UploadFile(userDiffPath, "b", "diffs/task1/diff.txt").Return(nil) p := packager.NewPackager(mockStorage, mockFileCache) @@ -210,20 +232,20 @@ func TestPrepareSolutionPackage_WithCacheHit(t *testing.T) { mockCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, TestCases: []messages.TestCase{tc}, } - msgID := filepath.Base(t.TempDir()) + msgID := uniqueMsgID(t) // Create temp cached files cachedInputPath := filepath.Join(t.TempDir(), "cached_in.txt") @@ -283,20 +305,20 @@ func TestPrepareSolutionPackage_WithCacheMiss(t *testing.T) { mockCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, TestCases: []messages.TestCase{tc}, } - msgID := filepath.Base(t.TempDir()) + msgID := uniqueMsgID(t) // Expect submission download (not cached) mockStorage.EXPECT().DownloadFile(submission, gomock.Any()).Return(nil) @@ -337,14 +359,14 @@ func TestPrepareSolutionPackage_CacheGetError_FallbackToDownload(t *testing.T) { mockCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, @@ -386,14 +408,14 @@ func TestPrepareSolutionPackage_CacheFileError_ContinuesWithoutCaching(t *testin mockCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, @@ -435,14 +457,14 @@ func TestPrepareSolutionPackage_NoTaskVersion_SkipsCache(t *testing.T) { mockCache := mocks.NewMockFileCache(ctrl) // prepare message WITHOUT TaskFilesVersion - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, @@ -483,14 +505,14 @@ func TestPrepareSolutionPackage_NilCache_DownloadsDirectly(t *testing.T) { mockStorage := mocks.NewMockStorage(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, @@ -523,14 +545,14 @@ func TestPrepareSolutionPackage_MixedCacheResults(t *testing.T) { mockCache := mocks.NewMockFileCache(ctrl) // prepare message - submission := messages.FileLocation{Bucket: "sub", Path: "solutions/1/main.cpp"} + submission := messages.FileLocation{Bucket: testSubmissionBucket, Path: testMainCppPath} tc := messages.TestCase{ Order: 1, - InputFile: messages.FileLocation{Bucket: "inputs", Path: "inputs/1/in.txt"}, - ExpectedOutput: messages.FileLocation{Bucket: "outputs", Path: "outputs/1/out.txt"}, - StdOutResult: messages.FileLocation{Bucket: "results", Path: "results/1/out.result"}, - StdErrResult: messages.FileLocation{Bucket: "results", Path: "results/1/err.result"}, - DiffResult: messages.FileLocation{Bucket: "results", Path: "results/1/diff.result"}, + InputFile: messages.FileLocation{Bucket: testInputsBucket, Path: testInputPath}, + ExpectedOutput: messages.FileLocation{Bucket: testOutputsBucket, Path: testOutputPath}, + StdOutResult: messages.FileLocation{Bucket: testResultsBucket, Path: testOutResultPath}, + StdErrResult: messages.FileLocation{Bucket: testResultsBucket, Path: testErrResultPath}, + DiffResult: messages.FileLocation{Bucket: testResultsBucket, Path: testDiffResultPath}, } msg := &messages.TaskQueueMessage{ SubmissionFile: submission, diff --git a/internal/stages/verifier/verifier_test.go b/internal/stages/verifier/verifier_test.go index 7fbeb75..83e455d 100644 --- a/internal/stages/verifier/verifier_test.go +++ b/internal/stages/verifier/verifier_test.go @@ -15,6 +15,8 @@ import ( "github.com/mini-maxit/worker/tests" ) +const testOutputFileName = "out.txt" + func TestEvaluateAllTestCases_AllPass(t *testing.T) { dir := t.TempDir() userOutDir := filepath.Join(dir, "userOut") @@ -24,8 +26,8 @@ func TestEvaluateAllTestCases_AllPass(t *testing.T) { execResDir := filepath.Join(dir, "execRes") // prepare files: expected and user output identical - tests.WriteFile(t, expectedOutDir, "out.txt", "hello\n") - tests.WriteFile(t, userOutDir, "out.txt", "hello\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "hello\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "hello\n") tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "0 0.100 0\n") cfg := &packager.TaskDirConfig{ @@ -39,8 +41,8 @@ func TestEvaluateAllTestCases_AllPass(t *testing.T) { ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}} + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}} res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg1", languages.PYTHON) if res.StatusCode != solution.Success { @@ -65,8 +67,8 @@ func TestEvaluateAllTestCases_OutputDifference(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "hello\n") - tests.WriteFile(t, userOutDir, "out.txt", "hi\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "hello\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "hi\n") tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "0 0.050 0\n") cfg := &packager.TaskDirConfig{ @@ -79,8 +81,8 @@ func TestEvaluateAllTestCases_OutputDifference(t *testing.T) { ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}} + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}} res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg2", languages.PYTHON) if res.StatusCode != solution.TestFailed { t.Fatalf("expected test failed, got: %v", res.StatusCode) @@ -105,8 +107,8 @@ func TestEvaluateAllTestCases_TimeAndMemoryAndRuntime(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "whatever\n") - tests.WriteFile(t, userOutDir, "out.txt", "whatever\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "whatever\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "whatever\n") // time limit exceeded (exit code 143) tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "143 0.0 0\n") @@ -119,8 +121,8 @@ func TestEvaluateAllTestCases_TimeAndMemoryAndRuntime(t *testing.T) { } ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}, TimeLimitMs: 5, } res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg3", languages.PYTHON) @@ -163,8 +165,8 @@ func setupMLETestCase( userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "output\n") - tests.WriteFile(t, userOutDir, "out.txt", "output\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "output\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "output\n") tests.WriteFile( t, execResDir, @@ -181,8 +183,8 @@ func setupMLETestCase( } tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}, MemoryLimitKB: 1000, } @@ -224,8 +226,8 @@ func TestMemoryLimitExceeded_LanguageSpecificPattern(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "output\n") - tests.WriteFile(t, userOutDir, "out.txt", "output\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "output\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "output\n") // exit code 1 (generic error) but stderr contains C++ memory limit error pattern tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "1 0.05 500\n") tests.WriteFile(t, userErrDir, "1.txt", "std::bad_alloc\n") @@ -240,8 +242,8 @@ func TestMemoryLimitExceeded_LanguageSpecificPattern(t *testing.T) { ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}, StdErrResult: messages.FileLocation{Path: "1.txt"}, MemoryLimitKB: 1000, } @@ -280,8 +282,8 @@ func TestMemoryLimitExceeded_SignalVariants(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "output\n") - tests.WriteFile(t, userOutDir, "out.txt", "output\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "output\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "output\n") tests.WriteFile( t, execResDir, @@ -299,8 +301,8 @@ func TestMemoryLimitExceeded_SignalVariants(t *testing.T) { ver := NewVerifier([]string{}) testCase := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}, MemoryLimitKB: 1000, } @@ -324,8 +326,8 @@ func TestEvaluateAllTestCases_CommandNotFound(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "result\n") - tests.WriteFile(t, userOutDir, "out.txt", "result\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "result\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "result\n") // exit code 127 (command not found, possibly due to memory limit preventing shared library mapping) tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "127 0.0 0\n") @@ -338,8 +340,8 @@ func TestEvaluateAllTestCases_CommandNotFound(t *testing.T) { } ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}, MemoryLimitKB: 1024, } res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg-cmd-not-found", languages.PYTHON) @@ -373,7 +375,7 @@ func TestEvaluateAllTestCases_CompareOutputFailure(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, userOutDir, "out.txt", "hello\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "hello\n") // Create expected output file but don't give read permissions (or reference non-existent file) // We'll use a path that references a file that doesn't exist tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "0 0.100 0\n") @@ -389,7 +391,7 @@ func TestEvaluateAllTestCases_CompareOutputFailure(t *testing.T) { ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, + StdOutResult: messages.FileLocation{Path: testOutputFileName}, ExpectedOutput: messages.FileLocation{Path: "nonexistent.txt"}} // File doesn't exist res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg-compare-fail", languages.PYTHON) @@ -421,8 +423,8 @@ func TestEvaluateAllTestCases_MissingExecResult(t *testing.T) { userErrDir := filepath.Join(dir, "userErr") execResDir := filepath.Join(dir, "execRes") - tests.WriteFile(t, expectedOutDir, "out.txt", "a\n") - tests.WriteFile(t, userOutDir, "out.txt", "a\n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "a\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "a\n") // do NOT create exec result files cfg := &packager.TaskDirConfig{ @@ -434,8 +436,8 @@ func TestEvaluateAllTestCases_MissingExecResult(t *testing.T) { } ver := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}} + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}} res := ver.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg6", languages.PYTHON) if res.StatusCode != solution.InternalError { t.Fatalf("expected internal error due to missing exec result, got: %v", res.StatusCode) @@ -454,8 +456,8 @@ func TestEvaluateAllTestCases_WithFlags_IgnoreWhitespace(t *testing.T) { execResDir := filepath.Join(dir, "execRes") // expected and user differ only by whitespace - tests.WriteFile(t, expectedOutDir, "out.txt", "hello\n") - tests.WriteFile(t, userOutDir, "out.txt", "hello \n") + tests.WriteFile(t, expectedOutDir, testOutputFileName, "hello\n") + tests.WriteFile(t, userOutDir, testOutputFileName, "hello \n") tests.WriteFile(t, execResDir, "1."+constants.ExecutionResultFileExt, "0 0.010 0\n") cfg := &packager.TaskDirConfig{ @@ -469,8 +471,8 @@ func TestEvaluateAllTestCases_WithFlags_IgnoreWhitespace(t *testing.T) { // without flags -> should detect difference verNoFlags := NewVerifier([]string{}) tc := messages.TestCase{ - StdOutResult: messages.FileLocation{Path: "out.txt"}, - ExpectedOutput: messages.FileLocation{Path: "out.txt"}} + StdOutResult: messages.FileLocation{Path: testOutputFileName}, + ExpectedOutput: messages.FileLocation{Path: testOutputFileName}} res := verNoFlags.EvaluateAllTestCases(cfg, []messages.TestCase{tc}, "msg-flags-1", languages.PYTHON) if res.StatusCode != solution.TestFailed { t.Fatalf("expected test failed without flags, got: %v", res.StatusCode) diff --git a/internal/storage/cache_test.go b/internal/storage/cache_test.go index 019a737..ce00429 100644 --- a/internal/storage/cache_test.go +++ b/internal/storage/cache_test.go @@ -13,6 +13,11 @@ import ( "github.com/stretchr/testify/require" ) +const ( + testBucketName = "test-bucket" + testBucketOne = "bucket1" +) + // createTestFile creates a temporary file with test content. func createTestFile(t *testing.T, content string) string { tempFile, err := os.CreateTemp(t.TempDir(), "test-file-*.txt") @@ -56,7 +61,7 @@ func TestFileCache_CacheFile(t *testing.T) { defer os.Remove(testFile) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/path/file.txt", } @@ -84,7 +89,7 @@ func TestFileCache_GetCachedFile_NotFound(t *testing.T) { require.NoError(t, err) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "nonexistent/file.txt", } @@ -107,7 +112,7 @@ func TestFileCache_GetCachedFile_Success(t *testing.T) { defer os.Remove(testFile) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/success.txt", } @@ -138,7 +143,7 @@ func TestFileCache_MultipleCacheEntries(t *testing.T) { content string }{ { - location: messages.FileLocation{Bucket: "bucket1", Path: "path1/file1.txt"}, + location: messages.FileLocation{Bucket: testBucketOne, Path: "path1/file1.txt"}, content: "content 1", }, { @@ -146,7 +151,7 @@ func TestFileCache_MultipleCacheEntries(t *testing.T) { content: "content 2", }, { - location: messages.FileLocation{Bucket: "bucket1", Path: "path3/file3.txt"}, + location: messages.FileLocation{Bucket: testBucketOne, Path: "path3/file3.txt"}, content: "content 3", }, } @@ -179,7 +184,7 @@ func TestFileCache_OverwriteExistingCache(t *testing.T) { require.NoError(t, err) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/overwrite.txt", } @@ -217,7 +222,7 @@ func TestFileCache_DifferentBucketsSamePath(t *testing.T) { // Cache file in bucket1 testFile1 := createTestFile(t, "bucket1 content") defer os.Remove(testFile1) - location1 := messages.FileLocation{Bucket: "bucket1", Path: samePath} + location1 := messages.FileLocation{Bucket: testBucketOne, Path: samePath} err = cache.CacheFile(location1, testFile1) require.NoError(t, err) @@ -261,7 +266,7 @@ func TestFileCache_CacheWithDifferentExtensions(t *testing.T) { for _, ext := range extensions { fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/file" + ext, } @@ -292,7 +297,7 @@ func TestFileCache_GetCachedFile_FileDeleted(t *testing.T) { defer os.Remove(testFile) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/deleted.txt", } @@ -323,7 +328,7 @@ func TestFileCache_CacheFile_InvalidSourcePath(t *testing.T) { require.NoError(t, err) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/invalid.txt", } @@ -345,7 +350,7 @@ func TestFileCache_CleanExpiredCache_NoExpiredEntries(t *testing.T) { defer os.Remove(testFile) fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/fresh.txt", } @@ -372,7 +377,7 @@ func TestFileCache_SpecialCharactersInPath(t *testing.T) { // File location with special characters fileLocation := messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: "test/файл с пробелами and special-chars_123.txt", } @@ -407,7 +412,7 @@ func TestFileCache_EvictionWhenFull(t *testing.T) { files := make([]messages.FileLocation, maxEntries+2) for i := range maxEntries + 2 { files[i] = messages.FileLocation{ - Bucket: "test-bucket", + Bucket: testBucketName, Path: fmt.Sprintf("test/file%d.txt", i), }