diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index 12d02b5661..77399d8bbc 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -129,6 +129,15 @@ services: - apps_network container_name: python-sandbox + node-sandbox: + build: + context: ./execution-service/execution/node + dockerfile: Dockerfile + networks: + - apps_network + container_name: node-sandbox + stdin_open: true # Enables interactive mode for passing standard input + networks: apps_network: diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index bce92e1b6f..b1629b0b08 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -160,6 +160,43 @@ The following json format will be returned: ] ``` +`POST /tests` + +To create a new test case, run the following command: + +```bash +curl -X POST http://localhost:8083/tests \ +-H "Content-Type: application/json" \ +-d '{ +"questionDocRefId": "sampleDocRefId123", +"questionTitle": "Sample Question Title", +"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +}' +``` + +`PUT /tests/{questionDocRefId}` + +To update an existing test case from an existing question, run the following command: + +```bash +curl -X PUT http://localhost:8083/tests/{questionDocRefId} \ +-H "Content-Type: application/json" \ +-d '{ +"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +}' +``` + +`DELETE /tests/{questionDocRefId}` + +To delete an existing test case from an existing question, run the following command: + +```bash +curl -X DELETE http://localhost:8083/tests/{questionDocRefId} \ +-H "Content-Type: application/json" +``` + `POST /tests/{questionDocRefId}/execute` To execute test cases via a question ID without custom test cases, run the following command, with custom code and language: diff --git a/apps/execution-service/constants/constant.go b/apps/execution-service/constants/constant.go index 46d3face08..1e3810a3e4 100644 --- a/apps/execution-service/constants/constant.go +++ b/apps/execution-service/constants/constant.go @@ -1,11 +1,11 @@ package constants const ( - JAVA = "Java" - PYTHON = "Python" - GOLANG = "Golang" - JAVASCRIPT = "Javascript" - CPP = "C++" + JAVA = "java" + PYTHON = "python" + GOLANG = "golang" + JAVASCRIPT = "javascript" + CPP = "c++" ) const ( @@ -17,6 +17,6 @@ var IS_VALID_LANGUAGE = map[string]bool{ PYTHON: true, //JAVA: true, //GOLANG: true, - //JAVASCRIPT: true, + JAVASCRIPT: true, //CPP: true, } diff --git a/apps/execution-service/execution/node/Dockerfile b/apps/execution-service/execution/node/Dockerfile new file mode 100644 index 0000000000..59ef6fcdee --- /dev/null +++ b/apps/execution-service/execution/node/Dockerfile @@ -0,0 +1,11 @@ +# Use a slim Node.js image +FROM node:18-slim + +# Set the working directory +WORKDIR /app + +# Install any dependencies if necessary (you can skip if no dependencies) +# COPY package*.json ./ +# RUN npm install + +# No entry point or CMD needed as you'll provide the command at runtime diff --git a/apps/execution-service/execution/node/javascript.go b/apps/execution-service/execution/node/javascript.go new file mode 100644 index 0000000000..c41c73c068 --- /dev/null +++ b/apps/execution-service/execution/node/javascript.go @@ -0,0 +1,33 @@ +package node + +import ( + "bytes" + "fmt" + "os/exec" + "strings" +) + +func RunJavaScriptCode(code string, input string) (string, string, error) { + cmd := exec.Command( + "docker", "run", "--rm", + "-i", // allows standard input to be passed in + "apps-node-sandbox", // Docker image with Node.js environment + "node", "-e", code, // Runs JavaScript code with Node.js + ) + + // Pass input to the JavaScript script + cmd.Stdin = bytes.NewBufferString(input) + + // Capture standard output and error output + var output bytes.Buffer + var errorOutput bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &errorOutput + + // Run the command + if err := cmd.Run(); err != nil { + return "", fmt.Sprintf("Command execution failed: %s: %v", errorOutput.String(), err), nil + } + + return strings.TrimSuffix(output.String(), "\n"), strings.TrimSuffix(errorOutput.String(), "\n"), nil +} diff --git a/apps/execution-service/go.mod b/apps/execution-service/go.mod index aff484d2a7..fa974a5550 100644 --- a/apps/execution-service/go.mod +++ b/apps/execution-service/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/joho/godotenv v1.5.1 + github.com/rabbitmq/amqp091-go v1.10.0 github.com/traefik/yaegi v0.16.1 google.golang.org/api v0.203.0 ) @@ -31,7 +32,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/apps/execution-service/go.sum b/apps/execution-service/go.sum index 8a595690fa..4733d88abb 100644 --- a/apps/execution-service/go.sum +++ b/apps/execution-service/go.sum @@ -111,6 +111,8 @@ go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHy go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= diff --git a/apps/execution-service/handlers/create.go b/apps/execution-service/handlers/create.go new file mode 100644 index 0000000000..6a0d9d0324 --- /dev/null +++ b/apps/execution-service/handlers/create.go @@ -0,0 +1,119 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "execution-service/models" + "execution-service/utils" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var test models.Test + if err := utils.DecodeJSONBody(w, r, &test); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Basic validation for question title and question ID + if test.QuestionDocRefId == "" || test.QuestionTitle == "" { + http.Error(w, "QuestionDocRefId and QuestionTitle are required", http.StatusBadRequest) + return + } + + // Normalise test cases + test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases) + test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases) + + // Automatically populate validation for input and output in test case + test.InputValidation = utils.GetDefaultValidation() + test.OutputValidation = utils.GetDefaultValidation() + + // Validate test case format + if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, test.InputValidation, + test.OutputValidation); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, test.InputValidation, + test.OutputValidation); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Check if a test already exists for the question + iter := s.Client.Collection("tests").Where("questionDocRefId", "==", test.QuestionDocRefId).Documents(ctx) + for { + _, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Error fetching test", http.StatusInternalServerError) + return + } + http.Error(w, "Test already exists for the question", http.StatusConflict) + return + } + defer iter.Stop() + + // Save test to Firestore + docRef, _, err := s.Client.Collection("tests").Add(ctx, map[string]interface{}{ + "questionDocRefId": test.QuestionDocRefId, + "questionTitle": test.QuestionTitle, + "visibleTestCases": test.VisibleTestCases, + "hiddenTestCases": test.HiddenTestCases, + "inputValidation": test.InputValidation, + "outputValidation": test.OutputValidation, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "Test not found", http.StatusInternalServerError) + return + } + http.Error(w, "Failed to get test", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(test) +} + +// Manual test cases + +//curl -X POST http://localhost:8083/tests \ +//-H "Content-Type: application/json" \ +//-d '{ +//"questionDocRefId": "sampleDocRefId123", +//"questionTitle": "Sample Question Title", +//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +//}' + +//curl -X POST http://localhost:8083/tests \ +//-H "Content-Type: application/json" \ +//-d "{ +//\"questionDocRefId\": \"sampleDocRefId12345\", +//\"questionTitle\": \"Sample Question Title\", +//\"visibleTestCases\": \"2\\nhello\\nolleh\\nHannah\\nhannaH\", +//\"hiddenTestCases\": \"2\\nHannah\\nhannaH\\nabcdefg\\ngfedcba\" +//}" diff --git a/apps/execution-service/handlers/delete.go b/apps/execution-service/handlers/delete.go new file mode 100644 index 0000000000..79b9987d3a --- /dev/null +++ b/apps/execution-service/handlers/delete.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) DeleteTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + docRefID := chi.URLParam(r, "questionDocRefId") + + docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", docRefID).Limit(1).Documents(ctx) + doc, err := docRef.Next() + if err != nil { + if err == iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer docRef.Stop() + + _, err = doc.Ref.Delete(ctx) + if err != nil { + http.Error(w, "Error deleting test", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +// Manual test cases + +//curl -X DELETE http://localhost:8083/tests/sampleDocRefId123 \ +//-H "Content-Type: application/json" diff --git a/apps/execution-service/handlers/readall.go b/apps/execution-service/handlers/readall.go new file mode 100644 index 0000000000..4b622d5294 --- /dev/null +++ b/apps/execution-service/handlers/readall.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "encoding/json" + "execution-service/models" + "execution-service/utils" + "net/http" + + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" +) + +func (s *Service) ReadAllTests(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + questionDocRefId := chi.URLParam(r, "questionDocRefId") + if questionDocRefId == "" { + http.Error(w, "questionDocRefId is required", http.StatusBadRequest) + return + } + + iter := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx) + doc, err := iter.Next() + if err != nil { + if err == iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer iter.Stop() + + var test models.Test + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, hiddenTestCases, err := utils.GetTestLengthAndUnexecutedCases(test.HiddenTestCases) + + var hiddenTests []models.HiddenTest + for _, hiddenTestCase := range hiddenTestCases { + hiddenTests = append(hiddenTests, models.HiddenTest{ + Input: hiddenTestCase.Input, + Expected: hiddenTestCase.Expected, + }) + } + + _, visibleTestCases, err := utils.GetTestLengthAndUnexecutedCases(test.VisibleTestCases) + + var visibleTests []models.VisibleTest + for _, visibleTestCase := range visibleTestCases { + visibleTests = append(visibleTests, models.VisibleTest{ + Input: visibleTestCase.Input, + Expected: visibleTestCase.Expected, + }) + } + + allTests := models.AllTests{ + VisibleTests: visibleTests, + HiddenTests: hiddenTests, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(allTests) +} + +//curl -X GET http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/ \ +//-H "Content-Type: application/json" diff --git a/apps/execution-service/handlers/read.go b/apps/execution-service/handlers/readvisible.go similarity index 100% rename from apps/execution-service/handlers/read.go rename to apps/execution-service/handlers/readvisible.go diff --git a/apps/execution-service/handlers/update.go b/apps/execution-service/handlers/update.go new file mode 100644 index 0000000000..b713a52c2c --- /dev/null +++ b/apps/execution-service/handlers/update.go @@ -0,0 +1,96 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "execution-service/models" + "execution-service/utils" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) UpdateTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // get param questionDocRefId + questionDocRefId := chi.URLParam(r, "questionDocRefId") + + var test models.Test + if err := utils.DecodeJSONBody(w, r, &test); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Normalise test cases + test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases) + test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases) + + // Only test cases will be updated + // Validate test case format with default validation + if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, utils.GetDefaultValidation(), + utils.GetDefaultValidation()); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, utils.GetDefaultValidation(), + utils.GetDefaultValidation()); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Update test in Firestore + docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx) + doc, err := docRef.Next() + if err != nil { + if err == iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer docRef.Stop() + + // Update database + updates := []firestore.Update{ + {Path: "visibleTestCases", Value: test.VisibleTestCases}, + {Path: "hiddenTestCases", Value: test.HiddenTestCases}, + {Path: "updatedAt", Value: firestore.ServerTimestamp}, + } + _, err = doc.Ref.Update(ctx, updates) + if err != nil { + http.Error(w, "Error updating test", http.StatusInternalServerError) + return + } + + // Get data + doc, err = doc.Ref.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get test", http.StatusInternalServerError) + return + } + + // Map data + if err = doc.DataTo(&test); err != nil { + http.Error(w, "Failed to map test data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(test) +} + +// Manual test cases + +//curl -X PUT http://localhost:8083/tests/sampleDocRefId123 \ +//-H "Content-Type: application/json" \ +//-d '{ +//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +//}' diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index e2da2948be..7bf004153f 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -83,16 +83,17 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { // Re: CreateTest // Current: Unused, since testcases are populated via script // Future extension: can be created by admin - //r.Post("/", service.CreateTest) + r.Post("/", service.CreateTest) r.Post("/populate", service.PopulateTests) r.Route("/{questionDocRefId}", func(r chi.Router) { // Re: UpdateTest, DeleteTest // Current: Unused, since testcases are executed within service and not exposed // Future extension: can be read by admin to view testcases - //r.Put("/", service.UpdateTest) - //r.Delete("/", service.DeleteTest) + r.Put("/", service.UpdateTest) + r.Delete("/", service.DeleteTest) r.Get("/", service.ReadVisibleTests) + r.Get("/readall", service.ReadAllTests) r.Post("/execute", service.ExecuteVisibleAndCustomTests) r.Post("/submit", service.ExecuteVisibleAndHiddenTestsAndSubmit) }) diff --git a/apps/execution-service/models/visibleTest.go b/apps/execution-service/models/visibleTest.go index 39c2bf48cb..2386df5dd2 100644 --- a/apps/execution-service/models/visibleTest.go +++ b/apps/execution-service/models/visibleTest.go @@ -4,3 +4,13 @@ type VisibleTest struct { Input string `json:"input"` Expected string `json:"expected"` } + +type HiddenTest struct { + Input string `json:"input"` + Expected string `json:"expected"` +} + +type AllTests struct { + VisibleTests []VisibleTest `json:"visibleTests"` + HiddenTests []HiddenTest `json:"hiddenTests"` +} diff --git a/apps/execution-service/utils/executeTest.go b/apps/execution-service/utils/executeTest.go index b411ba7298..f2bfd1cf50 100644 --- a/apps/execution-service/utils/executeTest.go +++ b/apps/execution-service/utils/executeTest.go @@ -2,6 +2,7 @@ package utils import ( "execution-service/constants" + "execution-service/execution/node" "execution-service/execution/python" "execution-service/models" "fmt" @@ -15,6 +16,8 @@ func ExecuteVisibleAndCustomTests(code models.Code, test models.Test) (models.Ex case constants.PYTHON: testResults, err = getVisibleAndCustomTestResults(code, test, python.RunPythonCode) break + case constants.JAVASCRIPT: + testResults, err = getVisibleAndCustomTestResults(code, test, node.RunJavaScriptCode) default: return models.ExecutionResults{}, fmt.Errorf("unsupported language: %s", code.Language) } @@ -33,6 +36,8 @@ func ExecuteVisibleAndHiddenTests(code models.Code, test models.Test) (models.Su case constants.PYTHON: testResults, err = getVisibleAndHiddenTestResults(code, test, python.RunPythonCode) break + case constants.JAVASCRIPT: + testResults, err = getVisibleAndHiddenTestResults(code, test, node.RunJavaScriptCode) default: return models.SubmissionResults{}, fmt.Errorf("unsupported language: %s", code.Language) } diff --git a/apps/execution-service/utils/populate.go b/apps/execution-service/utils/populate.go index fc23098712..767b565af2 100644 --- a/apps/execution-service/utils/populate.go +++ b/apps/execution-service/utils/populate.go @@ -673,3 +673,9 @@ func validateInputOrOutput(inputOrOutput string) bool { } `, importCode.String(), functionBody) } + +func GetDefaultValidation() string { + return getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`) +} diff --git a/apps/execution-service/utils/validateTestCaseFormat.go b/apps/execution-service/utils/validateTestCaseFormat.go index e38e0881b4..5410a0e4f0 100644 --- a/apps/execution-service/utils/validateTestCaseFormat.go +++ b/apps/execution-service/utils/validateTestCaseFormat.go @@ -77,3 +77,7 @@ package main result := validateFunc.Interface().(func(string) bool)(inputOrOutput) return result, nil } + +func NormaliseTestCaseFormat(testCase string) string { + return strings.ReplaceAll(testCase, "\\n", "\n") +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 72690ce9c6..3f1aaa4c0b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -34,6 +34,7 @@ "react-use-websocket": "^4.9.0", "sass": "^1.79.2", "typeface-montserrat": "^1.1.13", + "uuid": "^11.0.3", "y-codemirror.next": "^0.3.5", "y-webrtc": "^10.3.0", "yjs": "^13.6.20" @@ -59,4 +60,4 @@ "typescript": "^5" }, "packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0" -} +} \ No newline at end of file diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index 13ea2f0ed5..2462485e68 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: typeface-montserrat: specifier: ^1.1.13 version: 1.1.13 + uuid: + specifier: ^11.0.3 + version: 11.0.3 y-codemirror.next: specifier: ^0.3.5 version: 0.3.5(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(yjs@13.6.20) @@ -3140,6 +3143,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4955,8 +4962,8 @@ snapshots: '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.0.0) eslint-plugin-react: 7.37.1(eslint@8.0.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.0.0) @@ -4975,7 +4982,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 @@ -4987,7 +4994,7 @@ snapshots: is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -5001,11 +5008,11 @@ snapshots: '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7096,6 +7103,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.0.3: {} + v8-compile-cache-lib@3.0.1: {} v8-compile-cache@2.4.0: {} diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 4def83a20a..1ed32103aa 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -71,7 +71,7 @@ export default function CollaborationPage(props: CollaborationProps) { const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); - const [selectedLanguage, setSelectedLanguage] = useState("Python"); // State to hold the selected language item + const [selectedLanguage, setSelectedLanguage] = useState("python"); // State to hold the selected language item // Session states const [collaborationId, setCollaborationId] = useState( @@ -232,6 +232,10 @@ export default function CollaborationPage(props: CollaborationProps) { localStorage.setItem("visibleTestResults", JSON.stringify(data.visibleTestResults)); }; + const updateLangauge = (data: string) => { + setSelectedLanguage(data); + } + const handleRunTestCases = async () => { if (!questionDocRefId) { throw new Error("Question ID not found"); @@ -511,7 +515,7 @@ export default function CollaborationPage(props: CollaborationProps) { ref={editorRef} user={currentUser} collaborationId={collaborationId} - language={selectedLanguage} + updateLanguage={updateLangauge} setMatchedUser={setMatchedUser} handleCloseCollaboration={handleCloseCollaboration} providerRef={providerRef} diff --git a/apps/frontend/src/app/question/page.tsx b/apps/frontend/src/app/question/page.tsx index cb88d0bf3f..3e1e1048f0 100644 --- a/apps/frontend/src/app/question/page.tsx +++ b/apps/frontend/src/app/question/page.tsx @@ -14,12 +14,18 @@ import { Tag, Modal, Form, + Tabs, + Checkbox, + Tooltip, + Card, + Spin, } from "antd"; import { Content } from "antd/es/layout/layout"; import { DeleteOutlined, EditOutlined, PlusCircleOutlined, + PlusOutlined, SearchOutlined, } from "@ant-design/icons"; import "./styles.scss"; @@ -40,6 +46,15 @@ import { import Link from "next/link"; import TextArea from "antd/es/input/TextArea"; import { ValidateUser, VerifyTokenResponseType } from "../services/user"; +import TabPane from "antd/es/tabs/TabPane"; +import { + CreateTestcases, + DeleteTestcases, + ReadAllTestcases, + TestData, + UpdateTestcases, +} from "../services/execute"; +import { v4 as uuidv4 } from "uuid"; /** * defines the State of the page whe a user is deleing an object. Has 3 general states: @@ -49,6 +64,10 @@ import { ValidateUser, VerifyTokenResponseType } from "../services/user"; */ type DeletionStage = {} | { index: Question; deleteConfirmed: boolean }; +type Test = TestData & { + key?: string; +}; + function DeleteModal({ isDeleting, questionTitle, @@ -120,13 +139,14 @@ export default function QuestionListPage() { const [isAdmin, setIsAdmin] = useState(undefined); useLayoutEffect(() => { - ValidateUser() - .then((data: VerifyTokenResponseType) => { - setIsAdmin(data.data.isAdmin); - }) + ValidateUser().then((data: VerifyTokenResponseType) => { + setIsAdmin(data.data.isAdmin); + }); }, []); const handleEditClick = (index: number, question: Question) => { + fetchTestsForQuestion(question.docRefId); + // Open the modal for the specific question const updatedModals = isEditModalOpen && isEditModalOpen.map((_, idx) => idx === index); @@ -156,7 +176,18 @@ export default function QuestionListPage() { docRefId: string ) => { try { - const editedQuestion = await EditQuestion(values, docRefId); + const editedQuestion = await EditQuestion( + { + title: values.title, + description: values.description, + categories: values.categories, + complexity: values.complexity, + }, + docRefId + ); + + await UpdateTestcases(docRefId, visibleTests, hiddenTests); + // Reset form or update UI as needed handleModalClose(index); editForm.resetFields(); @@ -169,7 +200,20 @@ export default function QuestionListPage() { const handleCreateQuestion = async (values: NewQuestion) => { try { - const createdQuestion = await CreateQuestion(values); + const createdQuestion = await CreateQuestion({ + title: values.title, + description: values.description, + categories: values.categories, + complexity: values.complexity, + }); + + await CreateTestcases( + createdQuestion.docRefId, + createdQuestion.title, + visibleTests, + hiddenTests + ); + // Reset form or update UI as needed setIsNewProblemModelOpen(false); form.resetFields(); @@ -181,6 +225,8 @@ export default function QuestionListPage() { }; const showNewProblemModal = () => { + setVisibleTests([{ key: uuidv4(), input: "", expected: "" }]); + setHiddenTests([]); setIsNewProblemModelOpen(true); }; @@ -248,6 +294,100 @@ export default function QuestionListPage() { return () => clearTimeout(timeout); }, [search]); + // Testcases + + const [visibleTests, setVisibleTests] = useState([]); + const [hiddenTests, setHiddenTests] = useState([]); + const [isTestsLoading, setIsTestsLoading] = useState(true); + const [activeKey, setActiveKey] = useState(undefined); + const [testActiveKey, setTestActiveKey] = useState("1"); + + const handleAddVisibleTest = () => { + const newKey = uuidv4(); + setVisibleTests([ + ...visibleTests, + { key: newKey, input: "", expected: "" }, + ]); + setActiveKey(newKey); + }; + + const handleAddHiddenTest = () => { + const newKey = uuidv4(); + setHiddenTests([...hiddenTests, { key: newKey, input: "", expected: "" }]); + setActiveKey(newKey); + }; + + const handleRemoveVisibleTest = (targetKey: string) => { + if (visibleTests.length > 1) { + setVisibleTests( + visibleTests.filter((test: Test) => test.key !== targetKey) + ); + } + }; + + const handleRemoveHiddenTest = (targetKey: string) => { + setHiddenTests(hiddenTests.filter((test: Test) => test.key !== targetKey)); + }; + + const handleTestChange = ( + type: string, + index: number, + input?: string, + expected?: string + ) => { + // Determine which array to update based on the type (visible or hidden) + if (type === "visible") { + const updatedTests = [...visibleTests]; + updatedTests[index].input = input ?? updatedTests[index].input; + updatedTests[index].expected = expected ?? updatedTests[index].expected; + setVisibleTests(updatedTests); + } else if (type === "hidden") { + const updatedTests = [...hiddenTests]; + updatedTests[index].input = input ?? updatedTests[index].input; + updatedTests[index].expected = expected ?? updatedTests[index].expected; + setHiddenTests(updatedTests); + } + }; + + async function fetchTestsForQuestion(questionId: string) { + setIsTestsLoading(true); + ReadAllTestcases(questionId) + .then((response) => { + const { visibleTests, hiddenTests } = response; + + // Add a unique key to each test + if (visibleTests) { + const keyedVisibleTests = visibleTests.map((test, index) => ({ + ...test, + key: uuidv4(), + })); + setVisibleTests(keyedVisibleTests); + setActiveKey(keyedVisibleTests[0].key); + setTestActiveKey("1"); + } + if (hiddenTests) { + setHiddenTests( + hiddenTests.map((test) => ({ + ...test, + key: uuidv4(), + })) + ); + } + }) + .catch((err) => { + error("Error fetching tests for question."); + }) + .finally(() => { + setIsTestsLoading(false); + }); + } + + async function deleteTestsByDocRefId(docRefId: string) { + await DeleteTestcases(docRefId).catch((e) => { + error("Error deleting tests associated with the question."); + }); + } + // Table column specification var columns: TableProps["columns"]; if (isAdmin) { @@ -343,7 +483,7 @@ export default function QuestionListPage() { }, ]} > -