From b23580aa608e2c3b09aba388baa1ae228faa8e70 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Fri, 1 Nov 2024 22:43:02 +0800 Subject: [PATCH 01/16] Add code execution service --- apps/docker-compose.yml | 13 + apps/execution-service/.dockerignore | 9 + apps/execution-service/.gitignore | 2 + apps/execution-service/Dockerfile | 18 + apps/execution-service/README.md | 88 +++ apps/execution-service/enums/language.go | 10 + .../execution/python/python.go | 44 ++ apps/execution-service/go.mod | 53 ++ apps/execution-service/go.sum | 197 ++++++ apps/execution-service/handlers/execute.go | 68 ++ apps/execution-service/handlers/populate.go | 31 + apps/execution-service/handlers/read.go | 54 ++ apps/execution-service/handlers/server.go | 9 + apps/execution-service/main.go | 112 ++++ apps/execution-service/models/code.go | 7 + apps/execution-service/models/question.go | 6 + apps/execution-service/models/test.go | 18 + apps/execution-service/models/testResult.go | 20 + apps/execution-service/models/visibleTest.go | 6 + apps/execution-service/test.go | 59 ++ apps/execution-service/utils/decode.go | 20 + apps/execution-service/utils/executeTest.go | 179 +++++ apps/execution-service/utils/populate.go | 634 ++++++++++++++++++ apps/execution-service/utils/testCase.go | 80 +++ .../utils/validateTestCaseFormat.go | 87 +++ apps/matching-service/.dockerignore | 9 + apps/question-service/utils/populate.go | 72 +- 27 files changed, 1901 insertions(+), 4 deletions(-) create mode 100644 apps/execution-service/.dockerignore create mode 100644 apps/execution-service/.gitignore create mode 100644 apps/execution-service/Dockerfile create mode 100644 apps/execution-service/README.md create mode 100644 apps/execution-service/enums/language.go create mode 100644 apps/execution-service/execution/python/python.go create mode 100644 apps/execution-service/go.mod create mode 100644 apps/execution-service/go.sum create mode 100644 apps/execution-service/handlers/execute.go create mode 100644 apps/execution-service/handlers/populate.go create mode 100644 apps/execution-service/handlers/read.go create mode 100644 apps/execution-service/handlers/server.go create mode 100644 apps/execution-service/main.go create mode 100644 apps/execution-service/models/code.go create mode 100644 apps/execution-service/models/question.go create mode 100644 apps/execution-service/models/test.go create mode 100644 apps/execution-service/models/testResult.go create mode 100644 apps/execution-service/models/visibleTest.go create mode 100644 apps/execution-service/test.go create mode 100644 apps/execution-service/utils/decode.go create mode 100644 apps/execution-service/utils/executeTest.go create mode 100644 apps/execution-service/utils/populate.go create mode 100644 apps/execution-service/utils/testCase.go create mode 100644 apps/execution-service/utils/validateTestCaseFormat.go create mode 100644 apps/matching-service/.dockerignore diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index 0a0ef34fdf..32b41aa39e 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -67,6 +67,19 @@ services: volumes: - ./history-service:/history-service + execution-service: + build: + context: ./execution-service + dockerfile: Dockerfile + ports: + - 8083:8083 + env_file: + - ./execution-service/.env + networks: + - apps_network + volumes: + - ./execution-service:/execution-service + redis: image: redis:latest networks: diff --git a/apps/execution-service/.dockerignore b/apps/execution-service/.dockerignore new file mode 100644 index 0000000000..c6228e8d6c --- /dev/null +++ b/apps/execution-service/.dockerignore @@ -0,0 +1,9 @@ +.env.example + +.git +.gitignore + +.dockerignore +Dockerfile + +README.md diff --git a/apps/execution-service/.gitignore b/apps/execution-service/.gitignore new file mode 100644 index 0000000000..ef986d267e --- /dev/null +++ b/apps/execution-service/.gitignore @@ -0,0 +1,2 @@ +.env +cs3219-staging-codeexecution-firebase-adminsdk-ce48j-00ab09514c.json diff --git a/apps/execution-service/Dockerfile b/apps/execution-service/Dockerfile new file mode 100644 index 0000000000..0cabe00edd --- /dev/null +++ b/apps/execution-service/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.23 + +WORKDIR /usr/src/app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ + +RUN go mod tidy && go mod download && go mod verify + +COPY .env /usr/src/app/.env + +COPY . . + +RUN go build -v -o /usr/local/bin/app ./main.go + +EXPOSE 8083 8083 + +CMD ["app"] diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md new file mode 100644 index 0000000000..eaa740b179 --- /dev/null +++ b/apps/execution-service/README.md @@ -0,0 +1,88 @@ +# Execution Service + +## Overview + +The Execution Service is built with Go, utilizing Firestore as the database and Chi as the HTTP router. It provides an API to manage test cases, such as populating test cases (via question-service), reading visible test cases and executing visible, hidden and custom test cases. + +## Features + +- Populate test cases (via populate questins in question-service) +- Read visible test cases via a question ID +- Execute visible test cases via a question ID + +## Technologies Used + +- Go (Golang) +- Firestore (Google Cloud Firestore) +- Chi (HTTP router) +- Yaegi (Go interpreter) + +## Getting Started + +### Prerequisites + +- Go 1.16 or later +- Google Cloud SDK +- Firestore database setup in your Google Cloud project + +### Installation + +1. Clone the repository + +2. Set up your Firestore client + +3. Install dependencies: + +```bash +go mod tidy +``` + +4. Create the `.env` file from copying the `.env.example`, and copy the firebase JSON file into execution-service/ fill in the `FIREBASE_CREDENTIAL_PATH` with the path of the firebase credential JSON file. + +### Running the Application + +To start the server, run the following command: + +```bash +go run main.go +``` + +The server will be available at http://localhost:8083. + +## Running the Application via Docker + +To run the application via Docker, run the following command: + +```bash +docker build -t question-service . +``` + +```bash +docker run -p 8083:8083 --env-file .env -d execution-service +``` + +The server will be available at http://localhost:8083. + +## API Endpoints + +- `POST /tests/populate` +- `GET /tests/{questionDocRefId}/` +- `GET /tests/{questionDocRefId}/execute` + +## Managing Firebase + +To reset and repopulate the database, run the following command: + +```bash +go run main.go +``` + +## Repopulate test cases + +To repopulate test cases, you need to repopulate the questions in the question-service, which will automatically call the execution-service to populate the test cases. + +In question-service, run the following command: + +```bash +go run main.go -populate +``` diff --git a/apps/execution-service/enums/language.go b/apps/execution-service/enums/language.go new file mode 100644 index 0000000000..7f40b81891 --- /dev/null +++ b/apps/execution-service/enums/language.go @@ -0,0 +1,10 @@ +package enums + +// Create enums of languages +const ( + JAVA = "Java" + PYTHON = "Python" + GOLANG = "Golang" + JAVASCRIPT = "Javascript" + CPP = "C++" +) diff --git a/apps/execution-service/execution/python/python.go b/apps/execution-service/execution/python/python.go new file mode 100644 index 0000000000..a8f89316fc --- /dev/null +++ b/apps/execution-service/execution/python/python.go @@ -0,0 +1,44 @@ +package python + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" +) + +// RunPythonCode executes the provided Python code with the given input +func RunPythonCode(code string, input string) (string, string, error) { + // Create a temporary Python file to execute + tmpFile, err := os.CreateTemp("", "*.py") + if err != nil { + return "", "", fmt.Errorf("failed to create temporary file: %w", err) + } + defer os.Remove(tmpFile.Name()) // Clean up the temporary file afterwards + + // Write the provided code to the temporary file + if _, err := tmpFile.WriteString(code); err != nil { + return "", "", fmt.Errorf("failed to write code to temporary file: %w", err) + } + if err := tmpFile.Close(); err != nil { + return "", "", fmt.Errorf("failed to close temporary file: %w", err) + } + + // Prepare the command to execute the Python script + cmd := exec.Command("python3", tmpFile.Name()) + cmd.Stdin = bytes.NewBufferString(input) + + // Capture the output and error + 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: %w", 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 new file mode 100644 index 0000000000..32a516cf23 --- /dev/null +++ b/apps/execution-service/go.mod @@ -0,0 +1,53 @@ +module execution-service + +go 1.23.1 + +require ( + cloud.google.com/go/firestore v1.17.0 + firebase.google.com/go/v4 v4.15.0 + 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/traefik/yaegi v0.16.1 + google.golang.org/api v0.203.0 +) + +require ( + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.9.9 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/longrunning v0.6.1 // indirect + cloud.google.com/go/storage v1.43.0 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.8 // indirect + 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 + 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 + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/appengine/v2 v2.0.2 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/apps/execution-service/go.sum b/apps/execution-service/go.sum new file mode 100644 index 0000000000..7d3c75e147 --- /dev/null +++ b/apps/execution-service/go.sum @@ -0,0 +1,197 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= +cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= +cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= +firebase.google.com/go/v4 v4.15.0 h1:k27M+cHbyN1YpBI2Cf4NSjeHnnYRB9ldXwpqA5KikN0= +firebase.google.com/go/v4 v4.15.0/go.mod h1:S/4MJqVZn1robtXkHhpRUbwOC4gdYtgsiMMJQ4x+xmQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= +github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +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= +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= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/apps/execution-service/handlers/execute.go b/apps/execution-service/handlers/execute.go new file mode 100644 index 0000000000..e6cad62229 --- /dev/null +++ b/apps/execution-service/handlers/execute.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "encoding/json" + "execution-service/models" + "execution-service/utils" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) ExecuteTest(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 + } + + var code models.Code + if err := utils.DecodeJSONBody(w, r, &code); err != nil { + http.Error(w, err.Error(), 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 + } + + var test models.Test + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + testResults, err := utils.ExecuteTest(code, test) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(testResults) +} + +//curl -X POST http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/execute \ +//-H "Content-Type: application/json" \ +//-d '{ +//"code": "name = input()\nprint(name[::-1])", +//"language": "Python" +//}' + +//curl -X POST http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/execute \ +//-H "Content-Type: application/json" \ +//-d '{ +//"code": "name = input()\nprint(name[::-1])", +//"language": "Python", +//"customTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba\n" +//}' diff --git a/apps/execution-service/handlers/populate.go b/apps/execution-service/handlers/populate.go new file mode 100644 index 0000000000..014b4fe760 --- /dev/null +++ b/apps/execution-service/handlers/populate.go @@ -0,0 +1,31 @@ +package handlers + +import ( + "execution-service/models" + "execution-service/utils" + "net/http" +) + +func (s *Service) PopulateTests(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var questions []models.Question + if err := utils.DecodeJSONBody(w, r, &questions); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + questionTitleToDocRefIdMap := make(map[string]string) + for _, question := range questions { + questionTitleToDocRefIdMap[question.Title] = question.QuestionDocRefId + } + + err := utils.RepopulateTests(ctx, s.Client, questionTitleToDocRefIdMap) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} diff --git a/apps/execution-service/handlers/read.go b/apps/execution-service/handlers/read.go new file mode 100644 index 0000000000..db7fd4a21a --- /dev/null +++ b/apps/execution-service/handlers/read.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "encoding/json" + "execution-service/models" + "execution-service/utils" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) ReadTest(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 + } + + var test models.Test + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, 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, + }) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(visibleTests) +} + +//curl -X GET http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/ \ +//-H "Content-Type: application/json" diff --git a/apps/execution-service/handlers/server.go b/apps/execution-service/handlers/server.go new file mode 100644 index 0000000000..2d237dc025 --- /dev/null +++ b/apps/execution-service/handlers/server.go @@ -0,0 +1,9 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" +) + +type Service struct { + Client *firestore.Client +} diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go new file mode 100644 index 0000000000..e9991c5f82 --- /dev/null +++ b/apps/execution-service/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "execution-service/handlers" + "fmt" + "log" + "net/http" + "os" + "time" + + "cloud.google.com/go/firestore" + firebase "firebase.google.com/go/v4" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + "google.golang.org/api/option" +) + +func main() { + // Load .env file + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + // Initialize Firestore client + ctx := context.Background() + client, err := initFirestore(ctx) + if err != nil { + log.Fatalf("Failed to initialize Firestore client: %v", err) + } + defer client.Close() + + service := &handlers.Service{Client: client} + + r := initChiRouter(service) + initRestServer(r) +} + +// initFirestore initializes the Firestore client +func initFirestore(ctx context.Context) (*firestore.Client, error) { + credentialsPath := os.Getenv("FIREBASE_CREDENTIAL_PATH") + opt := option.WithCredentialsFile(credentialsPath) + app, err := firebase.NewApp(ctx, nil, opt) + if err != nil { + return nil, fmt.Errorf("failed to initialize Firebase App: %v", err) + } + + client, err := app.Firestore(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get Firestore client: %v", err) + } + return client, nil +} + +func initChiRouter(service *handlers.Service) *chi.Mux { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Timeout(60 * time.Second)) + + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"http://localhost:3000"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + registerRoutes(r, service) + + return r +} + +func registerRoutes(r *chi.Mux, service *handlers.Service) { + r.Route("/tests", func(r chi.Router) { + // Re: CreateTest + // Current: Unused, since testcases are populated via script + // Future extension: can be created by admin + //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.Get("/", service.ReadTest) + r.Post("/execute", service.ExecuteTest) + }) + }) +} + +func initRestServer(r *chi.Mux) { + // Serve on port 8080 + port := os.Getenv("PORT") + if port == "" { + port = "8083" + } + + // Start the server + log.Printf("Starting REST server on http://localhost:%s", port) + err := http.ListenAndServe(fmt.Sprintf(":%s", port), r) + if err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/apps/execution-service/models/code.go b/apps/execution-service/models/code.go new file mode 100644 index 0000000000..bd8ab16f84 --- /dev/null +++ b/apps/execution-service/models/code.go @@ -0,0 +1,7 @@ +package models + +type Code struct { + Code string `json:"code"` + Language string `json:"language"` + CustomTestCases string `json:"customTestCases"` +} diff --git a/apps/execution-service/models/question.go b/apps/execution-service/models/question.go new file mode 100644 index 0000000000..1a470c1839 --- /dev/null +++ b/apps/execution-service/models/question.go @@ -0,0 +1,6 @@ +package models + +type Question struct { + QuestionDocRefId string `json:"docRefId"` + Title string `json:"title"` +} diff --git a/apps/execution-service/models/test.go b/apps/execution-service/models/test.go new file mode 100644 index 0000000000..9b6c6d80ac --- /dev/null +++ b/apps/execution-service/models/test.go @@ -0,0 +1,18 @@ +package models + +import "time" + +type Test struct { + QuestionDocRefId string `json:"questionDocRefId" firestore:"questionDocRefId"` + VisibleTestCases string `json:"visibleTestCases" firestore:"visibleTestCases"` + HiddenTestCases string `json:"hiddenTestCases" firestore:"hiddenTestCases"` + InputValidation string `json:"inputValidation" firestore:"inputValidation"` + OutputValidation string `json:"outputValidation" firestore:"outputValidation"` + + // Special DB fields + CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` + + // Not stored in DB but used by json decoder + QuestionTitle string `json:"questionTitle" firestore:"questionTitle"` +} diff --git a/apps/execution-service/models/testResult.go b/apps/execution-service/models/testResult.go new file mode 100644 index 0000000000..df60799cd1 --- /dev/null +++ b/apps/execution-service/models/testResult.go @@ -0,0 +1,20 @@ +package models + +type TestResults struct { + VisibleTestResults []IndividualTestResult `json:"visibleTestResults"` + HiddenTestResults GeneralTestResults `json:"hiddenTestResults"` + CustomTestResults []IndividualTestResult `json:"customTestResults"` +} + +type IndividualTestResult struct { + Input string `json:"input"` + Expected string `json:"expected"` + Actual string `json:"actual"` + Passed bool `json:"passed"` + Error string `json:"error,omitempty"` +} + +type GeneralTestResults struct { + Passed int `json:"passed"` + Total int `json:"total"` +} diff --git a/apps/execution-service/models/visibleTest.go b/apps/execution-service/models/visibleTest.go new file mode 100644 index 0000000000..39c2bf48cb --- /dev/null +++ b/apps/execution-service/models/visibleTest.go @@ -0,0 +1,6 @@ +package models + +type VisibleTest struct { + Input string `json:"input"` + Expected string `json:"expected"` +} diff --git a/apps/execution-service/test.go b/apps/execution-service/test.go new file mode 100644 index 0000000000..8af33b0676 --- /dev/null +++ b/apps/execution-service/test.go @@ -0,0 +1,59 @@ +package main + +import ( + "execution-service/execution/python" +) + +func main() { + println("testing: ") + output, errorOutput, _ := test() + println("output: ", output) + println("errorOutput: ", errorOutput) +} + +//func test() bool { +// inputOrOutput := `[]` +// +// output := inputOrOutput +// +// if output == "[]" { +// return true +// } +// +// // Check that the output is enclosed in square brackets +// if len(output) < 2 || output[0] != '[' || output[len(output)-1] != ']' { +// return false +// } +// +// // Extract the content between square brackets +// content := output[1 : len(output)-1] +// +// // Split by commas without trimming spaces +// sequences := strings.Split(content, ", ") +// for _, seq := range sequences { +// // Check if each sequence is properly enclosed in double quotes and is exactly 10 characters +// if len(seq) != 12 || seq[0] != '"' || seq[11] != '"' { +// return false +// } +// +// // Check that the sequence only contains valid DNA characters between the quotes +// for i := 1; i < 11; i++ { +// ch := seq[i] +// if ch != 'A' && ch != 'C' && ch != 'G' && ch != 'T' { +// return false +// } +// } +// } +// return true +//} + +func test() (string, string, error) { + code := ` +nam = input() +print(name[::-1]) +` + + input := "hello" + + return python.RunPythonCode(code, input) +} diff --git a/apps/execution-service/utils/decode.go b/apps/execution-service/utils/decode.go new file mode 100644 index 0000000000..981df9a617 --- /dev/null +++ b/apps/execution-service/utils/decode.go @@ -0,0 +1,20 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// DecodeJSONBody decodes the request body into the given destination +func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error { + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&dst) + if err != nil { + return fmt.Errorf("Invalid request payload: " + err.Error()) + } + + return nil +} diff --git a/apps/execution-service/utils/executeTest.go b/apps/execution-service/utils/executeTest.go new file mode 100644 index 0000000000..cdc3853777 --- /dev/null +++ b/apps/execution-service/utils/executeTest.go @@ -0,0 +1,179 @@ +package utils + +import ( + "execution-service/enums" + "execution-service/execution/python" + "execution-service/models" + "fmt" +) + +func ExecuteTest(code models.Code, test models.Test) (models.TestResults, error) { + var err error + var testResults models.TestResults + + switch code.Language { + case enums.PYTHON: + testResults, err = getTestResultFromTest(code, test, python.RunPythonCode) + break + default: + return models.TestResults{}, fmt.Errorf("unsupported language: %s", code.Language) + } + if err != nil { + return models.TestResults{}, err + } + + return testResults, nil +} + +//func getVisibleTestResultsWithCompilationError(test models.Test, +// testCaseErrorStr string) ([]models.IndividualTestResult, error) { +// _, visibleTestResults, err := GetTestLengthAndUnexecutedCases(test.VisibleTestCases) +// if err != nil { +// return nil, err +// } +// +// for _, visibleTestResult := range visibleTestResults { +// visibleTestResult.Actual = "" +// visibleTestResult.Passed = false +// visibleTestResult.Error = testCaseErrorStr +// } +// +// return visibleTestResults, nil +//} +// +//func getHiddenTestResultsWithCompilationError(test models.Test) (models.GeneralTestResults, error) { +// numHiddenTests, err := GetTestLength(test.HiddenTestCases) +// if err != nil { +// return models.GeneralTestResults{}, err +// } +// +// return models.GeneralTestResults{ +// Passed: 0, +// Total: numHiddenTests, +// }, nil +//} + +func getIndividualTestResultFromCodeExecutor(code string, unexecutedTestResult models.IndividualTestResult, + executor func(string, string) (string, string, error)) (models.IndividualTestResult, error) { + output, outputErr, err := executor(code, unexecutedTestResult.Input) + if err != nil { + return models.IndividualTestResult{}, err + } + return models.IndividualTestResult{ + Input: unexecutedTestResult.Input, + Expected: unexecutedTestResult.Expected, + Actual: output, + Passed: output == unexecutedTestResult.Expected, + Error: outputErr, + }, nil +} + +func getAllTestResultsFromFormattedTestCase(code string, testCase string, + executor func(string, string) (string, string, error)) ([]models.IndividualTestResult, error) { + _, testResults, err := GetTestLengthAndUnexecutedCases(testCase) + if err != nil { + return nil, err + } + + for i := range testResults { + currTestResult, err := getIndividualTestResultFromCodeExecutor(code, testResults[i], executor) + if err != nil { + return nil, err + } + testResults[i].Actual = currTestResult.Actual + testResults[i].Passed = currTestResult.Passed + testResults[i].Error = currTestResult.Error + } + + return testResults, nil +} + +func getGenericTestResultsFromFormattedTestCase(code string, testCase string, + executor func(string, string) (string, string, error)) (models.GeneralTestResults, error) { + testResults, err := getAllTestResultsFromFormattedTestCase(code, testCase, executor) + if err != nil { + return models.GeneralTestResults{}, err + } + + var passed int + for _, testResult := range testResults { + if testResult.Passed { + passed++ + } + } + + return models.GeneralTestResults{ + Passed: passed, + Total: len(testResults), + }, nil +} + +func getTestResultFromTest(code models.Code, test models.Test, + executor func(string, string) (string, string, error)) (models.TestResults, error) { + visibleTestResults, err := getAllTestResultsFromFormattedTestCase(code.Code, test.VisibleTestCases, executor) + if err != nil { + return models.TestResults{}, err + } + + hiddenTestResults, err := getGenericTestResultsFromFormattedTestCase(code.Code, test.HiddenTestCases, executor) + if err != nil { + return models.TestResults{}, err + } + + var customTestResults []models.IndividualTestResult + if code.CustomTestCases != "" { + customTestResults, err = getAllTestResultsFromFormattedTestCase(code.Code, code.CustomTestCases, executor) + if err != nil { + return models.TestResults{}, err + } + } + + return models.TestResults{ + VisibleTestResults: visibleTestResults, + HiddenTestResults: hiddenTestResults, + CustomTestResults: customTestResults, + }, nil +} + +//func getVisibleTestResults(code string, test models.Test) ([]models.IndividualTestResult, error) { +// _, visibleTestResults, err := GetTestLengthAndUnexecutedCases(test.VisibleTestCases) +// if err != nil { +// return nil, err +// } +// +// // Initialize Yaegi interpreter +// i := interp.New(interp.Options{}) +// i.Use(stdlib.Symbols) +// +// _, err = i.Eval(code) +// if err != nil { +// return nil, fmt.Errorf("error evaluating code: %v", err) +// } +// +// // Execute each test case +// for _, visibleTestResult := range visibleTestResults { +// // Create an output buffer to capture stdout +// var stdout bytes.Buffer +// i.Stdout = &stdout +// +// // Set up the input for the test case +// input := strings.NewReader(visibleTestResult.Input + "\n") +// i.Stdin = input +// +// // Run the code +// _, err := i.Eval("main.main()") +// if err != nil { +// visibleTestResult.Actual = "" +// visibleTestResult.Passed = false +// visibleTestResult.Error = err.Error() +// continue +// } +// +// actualOutput := strings.TrimSpace(stdout.String()) +// +// visibleTestResult.Actual = actualOutput +// visibleTestResult.Passed = actualOutput == visibleTestResult.Expected +// } +// +// return visibleTestResults, nil +//} diff --git a/apps/execution-service/utils/populate.go b/apps/execution-service/utils/populate.go new file mode 100644 index 0000000000..df1c386a5e --- /dev/null +++ b/apps/execution-service/utils/populate.go @@ -0,0 +1,634 @@ +package utils + +import ( + "cloud.google.com/go/firestore" + "context" + "execution-service/models" + "fmt" + "google.golang.org/api/iterator" + "log" + "strings" +) + +// RepopulateTests Populate deletes all testcases and repopulates testcases +func RepopulateTests(ctx context.Context, client *firestore.Client, + questionTitleToDocRefIdMap map[string]string) error { + + // delete all existing document in the collection + iter := client.Collection("tests").Documents(ctx) + for { + doc, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return fmt.Errorf("failed to iterate over tests: %v", err) + } + + if _, err := doc.Ref.Delete(ctx); err != nil { + return fmt.Errorf("failed to delete test: %v", err) + } + } + + var tests = []models.Test{ + { + QuestionTitle: "Reverse a String", + VisibleTestCases: ` +1 +hello +olleh +`, + HiddenTestCases: ` +2 +Hannah +hannaH +abcdefg +gfedcba +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Linked List Cycle Detection", + VisibleTestCases: ` +1 +[3,2,0,-4] -> pos = 1 +true +`, + HiddenTestCases: ` +2 +[1,2] -> pos = 0 +true +[1] +false +`, + InputValidation: getPackagesAndFunction([]string{"strconv", "strings"}, ` +hasCycle := false + +// Step 1: Split by " -> pos = " +parts := strings.Split(inputOrOutput, " -> pos = ") +if len(parts) == 2 { + hasCycle = true +} else if len(parts) != 1 { + return false +} + +listPart := strings.TrimSpace(parts[0]) // Get the list part + +//Validate the list format +if len(listPart) < 2 || listPart[0] != '[' || listPart[len(listPart)-1] != ']' { + return false +} + +listContent := listPart[1 : len(listPart)-1] // Remove brackets +if listContent == "" { // Check for empty list + return false +} + +// Split the list by commas and validate each element +elements := strings.Split(listContent, ",") +for _, elem := range elements { + elem = strings.TrimSpace(elem) // Trim whitespace + if _, err := strconv.Atoi(elem); err != nil { // Check if it's an integer + return false + } +} + +if !hasCycle { + return true +} + +posPart := strings.TrimSpace(parts[1]) // Get the position part + +//Validate the position +posValue, err := strconv.Atoi(posPart) +if err != nil || posValue < 0 || posValue >= len(elements) { + return false +} + +return true +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return inputOrOutput == "true" || inputOrOutput == "false" +`), + }, + { + QuestionTitle: "Roman to Integer", + VisibleTestCases: ` +1 +III +3 +`, + HiddenTestCases: ` +2 +IV +4 +MCMXCIV +1994 +`, + InputValidation: getPackagesAndFunction([]string{"strings"}, ` +valid := "IVXLCDM" +for _, char := range inputOrOutput { + if !strings.ContainsRune(valid, char) { + return false + } +} +return true +`), + OutputValidation: getPackagesAndFunction([]string{"strconv"}, ` +_, err := strconv.Atoi(inputOrOutput) +return err == nil +`), + }, + { + QuestionTitle: "Add Binary", + VisibleTestCases: ` +1 +"11", "1" +"100" +`, + HiddenTestCases: ` +2 +"1010", "1011" +"10101" +"111", "111" +"1110" +`, + InputValidation: getPackagesAndFunction([]string{"regexp"}, ` +binaryRegex := regexp.MustCompile("^\"([01]+)\",\\s*\"([01]+)\"$") +return binaryRegex.MatchString(inputOrOutput) +`), + OutputValidation: getPackagesAndFunction([]string{"regexp"}, ` +binaryRegex := regexp.MustCompile("^\"([01]+)\"$") +return binaryRegex.MatchString(inputOrOutput) +`), + }, + { + QuestionTitle: "Fibonacci Number", + VisibleTestCases: ` +1 +0 +0 +`, + HiddenTestCases: ` +2 +1 +1 +10 +55 +`, + InputValidation: getPackagesAndFunction([]string{"strconv"}, ` +num, err := strconv.Atoi(inputOrOutput) +return err == nil && num >= 0 +`), + OutputValidation: getPackagesAndFunction([]string{"strconv"}, ` +num, err := strconv.Atoi(inputOrOutput) +return err == nil && num >= 0 +`), + }, + { + QuestionTitle: "Implement Stack using Queues", + VisibleTestCases: ` +1 +push(1), push(2), top() +2 +`, + HiddenTestCases: ` +2 +push(1), push(2), pop(), top() +1 +push(1), empty() +false +`, + InputValidation: getPackagesAndFunction([]string{"strconv", "strings"}, ` +// Split the line by commas to handle multiple operations +operations := strings.Split(inputOrOutput, ",") +for _, op := range operations { + op = strings.TrimSpace(op) // Trim whitespace + // Check if the operation is valid + if strings.HasPrefix(op, "push(") && strings.HasSuffix(op, ")") { + // Check if it's a push operation with a valid integer + numStr := op[5 : len(op)-1] // Extract the number string + if _, err := strconv.Atoi(numStr); err != nil { + return false + } + } else if op != "pop()" && op != "top()" && op != "empty()" { + // If the operation is not one of the valid ones + return false + } +} +return true +`), + OutputValidation: getPackagesAndFunction([]string{"strconv"}, ` +if inputOrOutput == "true" || inputOrOutput == "false" || inputOrOutput == "null" { + return true +} +_, err := strconv.Atoi(inputOrOutput) +return err == nil +`), + }, { + QuestionTitle: "Combine Two Tables", + VisibleTestCases: ` +1 +Person: [(1, "Smith", "John"), (2, "Doe", "Jane")], Address: [(1, 1, "NYC", "NY"), (2, 3, "LA", "CA")] +[("John", "Smith", "NYC", "NY"), ("Jane", "Doe", null, null)] +`, + HiddenTestCases: ` +2 +Person: [(1, "Black", "Jim")], Address: [(1, 1, "Miami", "FL")] +[("Jim", "Black", "Miami", "FL")] +Person: [(1, "White", "Mary")], Address: [] +[("Mary", "White", null, null)] +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Repeated DNA Sequences", + VisibleTestCases: ` +1 +AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT +["AAAAACCCCC", "CCCCCAAAAA"] +`, + HiddenTestCases: ` +2 +AAAAAAAAAAAAA +["AAAAAAAAAA"] +ACGTACGTACGT +[] +`, + InputValidation: getPackagesAndFunction([]string{}, ` +input := inputOrOutput + +// Check that input length is at least 10 +if len(input) < 10 { + return false +} + +// Check that input contains only 'A', 'C', 'G', and 'T' +for _, ch := range input { + if ch != 'A' && ch != 'C' && ch != 'G' && ch != 'T' { + return false + } +} +return true +`), + OutputValidation: getPackagesAndFunction([]string{"strings"}, ` +output := inputOrOutput + +if output == "[]" { + return true +} + +// Check that the output is enclosed in square brackets +if len(output) < 2 || output[0] != '[' || output[len(output)-1] != ']' { + return false +} + +// Extract the content between square brackets +content := output[1 : len(output)-1] + +// Split by commas without trimming spaces +sequences := strings.Split(content, ", ") +for _, seq := range sequences { + // Check if each sequence is properly enclosed in double quotes and is exactly 10 characters + if len(seq) != 12 || seq[0] != '"' || seq[11] != '"' { + return false + } + + // Check that the sequence only contains valid DNA characters between the quotes + for i := 1; i < 11; i++ { + ch := seq[i] + if ch != 'A' && ch != 'C' && ch != 'G' && ch != 'T' { + return false + } + } +} +return true +`), + }, + { + QuestionTitle: "Course Schedule", + VisibleTestCases: ` +1 +2, [[1,0]] +true +`, + HiddenTestCases: ` +2 +2, [[1,0],[0,1]] +false +4, [[1,0],[2,0],[3,1],[3,2]] +true +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "LRU Cache Design", + VisibleTestCases: ` +1 +put(1, 1), put(2, 2), get(1) +1 +`, + HiddenTestCases: ` +2 +put(1, 1), put(2, 2), put(3, 3), get(2) +-1 +put(1, 1), put(2, 2), put(1, 10), get(1) +10 +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Longest Common Subsequence", + VisibleTestCases: ` +1 +"abcde", "ace" +3 +`, + HiddenTestCases: ` +2 +"abc", "abc" +3 +"abc", "def" +0 +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Rotate Image", + VisibleTestCases: ` +1 +[[1,2,3],[4,5,6],[7,8,9]] +[[7,4,1],[8,5,2],[9,6,3]] +`, + HiddenTestCases: ` +2 +[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] +[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] +[[1]] +[[1]] +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Airplane Seat Assignment Probability", + VisibleTestCases: ` +1 +1 +1.00000 +`, + HiddenTestCases: ` +2 +2 +0.50000 +3 +0.50000 +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Validate Binary Search Tree", + VisibleTestCases: ` +1 +[2,1,3] +true +`, + HiddenTestCases: ` +2 +[5,1,4,null,null,3,6] +false +[1] +true +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Sliding Window Maximum", + VisibleTestCases: ` +1 +[1,3,-1,-3,5,3,6,7], k=3 +[3,3,5,5,6,7] +`, + HiddenTestCases: ` +2 +[1, -1], k=1 +[1, -1] +[9, 11], k=2 +[11] +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "N-Queen Problem", + VisibleTestCases: ` +1 +4 +[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] +`, + HiddenTestCases: ` +2 +1 +[["Q"]] +2 +[] +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Serialize and Deserialize a Binary Tree", + VisibleTestCases: ` +1 +[1,2,3,null,null,4,5] +"1 2 null null 3 4 null null 5 null null" +`, + HiddenTestCases: ` +2 +[] +"null" +[1] +"1 null null" +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Wildcard Matching", + VisibleTestCases: ` +1 +"aa", "a" +false +`, + HiddenTestCases: ` +2 +"aa", "*" +true +"cb", "?a" +false +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Chalkboard XOR Game", + VisibleTestCases: ` +1 +[1,1,2] +false +`, + HiddenTestCases: ` +2 +[1,2,3] +true +[0,0,0] +true +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + { + QuestionTitle: "Trips and Users", + VisibleTestCases: ` +1 +Trips: [(1, 1, 10, 'NYC', 'completed', '2013-10-01'), (2, 2, 11, 'NYC', 'cancelled_by_driver', '2013-10-01')],Users: [(10, 'No', 'client'), (11, 'No', 'driver')] +0.50 +`, + HiddenTestCases: ` +2 +Trips: [(1, 1, 10, 'NYC', 'completed', '2013-10-02')],Users: [(10, 'No', 'client'), (11, 'No', 'driver')] +0.00 +Trips: [(1, 1, 10, 'NYC', 'completed', '2013-10-03'), (2, 2, 11, 'NYC', 'cancelled_by_client', '2013-10-03')],Users: [(10, 'No', 'client'), (11, 'Yes', 'driver')] +0.00 +`, + InputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + OutputValidation: getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`), + }, + } + + for _, test := range tests { + if _, exists := questionTitleToDocRefIdMap[test.QuestionTitle]; !exists { + return fmt.Errorf("testcase's question title %s not found in questionTitleToDocRefIdMap", + test.QuestionTitle) + } + } + + for _, test := range tests { + _, err := ValidateTestCaseFormat(test.VisibleTestCases, test.InputValidation, test.OutputValidation) + if err != nil { + return fmt.Errorf("failed to validate visible test case format: %v", err) + } + _, err = ValidateTestCaseFormat(test.HiddenTestCases, test.InputValidation, test.OutputValidation) + if err != nil { + return fmt.Errorf("failed to validate hidden test case format: %v", err) + } + } + + for _, test := range tests { + _, _, err := client.Collection("tests").Add(ctx, map[string]interface{}{ + "questionDocRefId": questionTitleToDocRefIdMap[test.QuestionTitle], + "visibleTestCases": test.VisibleTestCases, + "hiddenTestCases": test.HiddenTestCases, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + return fmt.Errorf("failed to add test: %v", err) + } + } + + log.Println("Cleaned tests collection and repopulated tests.") + return nil +} + +func getPackagesAndFunction(imports []string, functionBody string) string { + // Use a string builder for efficient string concatenation + var importCode strings.Builder + + if len(imports) > 0 { + // Start the import block + importCode.WriteString("import (\n") + + // Iterate over the imports and add them to the builder + for _, imp := range imports { + importCode.WriteString(fmt.Sprintf("\t%q\n", imp)) + } + + // Close the import block + importCode.WriteString(")") + } + + // add tab before every line in functionBody including first line + functionBody = strings.ReplaceAll(functionBody, "\n", "\n\t") + + return fmt.Sprintf(` +%s + +func validateInputOrOutput(inputOrOutput string) bool { +%s +} +`, importCode.String(), functionBody) +} diff --git a/apps/execution-service/utils/testCase.go b/apps/execution-service/utils/testCase.go new file mode 100644 index 0000000000..919365256f --- /dev/null +++ b/apps/execution-service/utils/testCase.go @@ -0,0 +1,80 @@ +package utils + +import ( + "execution-service/models" + "fmt" + "strconv" + "strings" +) + +func GetTestLength(testCase string) (int, error) { + lines := strings.Split(strings.TrimSpace(testCase), "\n") + if len(lines) < 1 { + return 0, fmt.Errorf("test case format is incorrect, no lines found") + } + numCases, err := strconv.Atoi(lines[0]) + if err != nil { + return 0, fmt.Errorf("test case format is incorrect, first line must be an integer") + } + return numCases, nil +} + +func GetTestLengthAndUnexecutedCases(testCase string) (int, []models.IndividualTestResult, error) { + lines := strings.Split(strings.TrimSpace(testCase), "\n") + if len(lines) < 1 { + return 0, nil, fmt.Errorf("test case format is incorrect, no lines found") + } + + numCases, err := strconv.Atoi(lines[0]) + if err != nil { + return 0, nil, fmt.Errorf("test case format is incorrect, first line must be an integer") + } + + if len(lines) != 1+2*numCases { + return 0, nil, fmt.Errorf("test case format is incorrect, expected %d lines but got %d", 1+2*numCases, len(lines)) + } + + var testResults []models.IndividualTestResult + for i := 1; i < len(lines); i += 2 { + testResults = append(testResults, models.IndividualTestResult{ + Input: lines[i], + Expected: lines[i+1], + }) + } + return numCases, testResults, nil +} + +//func GetTestLengthAndExecutedCases(code string, testCase string) (int, []models.IndividualTestResult, error) { +// lines := strings.Split(strings.TrimSpace(testCase), "\n") +// if len(lines) < 1 { +// return 0, nil, fmt.Errorf("test case format is incorrect, no lines found") +// } +// +// numCases, err := strconv.Atoi(lines[0]) +// if err != nil { +// return 0, nil, fmt.Errorf("test case format is incorrect, first line must be an integer") +// } +// +// if len(lines) != 1+2*numCases { +// return 0, nil, fmt.Errorf("test case format is incorrect, expected %d lines but got %d", 1+2*numCases, len(lines)) +// } +// +// var testResults []models.IndividualTestResult +// for i := 1; i < len(lines); i += 2 { +// // execute code dynamically with input, and compare output with expected +// +// } +// +// numCases, testResults, err := GetTestLengthAndUnexecutedCases(testCase) +// if err != nil { +// return 0, nil, err +// } +// +// for i := range testCases { +// testCases[i].Actual = "" +// testCases[i].Passed = false +// testCases[i].Error = "" +// } +// +// return numCases, testCases, nil +//} diff --git a/apps/execution-service/utils/validateTestCaseFormat.go b/apps/execution-service/utils/validateTestCaseFormat.go new file mode 100644 index 0000000000..ef03752a1b --- /dev/null +++ b/apps/execution-service/utils/validateTestCaseFormat.go @@ -0,0 +1,87 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" +) + +func ValidateTestCaseFormat(testCase string, validateInputCode string, validateOutputCode string) (bool, error) { + lines := strings.Split(strings.TrimSpace(testCase), "\n") + + // Check if there is at least one line (the number of test cases line) + if len(lines) < 1 { + return false, fmt.Errorf("test case format is incorrect, no lines found") + } + + // Parse the first line to get the expected number of test cases + numCases, err := strconv.Atoi(lines[0]) + if err != nil { + return false, fmt.Errorf("test case format is incorrect, first line must be an integer") + } + + // Calculate the required number of lines: 1 for count + 2 lines per test case + expectedLines := 1 + 2*numCases + if len(lines) != expectedLines { + return false, fmt.Errorf("test case format is incorrect, expected %d lines but got %d", expectedLines, + len(lines)) + } + + println("test1") + + for i := 1; i < len(lines); i += 2 { + println("test2") + ok, err := validateInputOrOutputFormat(validateInputCode, lines[i]) + if err != nil { + return false, fmt.Errorf("error validating input: %v", err) + } + if !ok { + return false, fmt.Errorf("test case format is incorrect, input format is invalid") + } + println("test3") + ok, err = validateInputOrOutputFormat(validateOutputCode, lines[i+1]) + if err != nil { + return false, fmt.Errorf("error validating output: %v", err) + } + if !ok { + return false, fmt.Errorf("test case format is incorrect, output format is invalid") + } + println("test4") + } + println("test5") + + return true, nil +} + +func validateInputOrOutputFormat(validateInputOrOutputCode string, inputOrOutput string) (bool, error) { + // Initialize the yaegi interpreter + i := interp.New(interp.Options{}) + i.Use(stdlib.Symbols) + + // Create a Go function as a string and include the provided validation code + fullCode := fmt.Sprintf(` +package main +%s +`, validateInputOrOutputCode) + + println(fullCode) + + // Evaluate the function code + _, err := i.Eval(fullCode) + if err != nil { + return false, fmt.Errorf("error evaluating code: %v", err) + } + + // Retrieve the validateInput function from the interpreter + validateFunc, err := i.Eval("main.validateInputOrOutput") + if err != nil { + return false, fmt.Errorf("validateInputOrOutput function not found") + } + + // Call the function with the provided input + result := validateFunc.Interface().(func(string) bool)(inputOrOutput) + return result, nil +} diff --git a/apps/matching-service/.dockerignore b/apps/matching-service/.dockerignore new file mode 100644 index 0000000000..c6228e8d6c --- /dev/null +++ b/apps/matching-service/.dockerignore @@ -0,0 +1,9 @@ +.env.example + +.git +.gitignore + +.dockerignore +Dockerfile + +README.md diff --git a/apps/question-service/utils/populate.go b/apps/question-service/utils/populate.go index 140ee8393d..7aee788e7a 100644 --- a/apps/question-service/utils/populate.go +++ b/apps/question-service/utils/populate.go @@ -1,8 +1,13 @@ package utils import ( + "bytes" "context" + "encoding/json" + "fmt" "log" + "net/http" + "os" "question-service/models" "time" @@ -12,8 +17,9 @@ import ( // PopulateSampleQuestionsInTransaction deletes all existing questions and then adds new ones in a single transaction func populateSampleQuestionsInTransaction(ctx context.Context, client *firestore.Client) error { + // Sample questions to be added after deletion - sampleQuestions := []models.Question{ + var sampleQuestions = []models.Question{ { Title: "Reverse a String", Categories: []string{"Strings", "Algorithms"}, @@ -90,8 +96,8 @@ And table Address with the following columns: 4. state (varchar) addressId is the primary key. -Write a solution to report the first name, last name, city, and state of each person in the Person table. -If the address of a personId is not present in the Address table, +Write a solution to report the first name, last name, city, and state of each person in the Person table. +If the address of a personId is not present in the Address table, report null instead. Return the result table in any order.`, }, { @@ -293,6 +299,57 @@ Return the result table in any order.`, }) } +func repopulateTests(dbClient *firestore.Client) error { + ctx := context.Background() + + executionServiceUrl := os.Getenv("EXECUTION_SERVICE_URL") + url := executionServiceUrl + "tests/populate" + + // get title and docRefId of all questions + var questions []map[string]string + iter := dbClient.Collection("questions").Documents(ctx) + for { + doc, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return fmt.Errorf("failed to fetch question: %v", err) + } + + questions = append(questions, map[string]string{ + "title": doc.Data()["title"].(string), + "docRefId": doc.Ref.ID, + }) + } + + jsonData, err := json.Marshal(questions) + if err != nil { + return fmt.Errorf("failed to marshal question titles: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request to populate tests: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to populate tests: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to populate tests: %v", resp.Status) + } + + return nil +} + func Populate(client *firestore.Client) { ctx := context.Background() @@ -302,5 +359,12 @@ func Populate(client *firestore.Client) { log.Fatalf("Failed to populate sample questions in transaction: %v", err) } - log.Println("Counter reset, all questions deleted and sample questions added successfully in a transaction.") + // Populate testcases in the execution-service + err = repopulateTests(client) + if err != nil { + log.Fatalf("Failed to populate testcases: %v", err) + } + + log.Println("Counter reset, " + + "all questions deleted and sample questions added successfully in a transaction. Testcases repopulated in execution-service.") } From aa8be69591e2432e7444138d8abc2315b5649a55 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Fri, 1 Nov 2024 22:44:40 +0800 Subject: [PATCH 02/16] Add .env.example --- apps/execution-service/.env.example | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 apps/execution-service/.env.example diff --git a/apps/execution-service/.env.example b/apps/execution-service/.env.example new file mode 100644 index 0000000000..1cf22d0894 --- /dev/null +++ b/apps/execution-service/.env.example @@ -0,0 +1,2 @@ +FIREBASE_CREDENTIAL_PATH=cs3219-staging-codeexecution-firebase-adminsdk-ce48j-00ab09514c.json +PORT=8083 \ No newline at end of file From a1232e7ef9b8c428edda9d44d92692effb6eeacf Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Fri, 1 Nov 2024 22:50:27 +0800 Subject: [PATCH 03/16] Add api test cases and api usage examples --- apps/execution-service/README.md | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index eaa740b179..0bfc5593f1 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -86,3 +86,39 @@ In question-service, run the following command: ```bash go run main.go -populate ``` + +## API Documentation + +`GET /tests/{questionDocRefId}/` + +To read visible test cases via a question ID, run the following command: + +```bash +curl -X GET http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/ \ +-H "Content-Type: application/json" +``` + +`GET /tests/{questionDocRefId}/execute` + +To execute test cases via a question ID without custom test cases, run the following command, with custom code and language: + +```bash +curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ +-H "Content-Type: application/json" \ +-d '{ +"code": "name = input()\nprint(name[::-1])", +"language": "Python" +}' +``` + +To execute test cases via a question ID with custom test cases, run the following command, with custom code, language and custom test cases: + +```bash +curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ +-H "Content-Type: application/json" \ +-d '{ +"code": "name = input()\nprint(name[::-1])", +"language": "Python", +"customTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba\n" +}' +``` From c7a72bf8d5e700bbe14aa238618ee035aad12434 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Sat, 2 Nov 2024 18:10:39 +0800 Subject: [PATCH 04/16] Add accepted or attempted status and dockerise --- apps/execution-service/.env.example | 8 +- apps/execution-service/Dockerfile | 4 +- apps/execution-service/README.md | 161 ++++++++++++++---- apps/execution-service/enums/language.go | 10 -- apps/execution-service/handlers/execute.go | 8 +- apps/execution-service/handlers/read.go | 2 +- apps/execution-service/handlers/submit.go | 121 +++++++++++++ apps/execution-service/main.go | 5 +- .../execution-service/models/collaboration.go | 13 ++ .../models/collaborationHistory.go | 21 +++ apps/execution-service/models/testResult.go | 13 +- apps/execution-service/test.go | 59 ------- apps/execution-service/utils/executeTest.go | 159 ++++++++--------- apps/execution-service/utils/testCase.go | 35 ---- apps/history-service/Dockerfile | 2 +- apps/history-service/handlers/create.go | 1 + .../handlers/createOrUpdate.go | 1 + apps/history-service/handlers/update.go | 2 + apps/history-service/main.go | 4 +- .../models/{models.go => collaboration.go} | 6 + apps/question-service/.env.example | 13 +- apps/signalling-service/.env.example | 3 +- 22 files changed, 407 insertions(+), 244 deletions(-) delete mode 100644 apps/execution-service/enums/language.go create mode 100644 apps/execution-service/handlers/submit.go create mode 100644 apps/execution-service/models/collaboration.go create mode 100644 apps/execution-service/models/collaborationHistory.go delete mode 100644 apps/execution-service/test.go rename apps/history-service/models/{models.go => collaboration.go} (88%) diff --git a/apps/execution-service/.env.example b/apps/execution-service/.env.example index 1cf22d0894..6a1fb0bd86 100644 --- a/apps/execution-service/.env.example +++ b/apps/execution-service/.env.example @@ -1,2 +1,8 @@ FIREBASE_CREDENTIAL_PATH=cs3219-staging-codeexecution-firebase-adminsdk-ce48j-00ab09514c.json -PORT=8083 \ No newline at end of file +PORT=8083 + +# If you are NOT USING docker, use the below variables +# HISTORY_SERVICE_URL=http://localhost:8082/ + +# If you are USING docker, use the below variables +HISTORY_SERVICE_URL=http://history-service:8082/ diff --git a/apps/execution-service/Dockerfile b/apps/execution-service/Dockerfile index 0cabe00edd..1a0bb66e44 100644 --- a/apps/execution-service/Dockerfile +++ b/apps/execution-service/Dockerfile @@ -7,12 +7,10 @@ COPY go.mod go.sum ./ RUN go mod tidy && go mod download && go mod verify -COPY .env /usr/src/app/.env - COPY . . RUN go build -v -o /usr/local/bin/app ./main.go -EXPOSE 8083 8083 +EXPOSE 8083 CMD ["app"] diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index 0bfc5593f1..fc84f1647a 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -1,43 +1,14 @@ # Execution Service -## Overview - -The Execution Service is built with Go, utilizing Firestore as the database and Chi as the HTTP router. It provides an API to manage test cases, such as populating test cases (via question-service), reading visible test cases and executing visible, hidden and custom test cases. - -## Features - -- Populate test cases (via populate questins in question-service) -- Read visible test cases via a question ID -- Execute visible test cases via a question ID - -## Technologies Used - -- Go (Golang) -- Firestore (Google Cloud Firestore) -- Chi (HTTP router) -- Yaegi (Go interpreter) - -## Getting Started - -### Prerequisites - -- Go 1.16 or later -- Google Cloud SDK -- Firestore database setup in your Google Cloud project - ### Installation -1. Clone the repository - -2. Set up your Firestore client - -3. Install dependencies: +1. Install dependencies: ```bash go mod tidy ``` -4. Create the `.env` file from copying the `.env.example`, and copy the firebase JSON file into execution-service/ fill in the `FIREBASE_CREDENTIAL_PATH` with the path of the firebase credential JSON file. +2. Create the `.env` file from copying the `.env.example`, and copy the firebase JSON file into execution-service/ fill in the `FIREBASE_CREDENTIAL_PATH` with the path of the firebase credential JSON file. ### Running the Application @@ -94,16 +65,27 @@ go run main.go -populate To read visible test cases via a question ID, run the following command: ```bash -curl -X GET http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/ \ +curl -X GET http://localhost:8083/tests/{questioinDocRefId}/ \ -H "Content-Type: application/json" ``` +The following json format will be returned: + +```json +[ + { + "input":"hello", + "expected":"olleh" + } +] +``` + `GET /tests/{questionDocRefId}/execute` To execute test cases via a question ID without custom test cases, run the following command, with custom code and language: ```bash -curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ +curl -X POST http://localhost:8083/tests/{questioinDocRefId}/execute \ -H "Content-Type: application/json" \ -d '{ "code": "name = input()\nprint(name[::-1])", @@ -111,10 +93,27 @@ curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ }' ``` -To execute test cases via a question ID with custom test cases, run the following command, with custom code, language and custom test cases: +The following json format will be returned: + +```json +{ + "visibleTestResults":[ + { + "input":"hello", + "expected":"olleh", + "actual":"olleh", + "passed":true, + "error":"" + } + ], + "customTestResults":null +} +``` + +To execute visible and custom test cases via a question ID with custom test cases, run the following command, with custom code, language and custom test cases: ```bash -curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ +curl -X POST http://localhost:8083/tests/{questioinDocRefId}/execute \ -H "Content-Type: application/json" \ -d '{ "code": "name = input()\nprint(name[::-1])", @@ -122,3 +121,95 @@ curl -X POST http://localhost:8083/tests/{questionDocRefId}/execute \ "customTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba\n" }' ``` + +The following json format will be returned: + +```json +{ + "visibleTestResults":[ + { + "input":"hello", + "expected":"olleh", + "actual":"olleh", + "passed":true, + "error":"" + } + ], + "customTestResults":[ + { + "input":"Hannah", + "expected":"hannaH", + "actual":"hannaH", + "passed":true, + "error":"" + }, + { + "input":"abcdefg", + "expected":"gfedcba", + "actual":"gfedcba", + "passed":true, + "error":"" + } + ] +} +``` + +To submit a solution and execute visible and hidden test cases via a question ID, run the following command, with custom code and language: + +```bash +curl -X POST http://localhost:8083/tests/{questioinDocRefId}/submit \ +-H "Content-Type: application/json" \ +-d '{ +"title": "Example Title", +"code": "name = input()\nprint(name[::-1])", +"language": "Python", +"user": "user123", +"matchedUser": "user456", +"matchId": "match123", +"matchedTopics": ["topic1", "topic2"], +"questionDifficulty": "Medium", +"questionTopics": ["Loops", "Strings"] +}' +``` + +The following json format will be returned: + +```json +{ + "visibleTestResults":[ + { + "input":"hello", + "expected":"olleh", + "actual":"olleh", + "passed":true, + "error":"" + } + ], + "hiddenTestResults":{ + "passed":2, + "total":2 + }, + "status":"Accepted" +} +``` + +If compilation error exists or any of the tests (visible and hidden) fails, status "Attempted" will be returned: + +```json +{ + "visibleTestResults":[ + { + "input":"hello", + "expected":"olleh", + "actual":"", + "passed":false, + "error":"Command execution failed: Traceback (most recent call last):\n File \"/tmp/4149249165.py\", line 2, in \u003cmodule\u003e\n prit(name[::-1])\n ^^^^\nNameError: name 'prit' is not defined. Did you mean: 'print'?\n: %!w(*exec.ExitError=\u0026{0x4000364678 []})" + } + ], + "hiddenTestResults":{ + "passed":0, + "total":2 + }, + "status":"Attempted" +} +``` diff --git a/apps/execution-service/enums/language.go b/apps/execution-service/enums/language.go deleted file mode 100644 index 7f40b81891..0000000000 --- a/apps/execution-service/enums/language.go +++ /dev/null @@ -1,10 +0,0 @@ -package enums - -// Create enums of languages -const ( - JAVA = "Java" - PYTHON = "Python" - GOLANG = "Golang" - JAVASCRIPT = "Javascript" - CPP = "C++" -) diff --git a/apps/execution-service/handlers/execute.go b/apps/execution-service/handlers/execute.go index e6cad62229..eda0d6ab60 100644 --- a/apps/execution-service/handlers/execute.go +++ b/apps/execution-service/handlers/execute.go @@ -9,7 +9,7 @@ import ( "net/http" ) -func (s *Service) ExecuteTest(w http.ResponseWriter, r *http.Request) { +func (s *Service) ExecuteVisibleAndCustomTests(w http.ResponseWriter, r *http.Request) { ctx := r.Context() questionDocRefId := chi.URLParam(r, "questionDocRefId") @@ -41,7 +41,7 @@ func (s *Service) ExecuteTest(w http.ResponseWriter, r *http.Request) { return } - testResults, err := utils.ExecuteTest(code, test) + testResults, err := utils.ExecuteVisibleAndCustomTests(code, test) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -52,14 +52,14 @@ func (s *Service) ExecuteTest(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(testResults) } -//curl -X POST http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/execute \ +//curl -X POST http://localhost:8083/tests/Yt29JjnIDpRwIlYAX8OF/execute \ //-H "Content-Type: application/json" \ //-d '{ //"code": "name = input()\nprint(name[::-1])", //"language": "Python" //}' -//curl -X POST http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/execute \ +//curl -X POST http://localhost:8083/tests/Yt29JjnIDpRwIlYAX8OF/execute \ //-H "Content-Type: application/json" \ //-d '{ //"code": "name = input()\nprint(name[::-1])", diff --git a/apps/execution-service/handlers/read.go b/apps/execution-service/handlers/read.go index db7fd4a21a..d8193c7b5a 100644 --- a/apps/execution-service/handlers/read.go +++ b/apps/execution-service/handlers/read.go @@ -9,7 +9,7 @@ import ( "net/http" ) -func (s *Service) ReadTest(w http.ResponseWriter, r *http.Request) { +func (s *Service) ReadVisibleTests(w http.ResponseWriter, r *http.Request) { ctx := r.Context() questionDocRefId := chi.URLParam(r, "questionDocRefId") diff --git a/apps/execution-service/handlers/submit.go b/apps/execution-service/handlers/submit.go new file mode 100644 index 0000000000..c38c0fb094 --- /dev/null +++ b/apps/execution-service/handlers/submit.go @@ -0,0 +1,121 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "execution-service/models" + "execution-service/utils" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" + "os" +) + +func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(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 + } + + var collab models.Collaboration + if err := utils.DecodeJSONBody(w, r, &collab); err != nil { + http.Error(w, err.Error(), 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 + } + + var test models.Test + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + testResults, err := utils.ExecuteVisibleAndHiddenTests(models.Code{ + Code: collab.Code, + Language: collab.Language, + }, test) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Save the collaboration history via the history-service + // TODO: convert to message queue + collabHistory := models.CollaborationHistory{ + Title: collab.Title, + Code: collab.Code, + Language: collab.Language, + User: collab.User, + MatchedUser: collab.MatchedUser, + MatchID: collab.MatchID, + MatchedTopics: collab.MatchedTopics, + QuestionDocRefID: questionDocRefId, + QuestionDifficulty: collab.QuestionDifficulty, + QuestionTopics: collab.QuestionTopics, + } + + jsonData, err := json.Marshal(collabHistory) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // get history-service url from os env + historyServiceUrl := os.Getenv("HISTORY_SERVICE_URL") + if historyServiceUrl == "" { + http.Error(w, "HISTORY_SERVICE_URL is not set", http.StatusInternalServerError) + return + } + + req, err := http.NewRequest(http.MethodPut, historyServiceUrl+"histories/match/"+collabHistory.MatchID, + bytes.NewBuffer(jsonData)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + http.Error(w, "Failed to save collaboration history", http.StatusInternalServerError) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(testResults) +} + +//curl -X POST http://localhost:8083/tests/Yt29JjnIDpRwIlYAX8OF/submit \ +//-H "Content-Type: application/json" \ +//-d '{ +//"title": "Example Title", +//"code": "name = input()\nprint(name[::-1])", +//"language": "Python", +//"user": "user123", +//"matchedUser": "user456", +//"matchId": "match123", +//"matchedTopics": ["topic1", "topic2"], +//"questionDifficulty": "Medium", +//"questionTopics": ["Loops", "Strings"] +//}' diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index e9991c5f82..706d0afcd6 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -90,8 +90,9 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { // Future extension: can be read by admin to view testcases //r.Put("/", service.UpdateTest) //r.Delete("/", service.DeleteTest) - r.Get("/", service.ReadTest) - r.Post("/execute", service.ExecuteTest) + r.Get("/", service.ReadVisibleTests) + r.Post("/execute", service.ExecuteVisibleAndCustomTests) + r.Post("/submit", service.ExecuteVisibleAndHiddenTestsAndSubmit) }) }) } diff --git a/apps/execution-service/models/collaboration.go b/apps/execution-service/models/collaboration.go new file mode 100644 index 0000000000..4986fbbe0e --- /dev/null +++ b/apps/execution-service/models/collaboration.go @@ -0,0 +1,13 @@ +package models + +type Collaboration struct { + Title string `json:"title" firestore:"title"` + Code string `json:"code" firestore:"code"` + Language string `json:"language" firestore:"language"` + User string `json:"user" firestore:"user"` + MatchedUser string `json:"matchedUser" firestore:"matchedUser"` + MatchID string `json:"matchId" firestore:"matchId"` + MatchedTopics []string `json:"matchedTopics" firestore:"matchedTopics"` + QuestionDifficulty string `json:"questionDifficulty" firestore:"questionDifficulty"` + QuestionTopics []string `json:"questionTopics" firestore:"questionTopics"` +} diff --git a/apps/execution-service/models/collaborationHistory.go b/apps/execution-service/models/collaborationHistory.go new file mode 100644 index 0000000000..4b6a5ced37 --- /dev/null +++ b/apps/execution-service/models/collaborationHistory.go @@ -0,0 +1,21 @@ +package models + +import "time" + +type CollaborationHistory struct { + Title string `json:"title"` + Code string `json:"code"` + Language string `json:"language"` + User string `json:"user"` + MatchedUser string `json:"matchedUser"` + MatchID string `json:"matchId"` + MatchedTopics []string `json:"matchedTopics"` + QuestionDocRefID string `json:"questionDocRefId"` + QuestionDifficulty string `json:"questionDifficulty"` + QuestionTopics []string `json:"questionTopics"` + Status string `json:"status"` + + // Special DB fields + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/apps/execution-service/models/testResult.go b/apps/execution-service/models/testResult.go index df60799cd1..e0843acb72 100644 --- a/apps/execution-service/models/testResult.go +++ b/apps/execution-service/models/testResult.go @@ -6,12 +6,23 @@ type TestResults struct { CustomTestResults []IndividualTestResult `json:"customTestResults"` } +type ExecutionResults struct { + VisibleTestResults []IndividualTestResult `json:"visibleTestResults"` + CustomTestResults []IndividualTestResult `json:"customTestResults"` +} + +type SubmissionResults struct { + VisibleTestResults []IndividualTestResult `json:"visibleTestResults"` + HiddenTestResults GeneralTestResults `json:"hiddenTestResults"` + Status string `json:"status"` +} + type IndividualTestResult struct { Input string `json:"input"` Expected string `json:"expected"` Actual string `json:"actual"` Passed bool `json:"passed"` - Error string `json:"error,omitempty"` + Error string `json:"error"` } type GeneralTestResults struct { diff --git a/apps/execution-service/test.go b/apps/execution-service/test.go deleted file mode 100644 index 8af33b0676..0000000000 --- a/apps/execution-service/test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "execution-service/execution/python" -) - -func main() { - println("testing: ") - output, errorOutput, _ := test() - println("output: ", output) - println("errorOutput: ", errorOutput) -} - -//func test() bool { -// inputOrOutput := `[]` -// -// output := inputOrOutput -// -// if output == "[]" { -// return true -// } -// -// // Check that the output is enclosed in square brackets -// if len(output) < 2 || output[0] != '[' || output[len(output)-1] != ']' { -// return false -// } -// -// // Extract the content between square brackets -// content := output[1 : len(output)-1] -// -// // Split by commas without trimming spaces -// sequences := strings.Split(content, ", ") -// for _, seq := range sequences { -// // Check if each sequence is properly enclosed in double quotes and is exactly 10 characters -// if len(seq) != 12 || seq[0] != '"' || seq[11] != '"' { -// return false -// } -// -// // Check that the sequence only contains valid DNA characters between the quotes -// for i := 1; i < 11; i++ { -// ch := seq[i] -// if ch != 'A' && ch != 'C' && ch != 'G' && ch != 'T' { -// return false -// } -// } -// } -// return true -//} - -func test() (string, string, error) { - code := ` -nam = input() -print(name[::-1]) -` - - input := "hello" - - return python.RunPythonCode(code, input) -} diff --git a/apps/execution-service/utils/executeTest.go b/apps/execution-service/utils/executeTest.go index cdc3853777..b51051ca3e 100644 --- a/apps/execution-service/utils/executeTest.go +++ b/apps/execution-service/utils/executeTest.go @@ -1,57 +1,59 @@ package utils import ( - "execution-service/enums" "execution-service/execution/python" "execution-service/models" "fmt" ) -func ExecuteTest(code models.Code, test models.Test) (models.TestResults, error) { +const ( + JAVA = "Java" + PYTHON = "Python" + GOLANG = "Golang" + JAVASCRIPT = "Javascript" + CPP = "C++" +) + +const ( + ACCEPTED = "Accepted" + ATTEMPTED = "Attempted" +) + +func ExecuteVisibleAndCustomTests(code models.Code, test models.Test) (models.ExecutionResults, error) { var err error - var testResults models.TestResults + var testResults models.ExecutionResults switch code.Language { - case enums.PYTHON: - testResults, err = getTestResultFromTest(code, test, python.RunPythonCode) + case PYTHON: + testResults, err = getVisibleAndCustomTestResults(code, test, python.RunPythonCode) break default: - return models.TestResults{}, fmt.Errorf("unsupported language: %s", code.Language) + return models.ExecutionResults{}, fmt.Errorf("unsupported language: %s", code.Language) } if err != nil { - return models.TestResults{}, err + return models.ExecutionResults{}, err } return testResults, nil } -//func getVisibleTestResultsWithCompilationError(test models.Test, -// testCaseErrorStr string) ([]models.IndividualTestResult, error) { -// _, visibleTestResults, err := GetTestLengthAndUnexecutedCases(test.VisibleTestCases) -// if err != nil { -// return nil, err -// } -// -// for _, visibleTestResult := range visibleTestResults { -// visibleTestResult.Actual = "" -// visibleTestResult.Passed = false -// visibleTestResult.Error = testCaseErrorStr -// } -// -// return visibleTestResults, nil -//} -// -//func getHiddenTestResultsWithCompilationError(test models.Test) (models.GeneralTestResults, error) { -// numHiddenTests, err := GetTestLength(test.HiddenTestCases) -// if err != nil { -// return models.GeneralTestResults{}, err -// } -// -// return models.GeneralTestResults{ -// Passed: 0, -// Total: numHiddenTests, -// }, nil -//} +func ExecuteVisibleAndHiddenTests(code models.Code, test models.Test) (models.SubmissionResults, error) { + var err error + var testResults models.SubmissionResults + + switch code.Language { + case PYTHON: + testResults, err = getVisibleAndHiddenTestResults(code, test, python.RunPythonCode) + break + default: + return models.SubmissionResults{}, fmt.Errorf("unsupported language: %s", code.Language) + } + if err != nil { + return models.SubmissionResults{}, err + } + + return testResults, nil +} func getIndividualTestResultFromCodeExecutor(code string, unexecutedTestResult models.IndividualTestResult, executor func(string, string) (string, string, error)) (models.IndividualTestResult, error) { @@ -108,72 +110,55 @@ func getGenericTestResultsFromFormattedTestCase(code string, testCase string, }, nil } -func getTestResultFromTest(code models.Code, test models.Test, - executor func(string, string) (string, string, error)) (models.TestResults, error) { +func getVisibleAndCustomTestResults(code models.Code, test models.Test, + executor func(string, string) (string, string, error)) (models.ExecutionResults, error) { visibleTestResults, err := getAllTestResultsFromFormattedTestCase(code.Code, test.VisibleTestCases, executor) if err != nil { - return models.TestResults{}, err - } - - hiddenTestResults, err := getGenericTestResultsFromFormattedTestCase(code.Code, test.HiddenTestCases, executor) - if err != nil { - return models.TestResults{}, err + return models.ExecutionResults{}, err } var customTestResults []models.IndividualTestResult if code.CustomTestCases != "" { customTestResults, err = getAllTestResultsFromFormattedTestCase(code.Code, code.CustomTestCases, executor) if err != nil { - return models.TestResults{}, err + return models.ExecutionResults{}, err } } - return models.TestResults{ + return models.ExecutionResults{ VisibleTestResults: visibleTestResults, - HiddenTestResults: hiddenTestResults, CustomTestResults: customTestResults, }, nil } -//func getVisibleTestResults(code string, test models.Test) ([]models.IndividualTestResult, error) { -// _, visibleTestResults, err := GetTestLengthAndUnexecutedCases(test.VisibleTestCases) -// if err != nil { -// return nil, err -// } -// -// // Initialize Yaegi interpreter -// i := interp.New(interp.Options{}) -// i.Use(stdlib.Symbols) -// -// _, err = i.Eval(code) -// if err != nil { -// return nil, fmt.Errorf("error evaluating code: %v", err) -// } -// -// // Execute each test case -// for _, visibleTestResult := range visibleTestResults { -// // Create an output buffer to capture stdout -// var stdout bytes.Buffer -// i.Stdout = &stdout -// -// // Set up the input for the test case -// input := strings.NewReader(visibleTestResult.Input + "\n") -// i.Stdin = input -// -// // Run the code -// _, err := i.Eval("main.main()") -// if err != nil { -// visibleTestResult.Actual = "" -// visibleTestResult.Passed = false -// visibleTestResult.Error = err.Error() -// continue -// } -// -// actualOutput := strings.TrimSpace(stdout.String()) -// -// visibleTestResult.Actual = actualOutput -// visibleTestResult.Passed = actualOutput == visibleTestResult.Expected -// } -// -// return visibleTestResults, nil -//} +func getVisibleAndHiddenTestResults(code models.Code, test models.Test, + executor func(string, string) (string, string, error)) (models.SubmissionResults, error) { + visibleTestResults, err := getAllTestResultsFromFormattedTestCase(code.Code, test.VisibleTestCases, executor) + if err != nil { + return models.SubmissionResults{}, err + } + + hiddenTestResults, err := getGenericTestResultsFromFormattedTestCase(code.Code, test.HiddenTestCases, executor) + if err != nil { + return models.SubmissionResults{}, err + } + + status := ACCEPTED + if hiddenTestResults.Passed != hiddenTestResults.Total { + status = ATTEMPTED + } + if status == ACCEPTED { + for _, testResult := range visibleTestResults { + if !testResult.Passed { + status = ATTEMPTED + break + } + } + } + + return models.SubmissionResults{ + VisibleTestResults: visibleTestResults, + HiddenTestResults: hiddenTestResults, + Status: status, + }, nil +} diff --git a/apps/execution-service/utils/testCase.go b/apps/execution-service/utils/testCase.go index 919365256f..c353f98220 100644 --- a/apps/execution-service/utils/testCase.go +++ b/apps/execution-service/utils/testCase.go @@ -43,38 +43,3 @@ func GetTestLengthAndUnexecutedCases(testCase string) (int, []models.IndividualT } return numCases, testResults, nil } - -//func GetTestLengthAndExecutedCases(code string, testCase string) (int, []models.IndividualTestResult, error) { -// lines := strings.Split(strings.TrimSpace(testCase), "\n") -// if len(lines) < 1 { -// return 0, nil, fmt.Errorf("test case format is incorrect, no lines found") -// } -// -// numCases, err := strconv.Atoi(lines[0]) -// if err != nil { -// return 0, nil, fmt.Errorf("test case format is incorrect, first line must be an integer") -// } -// -// if len(lines) != 1+2*numCases { -// return 0, nil, fmt.Errorf("test case format is incorrect, expected %d lines but got %d", 1+2*numCases, len(lines)) -// } -// -// var testResults []models.IndividualTestResult -// for i := 1; i < len(lines); i += 2 { -// // execute code dynamically with input, and compare output with expected -// -// } -// -// numCases, testResults, err := GetTestLengthAndUnexecutedCases(testCase) -// if err != nil { -// return 0, nil, err -// } -// -// for i := range testCases { -// testCases[i].Actual = "" -// testCases[i].Passed = false -// testCases[i].Error = "" -// } -// -// return numCases, testCases, nil -//} diff --git a/apps/history-service/Dockerfile b/apps/history-service/Dockerfile index 325d4e3751..d26f475e62 100644 --- a/apps/history-service/Dockerfile +++ b/apps/history-service/Dockerfile @@ -11,6 +11,6 @@ COPY . . RUN go build -v -o /usr/local/bin/app ./main.go -EXPOSE 8082 8082 +EXPOSE 8082 CMD ["app"] diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index d981fb9190..9b4681adc6 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -33,6 +33,7 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { "questionDocRefId": collaborationHistory.QuestionDocRefID, "questionDifficulty": collaborationHistory.QuestionDifficulty, "questionTopics": collaborationHistory.QuestionTopics, + "status": collaborationHistory.Status, "createdAt": firestore.ServerTimestamp, "updatedAt": firestore.ServerTimestamp, }) diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go index f9df4bcc33..67f9195873 100644 --- a/apps/history-service/handlers/createOrUpdate.go +++ b/apps/history-service/handlers/createOrUpdate.go @@ -41,6 +41,7 @@ func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) "questionDocRefId": collaborationHistory.QuestionDocRefID, "questionDifficulty": collaborationHistory.QuestionDifficulty, "questionTopics": collaborationHistory.QuestionTopics, + "status": collaborationHistory.Status, "createdAt": firestore.ServerTimestamp, "updatedAt": firestore.ServerTimestamp, }) diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go index b6cb953709..2a79277dbb 100644 --- a/apps/history-service/handlers/update.go +++ b/apps/history-service/handlers/update.go @@ -42,6 +42,8 @@ func (s *Service) UpdateHistory(w http.ResponseWriter, r *http.Request) { // Prepare the update data. updates := []firestore.Update{ {Path: "code", Value: updatedHistory.Code}, + {Path: "language", Value: updatedHistory.Language}, + {Path: "status", Value: updatedHistory.Status}, {Path: "updatedAt", Value: firestore.ServerTimestamp}, } diff --git a/apps/history-service/main.go b/apps/history-service/main.go index cf69c934d2..e61c58fc02 100644 --- a/apps/history-service/main.go +++ b/apps/history-service/main.go @@ -75,10 +75,10 @@ func initChiRouter(service *handlers.Service) *chi.Mux { func registerRoutes(r *chi.Mux, service *handlers.Service) { r.Route("/histories", func(r chi.Router) { - r.Get("/{username}", service.ListUserHistories) + r.Get("/user/{username}", service.ListUserHistories) //r.Post("/", service.CreateHistory) - r.Route("/{matchId}", func(r chi.Router) { + r.Route("/match/{matchId}", func(r chi.Router) { r.Put("/", service.CreateOrUpdateHistory) r.Get("/", service.ReadHistory) //r.Put("/", service.UpdateHistory) diff --git a/apps/history-service/models/models.go b/apps/history-service/models/collaboration.go similarity index 88% rename from apps/history-service/models/models.go rename to apps/history-service/models/collaboration.go index 9928b8809b..da5e1f821b 100644 --- a/apps/history-service/models/models.go +++ b/apps/history-service/models/collaboration.go @@ -2,6 +2,11 @@ package models import "time" +const ( + ACCEPTED = "Accepted" + ATTEMPTED = "Attempted" +) + type CollaborationHistory struct { Title string `json:"title" firestore:"title"` Code string `json:"code" firestore:"code"` @@ -13,6 +18,7 @@ type CollaborationHistory struct { QuestionDocRefID string `json:"questionDocRefId" firestore:"questionDocRefId"` QuestionDifficulty string `json:"questionDifficulty" firestore:"questionDifficulty"` QuestionTopics []string `json:"questionTopics" firestore:"questionTopics"` + Status string `json:"status" firestore:"status"` // Special DB fields CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` diff --git a/apps/question-service/.env.example b/apps/question-service/.env.example index 88d758a7be..ed0273c62c 100644 --- a/apps/question-service/.env.example +++ b/apps/question-service/.env.example @@ -1,5 +1,14 @@ # Path to the firebase credential json -FIREBASE_CREDENTIAL_PATH=cs3219-g24-firebase-adminsdk-9cm7h-b1675603ab.json +# Staging +FIREBASE_CREDENTIAL_PATH=cs3219-g24-staging-firebase-adminsdk-suafv-9c0d1b2299.json +# Production +# FIREBASE_CREDENTIAL_PATH=cs3219-g24-firebase-adminsdk-9cm7h-b1675603ab.json # Secret for creating JWT signature -JWT_SECRET=you-can-replace-this-with-your-own-secret \ No newline at end of file +JWT_SECRET=you-can-replace-this-with-your-own-secret + +# If you are NOT USING docker, use the below variables +EXECUTION_SERVICE_URL="http://localhost:8083/" + +# If you are USING docker, use the below variables +# EXECUTION_SERVICE_URL="http://execution-service:8083/" \ No newline at end of file diff --git a/apps/signalling-service/.env.example b/apps/signalling-service/.env.example index 37300b8cdf..1b5ecaa58f 100644 --- a/apps/signalling-service/.env.example +++ b/apps/signalling-service/.env.example @@ -1 +1,2 @@ -PORT=4444 \ No newline at end of file +PORT=4444 +JWT_SECRET=you-can-replace-this-with-your-own-secret \ No newline at end of file From 7994e7fce294b20c7498bb55763bc4db2847e8a0 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Sat, 2 Nov 2024 18:27:25 +0800 Subject: [PATCH 05/16] Update test.yml --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82bd2743a4..9f26302336 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,14 +31,17 @@ jobs: MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} SIGNALLING_SERVICE_URL: ${{ vars.SIGNALLING_SERVICE_URL }} + EXECUTION_SERVICE_URL: ${{ vars.EXECUTION_SERVICE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }} + EXECUTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.EXECUTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} DB_CLOUD_URI: ${{ secrets.USER_SERVICE_DB_CLOUD_URI }} USER_SERVICE_PORT: ${{ vars.USER_SERVICE_PORT }} MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }} HISTORY_SERVICE_PORT: ${{ vars.HISTORY_SERVICE_PORT }} SIGNALLING_SERVICE_PORT: ${{ vars.SIGNALLING_SERVICE_PORT }} + EXECUTION_SERVICE_PORT: ${{ vars.EXECUTION_SERVICE_PORT }} MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }} REDIS_URL: ${{ vars.REDIS_URL }} QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }} @@ -49,6 +52,7 @@ jobs: echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env echo "NEXT_PUBLIC_HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env echo "NEXT_PUBLIC_SIGNALLING_SERVICE_URL=$SIGNALLING_SERVICE_URL" >> .env + echo "NEXT_PUBLIC_EXECUTION_SERVICE_URL=$SIGNALLING_SERVICE_URL" >> .env cd ../question-service echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env @@ -69,6 +73,11 @@ jobs: cd ../history-service echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env echo "PORT=$HISTORY_SERVICE_PORT" >> .env + + cd ../execution-service + echo "FIREBASE_CREDENTIAL_PATH=$EXECUTION_FIREBASE_CREDENTIAL_PATH" >> .env + echo "PORT=$EXECUTION_SERVICE_PORT" >> .env + echo "HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env cd ../signalling-service echo "PORT=$SIGNALLING_SERVICE_PORT" >> .env @@ -79,12 +88,17 @@ jobs: QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} HISTORY_FIREBASE_JSON: ${{ secrets.HISTORY_SERVICE_FIREBASE_CREDENTIAL }} HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }} + EXECUTION_FIREBASE_JSON: ${{ secrets.EXECUTION_SERVICE_FIREBASE_CREDENTIAL }} + EXECUTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.EXECUTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} run: | cd ./apps/question-service echo "$QUESTION_FIREBASE_JSON" > "./$QUESTION_FIREBASE_CREDENTIAL_PATH" cd ../history-service echo "$HISTORY_FIREBASE_JSON" > "./$HISTORY_FIREBASE_CREDENTIAL_PATH" + + cd ../execution-service + echo "$EXECUTION_FIREBASE_JSON" > "./$EXECUTION_FIREBASE_CREDENTIAL_PATH" - name: Build and Run Services run: | @@ -108,6 +122,7 @@ jobs: MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} SIGNALLING_SERVICE_URL: ${{ vars.SIGNALLING_SERVICE_URL }} + EXECUTION_SERVICE_URL: ${{ vars.EXECUTION_SERVICE_URL }} run: | echo "Testing Question Service..." curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up" @@ -117,6 +132,8 @@ jobs: curl -fsSL -o /dev/null $FRONTEND_URL && echo "Frontend is up" echo "Testing History Service..." curl -fsSL -o /dev/null $HISTORY_SERVICE_URL && echo "History Service is up" + echo "Testing Execution Service..." + curl -fsSL -o /dev/null $EXECUTION_SERVICE_URL && echo "Execution Service is up" echo "Testing Matching Service..." if ! (echo "Hello" | websocat $MATCHING_SERVICE_URL); then echo "WebSocket for Matching Service is not live" From d3d5b055b01f033cdc589dd3ceff90bc7bae3bfe Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Sun, 3 Nov 2024 23:25:52 +0800 Subject: [PATCH 06/16] Link frontend code execution to execution service and sync across matched users --- apps/frontend/package.json | 3 + apps/frontend/pnpm-lock.yaml | 439 ++++++++++-------- .../src/app/collaboration/[id]/page.tsx | 258 +++++++--- .../src/app/collaboration/[id]/styles.scss | 40 +- apps/frontend/src/app/services/execute.ts | 127 +++++ .../CollaborativeEditor.tsx | 54 ++- apps/frontend/src/middleware.ts | 8 +- 7 files changed, 650 insertions(+), 279 deletions(-) create mode 100644 apps/frontend/src/app/services/execute.ts diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 94cc9de16b..91d6de3d75 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ant-design/icons": "^5.5.1", "@ant-design/nextjs-registry": "^1.0.1", + "@codemirror/commands": "^6.7.1", "@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-java": "^6.0.1", @@ -18,6 +19,7 @@ "@codemirror/lang-python": "^6.1.6", "@codemirror/language": "^6.10.3", "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.1", "antd": "^5.20.6", "codemirror": "^6.0.1", "next": "14.2.13", @@ -32,6 +34,7 @@ "yjs": "^13.6.20" }, "devDependencies": { + "@types/codemirror": "^5.60.15", "@types/node": "^20", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index c114790857..8f8e07342a 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -10,10 +10,13 @@ importers: dependencies: '@ant-design/icons': specifier: ^5.5.1 - version: 5.5.1(react-dom@18.2.0)(react@18.2.0) + version: 5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@ant-design/nextjs-registry': specifier: ^1.0.1 - version: 1.0.1(@ant-design/cssinjs@1.21.1)(antd@5.20.6)(next@14.2.13)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.1(@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@codemirror/commands': + specifier: ^6.7.1 + version: 6.7.1 '@codemirror/lang-cpp': specifier: ^6.0.2 version: 6.0.2 @@ -35,15 +38,18 @@ importers: '@codemirror/state': specifier: ^6.4.1 version: 6.4.1 + '@codemirror/view': + specifier: ^6.34.1 + version: 6.34.1 antd: specifier: ^5.20.6 - version: 5.20.6(react-dom@18.2.0)(react@18.2.0) + version: 5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) codemirror: specifier: ^6.0.1 version: 6.0.1(@lezer/common@1.2.3) next: specifier: 14.2.13 - version: 14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2) + version: 14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2) react: specifier: ^18.2.0 version: 18.2.0 @@ -72,6 +78,9 @@ importers: specifier: ^13.6.20 version: 13.6.20 devDependencies: + '@types/codemirror': + specifier: ^5.60.15 + version: 5.60.15 '@types/node': specifier: ^20 version: 20.0.0 @@ -389,6 +398,12 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@types/codemirror@5.60.15': + resolution: {integrity: sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -404,6 +419,9 @@ packages: '@types/react@18.3.8': resolution: {integrity: sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==} + '@types/tern@0.23.9': + resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} + '@typescript-eslint/eslint-plugin@8.8.0': resolution: {integrity: sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1962,22 +1980,22 @@ snapshots: dependencies: '@ctrl/tinycolor': 3.6.1 - '@ant-design/cssinjs-utils@1.1.0(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/cssinjs-utils@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@babel/runtime': 7.25.7 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@ant-design/cssinjs@1.21.1(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 classnames: 2.5.1 csstype: 3.1.3 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) stylis: 4.3.4 @@ -1988,21 +2006,21 @@ snapshots: '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons@5.5.1(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/icons@5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@ant-design/colors': 7.1.0 '@ant-design/icons-svg': 4.4.2 '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@ant-design/nextjs-registry@1.0.1(@ant-design/cssinjs@1.21.1)(antd@5.20.6)(next@14.2.13)(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/nextjs-registry@1.0.1(@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) - antd: 5.20.6(react-dom@18.2.0)(react@18.2.0) - next: 14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + antd: 5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next: 14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2242,19 +2260,19 @@ snapshots: dependencies: '@babel/runtime': 7.25.7 - '@rc-component/color-picker@2.0.1(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/color-picker@2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@ant-design/fast-color': 2.0.6 '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/context@1.4.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/context@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2262,48 +2280,48 @@ snapshots: dependencies: '@babel/runtime': 7.25.7 - '@rc-component/mutate-observer@1.1.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/portal@1.1.2(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/portal@1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/qrcode@1.0.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/qrcode@1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/tour@1.15.1(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/tour@1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/trigger@2.2.3(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/trigger@2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2318,6 +2336,12 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.7.0 + '@types/codemirror@5.60.15': + dependencies: + '@types/tern': 0.23.9 + + '@types/estree@1.0.6': {} + '@types/json5@0.0.29': {} '@types/node@20.0.0': {} @@ -2333,7 +2357,11 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0)(eslint@8.0.0)(typescript@5.0.2)': + '@types/tern@0.23.9': + dependencies: + '@types/estree': 1.0.6 + + '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0)(typescript@5.0.2)': dependencies: '@eslint-community/regexpp': 4.11.1 '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) @@ -2346,6 +2374,7 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2358,6 +2387,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.8.0 debug: 4.3.7 eslint: 8.0.0 + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2373,6 +2403,7 @@ snapshots: '@typescript-eslint/utils': 8.8.0(eslint@8.0.0)(typescript@5.0.2) debug: 4.3.7 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - eslint @@ -2390,6 +2421,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2435,55 +2467,55 @@ snapshots: ansi-styles@6.2.1: {} - antd@5.20.6(react-dom@18.2.0)(react@18.2.0): + antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@ant-design/colors': 7.1.0 - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) - '@ant-design/cssinjs-utils': 1.1.0(react-dom@18.2.0)(react@18.2.0) - '@ant-design/icons': 5.5.1(react-dom@18.2.0)(react@18.2.0) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/cssinjs-utils': 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/icons': 5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@ant-design/react-slick': 1.1.2(react@18.2.0) '@babel/runtime': 7.25.7 '@ctrl/tinycolor': 3.6.1 - '@rc-component/color-picker': 2.0.1(react-dom@18.2.0)(react@18.2.0) - '@rc-component/mutate-observer': 1.1.0(react-dom@18.2.0)(react@18.2.0) - '@rc-component/qrcode': 1.0.0(react-dom@18.2.0)(react@18.2.0) - '@rc-component/tour': 1.15.1(react-dom@18.2.0)(react@18.2.0) - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/color-picker': 2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/mutate-observer': 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/qrcode': 1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/tour': 1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 copy-to-clipboard: 3.3.3 dayjs: 1.11.13 - rc-cascader: 3.28.1(react-dom@18.2.0)(react@18.2.0) - rc-checkbox: 3.3.0(react-dom@18.2.0)(react@18.2.0) - rc-collapse: 3.7.3(react-dom@18.2.0)(react@18.2.0) - rc-dialog: 9.5.2(react-dom@18.2.0)(react@18.2.0) - rc-drawer: 7.2.0(react-dom@18.2.0)(react@18.2.0) - rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-field-form: 2.4.0(react-dom@18.2.0)(react@18.2.0) - rc-image: 7.9.0(react-dom@18.2.0)(react@18.2.0) - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-input-number: 9.2.0(react-dom@18.2.0)(react@18.2.0) - rc-mentions: 2.15.0(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-notification: 5.6.2(react-dom@18.2.0)(react@18.2.0) - rc-pagination: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-picker: 4.6.15(dayjs@1.11.13)(react-dom@18.2.0)(react@18.2.0) - rc-progress: 4.0.0(react-dom@18.2.0)(react@18.2.0) - rc-rate: 2.13.0(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-segmented: 2.3.0(react-dom@18.2.0)(react@18.2.0) - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-slider: 11.1.6(react-dom@18.2.0)(react@18.2.0) - rc-steps: 6.0.1(react-dom@18.2.0)(react@18.2.0) - rc-switch: 4.1.0(react-dom@18.2.0)(react@18.2.0) - rc-table: 7.45.7(react-dom@18.2.0)(react@18.2.0) - rc-tabs: 15.1.1(react-dom@18.2.0)(react@18.2.0) - rc-textarea: 1.8.2(react-dom@18.2.0)(react@18.2.0) - rc-tooltip: 6.2.1(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-tree-select: 5.23.0(react-dom@18.2.0)(react@18.2.0) - rc-upload: 4.7.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-cascader: 3.28.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-checkbox: 3.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-collapse: 3.7.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dialog: 9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-drawer: 7.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dropdown: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-field-form: 2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-image: 7.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input-number: 9.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-mentions: 2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-notification: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-pagination: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-picker: 4.6.15(dayjs@1.11.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-progress: 4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-rate: 2.13.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-segmented: 2.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-slider: 11.1.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-steps: 6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-switch: 4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-table: 7.45.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tabs: 15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tooltip: 6.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree-select: 5.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-upload: 4.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scroll-into-view-if-needed: 3.1.0 @@ -2864,15 +2896,16 @@ snapshots: dependencies: '@next/eslint-plugin-next': 14.2.13 '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0)(eslint@8.0.0)(typescript@5.0.2) + '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0)(typescript@5.0.2) '@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-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-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)(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-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) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - eslint-import-resolver-webpack @@ -2887,38 +2920,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0)(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)(eslint@8.0.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.0.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(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@8.0.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 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) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(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@8.0.0): dependencies: - '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) debug: 3.2.7 + optionalDependencies: + '@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-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)(eslint@8.0.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0)(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)(eslint@8.0.0): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -2927,7 +2961,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(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@8.0.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -2937,6 +2971,8 @@ snapshots: object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3431,7 +3467,7 @@ snapshots: natural-compare@1.4.0: {} - next@14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2): + next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2): dependencies: '@next/env': 14.2.13 '@swc/helpers': 0.5.5 @@ -3441,7 +3477,6 @@ snapshots: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - sass: 1.79.2 styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: '@next/swc-darwin-arm64': 14.2.13 @@ -3453,6 +3488,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.13 '@next/swc-win32-ia32-msvc': 14.2.13 '@next/swc-win32-x64-msvc': 14.2.13 + sass: 1.79.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -3558,321 +3594,322 @@ snapshots: dependencies: safe-buffer: 5.2.1 - rc-cascader@3.28.1(react-dom@18.2.0)(react@18.2.0): + rc-cascader@3.28.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 array-tree-filter: 2.1.0 classnames: 2.5.1 - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-checkbox@3.3.0(react-dom@18.2.0)(react@18.2.0): + rc-checkbox@3.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-collapse@3.7.3(react-dom@18.2.0)(react@18.2.0): + rc-collapse@3.7.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-dialog@9.5.2(react-dom@18.2.0)(react@18.2.0): + rc-dialog@9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-drawer@7.2.0(react-dom@18.2.0)(react@18.2.0): + rc-drawer@7.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-dropdown@4.2.0(react-dom@18.2.0)(react@18.2.0): + rc-dropdown@4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-field-form@2.4.0(react-dom@18.2.0)(react@18.2.0): + rc-field-form@2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 '@rc-component/async-validator': 5.0.4 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-image@7.9.0(react-dom@18.2.0)(react@18.2.0): + rc-image@7.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-dialog: 9.5.2(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-dialog: 9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-input-number@9.2.0(react-dom@18.2.0)(react@18.2.0): + rc-input-number@9.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 '@rc-component/mini-decimal': 1.1.0 classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-input@1.6.3(react-dom@18.2.0)(react@18.2.0): + rc-input@1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-mentions@2.15.0(react-dom@18.2.0)(react@18.2.0): + rc-mentions@2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-textarea: 1.8.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-menu@9.14.1(react-dom@18.2.0)(react@18.2.0): + rc-menu@9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-motion@2.9.3(react-dom@18.2.0)(react@18.2.0): + rc-motion@2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-notification@5.6.2(react-dom@18.2.0)(react@18.2.0): + rc-notification@5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-overflow@1.3.2(react-dom@18.2.0)(react@18.2.0): + rc-overflow@1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-pagination@4.2.0(react-dom@18.2.0)(react@18.2.0): + rc-pagination@4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-picker@4.6.15(dayjs@1.11.13)(react-dom@18.2.0)(react@18.2.0): + rc-picker@4.6.15(dayjs@1.11.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - dayjs: 1.11.13 - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + dayjs: 1.11.13 - rc-progress@4.0.0(react-dom@18.2.0)(react@18.2.0): + rc-progress@4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-rate@2.13.0(react-dom@18.2.0)(react@18.2.0): + rc-rate@2.13.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-resize-observer@1.4.0(react-dom@18.2.0)(react@18.2.0): + rc-resize-observer@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) resize-observer-polyfill: 1.5.1 - rc-segmented@2.3.0(react-dom@18.2.0)(react@18.2.0): + rc-segmented@2.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-select@14.15.2(react-dom@18.2.0)(react@18.2.0): + rc-select@14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-slider@11.1.6(react-dom@18.2.0)(react@18.2.0): + rc-slider@11.1.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-steps@6.0.1(react-dom@18.2.0)(react@18.2.0): + rc-steps@6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-switch@4.1.0(react-dom@18.2.0)(react@18.2.0): + rc-switch@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-table@7.45.7(react-dom@18.2.0)(react@18.2.0): + rc-table@7.45.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/context': 1.4.0(react-dom@18.2.0)(react@18.2.0) + '@rc-component/context': 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tabs@15.1.1(react-dom@18.2.0)(react@18.2.0): + rc-tabs@15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-dropdown: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-textarea@1.8.2(react-dom@18.2.0)(react@18.2.0): + rc-textarea@1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tooltip@6.2.1(react-dom@18.2.0)(react@18.2.0): + rc-tooltip@6.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tree-select@5.23.0(react-dom@18.2.0)(react@18.2.0): + rc-tree-select@5.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tree@5.9.0(react-dom@18.2.0)(react@18.2.0): + rc-tree@5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-upload@4.7.0(react-dom@18.2.0)(react@18.2.0): + rc-upload@4.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-util@5.43.0(react-dom@18.2.0)(react@18.2.0): + rc-util@5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 18.3.1 - rc-virtual-list@3.14.8(react-dom@18.2.0)(react@18.2.0): + rc-virtual-list@3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 41c56023f0..83758ead95 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -13,6 +13,7 @@ import { TabsProps, Tag, Typography, + Spin, } from "antd"; import { Content } from "antd/es/layout/layout"; import "./styles.scss"; @@ -35,12 +36,15 @@ import CollaborativeEditor, { import { CreateOrUpdateHistory } from "@/app/services/history"; import { Language } from "@codemirror/language"; import { WebrtcProvider } from "y-webrtc"; +import { ExecuteVisibleAndCustomTests, ExecuteVisibleAndHiddenTestsAndSubmit, ExecutionResults, GetVisibleTests, isTestResult, SubmissionHiddenTestResultsAndStatus, SubmissionResults, Test, TestData, TestResult } from "@/app/services/execute"; interface CollaborationProps {} export default function CollaborationPage(props: CollaborationProps) { const router = useRouter(); const providerRef = useRef(null); + const submissionProviderRef = useRef(null); + const executionProviderRef = useRef(null); const editorRef = useRef(null); @@ -60,7 +64,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("Javascript"); // 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( @@ -82,10 +86,14 @@ export default function CollaborationPage(props: CollaborationProps) { undefined ); - // Manual test case states + // Test case states const [manualTestCase, setManualTestCase] = useState( undefined ); + const [visibleTestCases, setVisibleTestCases] = useState([]); + const [isLoadingTestCase, setIsLoadingTestCase] = useState(false); + const [isLoadingSubmission, setIsLoadingSubmission] = useState(false); + const [submissionHiddenTestResultsAndStatus, setSubmissionHiddenTestResultsAndStatus] = useState(undefined); // End Button Modal state const [isModalOpen, setIsModalOpen] = useState(false); @@ -141,34 +149,99 @@ export default function CollaborationPage(props: CollaborationProps) { }); }; - const sendCodeSavedStatusToMatchedUser = () => { + const infoMessage = (message: string) => { + messageApi.open({ + type: "info", + content: message, + }); + } + + const sendSubmissionResultsToMatchedUser = (data: SubmissionResults) => { if (!providerRef.current) { throw new Error("Provider not initialized"); } - providerRef.current.awareness.setLocalStateField("codeSavedStatus", true); - }; + providerRef.current.awareness.setLocalStateField("submissionResultsState", { + submissionResults: data, + id: Date.now(), + }); + } + + const sendExecutionResultsToMatchedUser = (data: ExecutionResults) => { + if (!providerRef.current) { + throw new Error("Provider not initialized"); + } + providerRef.current.awareness.setLocalStateField("executionResultsState", { + executionResults: data, + id: Date.now(), + }); + } + + const updateSubmissionResults = (data: SubmissionResults) => { + setSubmissionHiddenTestResultsAndStatus({ + hiddenTestResults: data.hiddenTestResults, + status: data.status, + }); + setVisibleTestCases(data.visibleTestResults); + } + + const updateExecutionResults = (data: ExecutionResults) => { + setVisibleTestCases(data.visibleTestResults); + } + + const handleRunTestCases = async () => { + if (!questionDocRefId) { + throw new Error("Question ID not found"); + } + + setIsLoadingTestCase(true); + try { + const data = await ExecuteVisibleAndCustomTests( + questionDocRefId, + { + code: code, + language: selectedLanguage, + customTestCases: "", + } + ); + setVisibleTestCases(data.visibleTestResults); + infoMessage("Test cases executed. Review the results below.") + sendExecutionResultsToMatchedUser(data); + } finally { + setIsLoadingTestCase(false); + } + } const handleSubmitCode = async () => { - if (!collaborationId) { - throw new Error("Collaboration ID not found"); + if (!questionDocRefId) { + throw new Error("Question ID not found"); + } + + setIsLoadingSubmission(true); + try { + const data = await ExecuteVisibleAndHiddenTestsAndSubmit( + questionDocRefId, + { + title: questionTitle ?? "", + code: code, + language: selectedLanguage, + user: currentUser ?? "", + matchedUser: matchedUser ?? "", + matchId: collaborationId ?? "", + matchedTopics: matchedTopics ?? [], + questionDifficulty: complexity ?? "", + questionTopics: categories, + } + ); + setVisibleTestCases(data.visibleTestResults); + setSubmissionHiddenTestResultsAndStatus({ + hiddenTestResults: data.hiddenTestResults, + status: data.status, + }); + sendSubmissionResultsToMatchedUser(data); + successMessage("Code saved successfully!"); + } finally { + setIsLoadingSubmission(false); } - const data = await CreateOrUpdateHistory( - { - title: questionTitle ?? "", - code: code, - language: selectedLanguage, - user: currentUser ?? "", - matchedUser: matchedUser ?? "", - matchId: collaborationId ?? "", - matchedTopics: matchedTopics ?? [], - questionDocRefId: questionDocRefId ?? "", - questionDifficulty: complexity ?? "", - questionTopics: categories, - }, - collaborationId - ); - successMessage("Code saved successfully!"); - sendCodeSavedStatusToMatchedUser(); }; const handleCodeChange = (code: string) => { @@ -204,6 +277,10 @@ export default function CollaborationPage(props: CollaborationProps) { setDescription(data.description); }); + GetVisibleTests(questionDocRefId).then((data: Test[]) => { + setVisibleTestCases(data); + }); + // Start stopwatch startStopwatch(); }, []); @@ -221,34 +298,44 @@ export default function CollaborationPage(props: CollaborationProps) { } }, [isSessionEndModalOpen, countDown]); - // Tabs component items for testcases - const items: TabsProps["items"] = [ - { - key: "1", - label: "Case 1", - children: ( - - ), // TODO: Setup test-cases in db for each qn and pull/paste here - }, - { - key: "2", - label: "Case 2", - children: ( - - ), - }, - { - key: "3", - label: "Case 3", + // Tabs component items for visibleTestCases + var items: TabsProps["items"] = visibleTestCases.map((item, index) => { + return { + key: index.toString(), + label: `Case ${index + 1}`, children: ( - setManualTestCase(e.target.value)} - placeholder="Input Manual Test Case" - rows={6} - /> +
+ + {isTestResult(item) && ( +
+ + + {item.passed ? "Passed" : "Failed"} + +
+ Actual Output: {item.actual} +
+ {item.error && ( + <> + Error: +
+ {item.error} +
+ + )} +
+ )} +
), - }, - ]; + }; + }); // Handles the cleaning of localstorage variables, stopping the timer & signalling collab user on webrtc // type: "initiator" | "peer" @@ -360,13 +447,19 @@ export default function CollaborationPage(props: CollaborationProps) { Test Cases {/* TODO: Link to execution service for running code against test-cases */} - +
+
+ {isLoadingTestCase && } +
+ +
@@ -383,13 +476,18 @@ export default function CollaborationPage(props: CollaborationProps) { Code
{/* TODO: Link to execution service for code submission */} - +
+
+ {isLoadingSubmission && } +
+ +
{collaborationId && currentUser && selectedLanguage && ( )} +
+ + + {submissionHiddenTestResultsAndStatus ? submissionHiddenTestResultsAndStatus.status : "Not Attempted"} + +
+ {submissionHiddenTestResultsAndStatus && ( + + Passed {submissionHiddenTestResultsAndStatus.hiddenTestResults.passed} / {submissionHiddenTestResultsAndStatus.hiddenTestResults.total} hidden test cases + + )} +
diff --git a/apps/frontend/src/app/collaboration/[id]/styles.scss b/apps/frontend/src/app/collaboration/[id]/styles.scss index 4f7b068e42..1d0ed44b31 100644 --- a/apps/frontend/src/app/collaboration/[id]/styles.scss +++ b/apps/frontend/src/app/collaboration/[id]/styles.scss @@ -22,12 +22,12 @@ } .question-row { - height: 60%; + height: 50%; padding: 1rem 0.25rem 0.25rem; } .test-row { - height: 40%; + height: 50%; padding: 0.25rem; } @@ -197,3 +197,39 @@ .info-modal-icon { color: red; } + +.test-button-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + padding-left: 10px; + + .spinner-container { + width: 24px; + min-height: 24px; + display: flex; + align-items: center; + } +} + +.test-result-container { + margin-top: 10px; +} + +.hidden-test-icon { + margin-right: 5px; +} + +.error-message { + overflow-x: auto; + overflow-y: auto; // Allows vertical scroll if content exceeds max-height + white-space: nowrap; + max-width: 100%; + padding: 4px; + min-height: 50px; // Adjust height as needed + max-height: 150px; // Adjust max height for scrollable area + border: 1px solid #ddd; // Optional: add a border for visibility + border-radius: 4px; // Optional: round the corners slightly + background-color: #f9f9f9; // Optional: light background color +} diff --git a/apps/frontend/src/app/services/execute.ts b/apps/frontend/src/app/services/execute.ts new file mode 100644 index 0000000000..2b568f1a05 --- /dev/null +++ b/apps/frontend/src/app/services/execute.ts @@ -0,0 +1,127 @@ +const EXECUTION_SERVICE_URL = process.env.NEXT_PUBLIC_EXECUTION_SERVICE_URL; + +export interface TestData { + input: string + expected: string +} + +export interface TestResult { + input: string + expected: string + actual: string + passed: boolean + error: string +} + +export type Test = TestResult | TestData + +export const isTestResult = (test: Test): test is TestResult => { + return 'actual' in test && 'passed' in test && 'error' in test; +}; + +export interface GeneralTestResults { + passed: number; + total: number; +} + +export interface SubmissionHiddenTestResultsAndStatus { + hiddenTestResults: GeneralTestResults; + status: string; +} + +export interface SubmissionResults extends SubmissionHiddenTestResultsAndStatus { + visibleTestResults: TestResult[]; +} + +export interface ExecutionResults { + visibleTestResults: TestResult[]; + customTestResults: TestResult[]; +} + +export interface Code { + code: string; + language: string; + customTestCases: string; +} + +export interface Collaboration { + title: string + code: string + language: string + user: string + matchedUser: string + matchId: string + matchedTopics: string[] + questionDifficulty: string + questionTopics: string[] +} + +export const GetVisibleTests = async ( + questionDocRefId: string, +): Promise => { + const response = await fetch( + `${EXECUTION_SERVICE_URL}tests/${questionDocRefId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error fetching test cases: ${response.status} ${response.statusText}` + ); + } +} + +export const ExecuteVisibleAndCustomTests = async ( + questionDocRefId: string, + code: Code, +): Promise => { + const response = await fetch( + `${EXECUTION_SERVICE_URL}tests/${questionDocRefId}/execute`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(code), + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error executing code: ${response.status} ${response.statusText}` + ); + } +} + +export const ExecuteVisibleAndHiddenTestsAndSubmit = async ( + questionDocRefId: string, + collaboration: Collaboration, +): Promise => { + const response = await fetch( + `${EXECUTION_SERVICE_URL}tests/${questionDocRefId}/submit`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(collaboration), + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error submitting code: ${response.status} ${response.statusText}` + ); + } +} diff --git a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx index 5fa8a88fe5..17c2d7b644 100644 --- a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx +++ b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx @@ -15,6 +15,8 @@ import * as Y from "yjs"; import { yCollab } from "y-codemirror.next"; import { WebrtcProvider } from "y-webrtc"; import { EditorView, basicSetup } from "codemirror"; +import { keymap } from "@codemirror/view" +import { indentWithTab } from "@codemirror/commands" import { EditorState, Compartment } from "@codemirror/state"; import { javascript, javascriptLanguage } from "@codemirror/lang-javascript"; import { python, pythonLanguage } from "@codemirror/lang-python"; @@ -25,6 +27,7 @@ import "./styles.scss"; import { message, Select } from "antd"; import { language } from "@codemirror/language"; import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; +import { ExecutionResults, SubmissionResults } from "@/app/services/execute"; interface CollaborativeEditorProps { user: string; @@ -35,6 +38,8 @@ interface CollaborativeEditorProps { providerRef: MutableRefObject; matchedUser: string; onCodeChange: (code: string) => void; + updateSubmissionResults: (results: SubmissionResults) => void; + updateExecutionResults: (results: ExecutionResults) => void; } export interface CollaborativeEditorHandle { @@ -54,7 +59,14 @@ interface Awareness { color: string; colorLight: string; }; - codeSavedStatus: boolean; + submissionResultsState: { + submissionResults: SubmissionResults; + id: number; + }; + executionResultsState: { + executionResults: ExecutionResults; + id: number; + } } export const usercolors = [ @@ -79,7 +91,7 @@ const CollaborativeEditor = forwardRef( ) => { const editorRef = useRef(null); // const providerRef = useRef(null); - const [selectedLanguage, setSelectedLanguage] = useState("JavaScript"); + const [selectedLanguage, setSelectedLanguage] = useState("Python"); let sessionEndNotified = false; const languageConf = new Compartment(); @@ -122,9 +134,10 @@ const CollaborativeEditor = forwardRef( languageLabel = "C++"; languageType = cppLanguage; } else { - newLanguage = javascript(); // Default to JavaScript - languageLabel = "JavaScript"; - languageType = javascriptLanguage; + // Default to Python + newLanguage = python(); + languageLabel = "Python"; + languageType = pythonLanguage; } const stateLanguage = tr.startState.facet(language); @@ -167,6 +180,9 @@ const CollaborativeEditor = forwardRef( }); }; + let latestExecutionId: number = (new Date(0)).getTime(); + let latestSubmissionId: number = (new Date(0)).getTime(); + useImperativeHandle(ref, () => ({ endSession: () => { if (props.providerRef.current) { @@ -237,7 +253,7 @@ const CollaborativeEditor = forwardRef( } }); - // Listener for awareness updates to receive status changes from peers + // Listener for awareness updates to receive submission results from peer provider.awareness.on("update", ({ added, updated }: AwarenessUpdate) => { added .concat(updated) @@ -246,8 +262,14 @@ const CollaborativeEditor = forwardRef( const state = provider.awareness .getStates() .get(clientID) as Awareness; - if (state && state.codeSavedStatus && !state.sessionEnded) { - // Display the received status message + + if ( + state && + state.submissionResultsState && + state.submissionResultsState.id !== latestSubmissionId + ) { + latestSubmissionId = state.submissionResultsState.id; + props.updateSubmissionResults(state.submissionResultsState.submissionResults); messageApi.open({ type: "success", content: `${ @@ -255,6 +277,21 @@ const CollaborativeEditor = forwardRef( } saved code successfully!`, }); } + + if ( + state && + state.executionResultsState && + state.executionResultsState.id !== latestExecutionId + ) { + latestExecutionId = state.executionResultsState.id; + props.updateExecutionResults(state.executionResultsState.executionResults); + messageApi.open({ + type: "info", + content: `${ + props.matchedUser ?? "Peer" + } executed test cases. Review the results below.`, + }); + } }); }); @@ -265,6 +302,7 @@ const CollaborativeEditor = forwardRef( languageConf.of(javascript()), autoLanguage, yCollab(ytext, provider.awareness, { undoManager }), + keymap.of([indentWithTab]), ], }); diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index 5ea9b2212a..a03d5af1c8 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -24,10 +24,10 @@ export default async function middleware(request: NextRequest) { return REDIRECT_TO_LOGIN; } - if (!await isValidToken(TOKEN.value)) { - REDIRECT_TO_LOGIN.cookies.delete("TOKEN"); - return REDIRECT_TO_LOGIN; - } + // if (!await isValidToken(TOKEN.value)) { + // REDIRECT_TO_LOGIN.cookies.delete("TOKEN"); + // return REDIRECT_TO_LOGIN; + // } return NextResponse.next(); From f32dd726fdbdc17c70898a90104e7182e0eb6762 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Sun, 3 Nov 2024 23:27:10 +0800 Subject: [PATCH 07/16] Update submission and execution logic --- apps/execution-service/constants/constant.go | 22 +++++ apps/execution-service/handlers/submit.go | 35 ++++++++ apps/execution-service/models/testResult.go | 6 -- apps/execution-service/utils/executeTest.go | 26 ++---- apps/execution-service/utils/populate.go | 80 ++++++++++++++----- .../utils/validateTestCaseFormat.go | 8 -- 6 files changed, 124 insertions(+), 53 deletions(-) create mode 100644 apps/execution-service/constants/constant.go diff --git a/apps/execution-service/constants/constant.go b/apps/execution-service/constants/constant.go new file mode 100644 index 0000000000..46d3face08 --- /dev/null +++ b/apps/execution-service/constants/constant.go @@ -0,0 +1,22 @@ +package constants + +const ( + JAVA = "Java" + PYTHON = "Python" + GOLANG = "Golang" + JAVASCRIPT = "Javascript" + CPP = "C++" +) + +const ( + ACCEPTED = "Accepted" + ATTEMPTED = "Attempted" +) + +var IS_VALID_LANGUAGE = map[string]bool{ + PYTHON: true, + //JAVA: true, + //GOLANG: true, + //JAVASCRIPT: true, + //CPP: true, +} diff --git a/apps/execution-service/handlers/submit.go b/apps/execution-service/handlers/submit.go index c38c0fb094..20e15754e6 100644 --- a/apps/execution-service/handlers/submit.go +++ b/apps/execution-service/handlers/submit.go @@ -3,8 +3,10 @@ package handlers import ( "bytes" "encoding/json" + "execution-service/constants" "execution-service/models" "execution-service/utils" + "fmt" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" "net/http" @@ -26,6 +28,11 @@ func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(w http.ResponseWriter, r return } + if err := validateCollaborationFields(collab); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + iter := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx) doc, err := iter.Next() if err != nil { @@ -106,6 +113,34 @@ func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(w http.ResponseWriter, r json.NewEncoder(w).Encode(testResults) } +func validateCollaborationFields(collab models.Collaboration) error { + if collab.Title == "" { + return fmt.Errorf("title is required") + } + + if !constants.IS_VALID_LANGUAGE[collab.Language] { + return fmt.Errorf("invalid language") + } + + if collab.User == "" { + return fmt.Errorf("user is required") + } + + if collab.MatchedUser == "" { + return fmt.Errorf("matchedUser is required") + } + + if collab.MatchID == "" { + return fmt.Errorf("matchId is required") + } + + if collab.QuestionDifficulty == "" { + return fmt.Errorf("questionDifficulty is required") + } + + return nil +} + //curl -X POST http://localhost:8083/tests/Yt29JjnIDpRwIlYAX8OF/submit \ //-H "Content-Type: application/json" \ //-d '{ diff --git a/apps/execution-service/models/testResult.go b/apps/execution-service/models/testResult.go index e0843acb72..1e812220d2 100644 --- a/apps/execution-service/models/testResult.go +++ b/apps/execution-service/models/testResult.go @@ -1,11 +1,5 @@ package models -type TestResults struct { - VisibleTestResults []IndividualTestResult `json:"visibleTestResults"` - HiddenTestResults GeneralTestResults `json:"hiddenTestResults"` - CustomTestResults []IndividualTestResult `json:"customTestResults"` -} - type ExecutionResults struct { VisibleTestResults []IndividualTestResult `json:"visibleTestResults"` CustomTestResults []IndividualTestResult `json:"customTestResults"` diff --git a/apps/execution-service/utils/executeTest.go b/apps/execution-service/utils/executeTest.go index b51051ca3e..b411ba7298 100644 --- a/apps/execution-service/utils/executeTest.go +++ b/apps/execution-service/utils/executeTest.go @@ -1,30 +1,18 @@ package utils import ( + "execution-service/constants" "execution-service/execution/python" "execution-service/models" "fmt" ) -const ( - JAVA = "Java" - PYTHON = "Python" - GOLANG = "Golang" - JAVASCRIPT = "Javascript" - CPP = "C++" -) - -const ( - ACCEPTED = "Accepted" - ATTEMPTED = "Attempted" -) - func ExecuteVisibleAndCustomTests(code models.Code, test models.Test) (models.ExecutionResults, error) { var err error var testResults models.ExecutionResults switch code.Language { - case PYTHON: + case constants.PYTHON: testResults, err = getVisibleAndCustomTestResults(code, test, python.RunPythonCode) break default: @@ -42,7 +30,7 @@ func ExecuteVisibleAndHiddenTests(code models.Code, test models.Test) (models.Su var testResults models.SubmissionResults switch code.Language { - case PYTHON: + case constants.PYTHON: testResults, err = getVisibleAndHiddenTestResults(code, test, python.RunPythonCode) break default: @@ -143,14 +131,14 @@ func getVisibleAndHiddenTestResults(code models.Code, test models.Test, return models.SubmissionResults{}, err } - status := ACCEPTED + status := constants.ACCEPTED if hiddenTestResults.Passed != hiddenTestResults.Total { - status = ATTEMPTED + status = constants.ATTEMPTED } - if status == ACCEPTED { + if status == constants.ACCEPTED { for _, testResult := range visibleTestResults { if !testResult.Passed { - status = ATTEMPTED + status = constants.ATTEMPTED break } } diff --git a/apps/execution-service/utils/populate.go b/apps/execution-service/utils/populate.go index df1c386a5e..bff45c414e 100644 --- a/apps/execution-service/utils/populate.go +++ b/apps/execution-service/utils/populate.go @@ -34,9 +34,11 @@ func RepopulateTests(ctx context.Context, client *firestore.Client, { QuestionTitle: "Reverse a String", VisibleTestCases: ` -1 +2 hello olleh +Hannah +hannaH `, HiddenTestCases: ` 2 @@ -55,9 +57,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Linked List Cycle Detection", VisibleTestCases: ` -1 +2 [3,2,0,-4] -> pos = 1 true +[1] +false `, HiddenTestCases: ` 2 @@ -119,9 +123,11 @@ return inputOrOutput == "true" || inputOrOutput == "false" { QuestionTitle: "Roman to Integer", VisibleTestCases: ` -1 +2 III 3 +IV +4 `, HiddenTestCases: ` 2 @@ -147,9 +153,11 @@ return err == nil { QuestionTitle: "Add Binary", VisibleTestCases: ` -1 +2 "11", "1" "100" +"1010", "1011" +"10101" `, HiddenTestCases: ` 2 @@ -170,9 +178,11 @@ return binaryRegex.MatchString(inputOrOutput) { QuestionTitle: "Fibonacci Number", VisibleTestCases: ` -1 +2 0 0 +10 +55 `, HiddenTestCases: ` 2 @@ -193,9 +203,11 @@ return err == nil && num >= 0 { QuestionTitle: "Implement Stack using Queues", VisibleTestCases: ` -1 +2 push(1), push(2), top() 2 +push(1), empty() +false `, HiddenTestCases: ` 2 @@ -233,9 +245,11 @@ return err == nil }, { QuestionTitle: "Combine Two Tables", VisibleTestCases: ` -1 +2 Person: [(1, "Smith", "John"), (2, "Doe", "Jane")], Address: [(1, 1, "NYC", "NY"), (2, 3, "LA", "CA")] [("John", "Smith", "NYC", "NY"), ("Jane", "Doe", null, null)] +Person: [(1, "White", "Mary")], Address: [] +[("Mary", "White", null, null)] `, HiddenTestCases: ` 2 @@ -254,9 +268,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Repeated DNA Sequences", VisibleTestCases: ` -1 +2 AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT ["AAAAACCCCC", "CCCCCAAAAA"] +ACGTACGTACGT +[] `, HiddenTestCases: ` 2 @@ -318,9 +334,11 @@ return true { QuestionTitle: "Course Schedule", VisibleTestCases: ` -1 +2 2, [[1,0]] true +2, [[1,0],[0,1]] +false `, HiddenTestCases: ` 2 @@ -339,9 +357,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "LRU Cache Design", VisibleTestCases: ` -1 +2 put(1, 1), put(2, 2), get(1) 1 +put(1, 1), put(2, 2), put(3, 3), get(2) +-1 `, HiddenTestCases: ` 2 @@ -360,9 +380,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Longest Common Subsequence", VisibleTestCases: ` -1 +2 "abcde", "ace" 3 +"abc", "def" +0 `, HiddenTestCases: ` 2 @@ -381,9 +403,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Rotate Image", VisibleTestCases: ` -1 +2 [[1,2,3],[4,5,6],[7,8,9]] [[7,4,1],[8,5,2],[9,6,3]] +[[1]] +[[1]] `, HiddenTestCases: ` 2 @@ -402,9 +426,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Airplane Seat Assignment Probability", VisibleTestCases: ` -1 +2 1 1.00000 +3 +0.50000 `, HiddenTestCases: ` 2 @@ -423,9 +449,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Validate Binary Search Tree", VisibleTestCases: ` -1 +2 [2,1,3] true +[5,1,4,null,null,3,6] +false `, HiddenTestCases: ` 2 @@ -444,9 +472,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Sliding Window Maximum", VisibleTestCases: ` -1 +2 [1,3,-1,-3,5,3,6,7], k=3 [3,3,5,5,6,7] +[9, 11], k=2 +[11] `, HiddenTestCases: ` 2 @@ -465,9 +495,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "N-Queen Problem", VisibleTestCases: ` -1 +2 4 [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] +2 +[] `, HiddenTestCases: ` 2 @@ -486,9 +518,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Serialize and Deserialize a Binary Tree", VisibleTestCases: ` -1 +2 [1,2,3,null,null,4,5] "1 2 null null 3 4 null null 5 null null" +[] +"null" `, HiddenTestCases: ` 2 @@ -507,9 +541,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Wildcard Matching", VisibleTestCases: ` -1 +2 "aa", "a" false +"aa", "*" +true `, HiddenTestCases: ` 2 @@ -528,9 +564,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Chalkboard XOR Game", VisibleTestCases: ` -1 +2 [1,1,2] false +[1,2,3] +true `, HiddenTestCases: ` 2 @@ -549,9 +587,11 @@ return len(inputOrOutput) > 0 { QuestionTitle: "Trips and Users", VisibleTestCases: ` -1 +2 Trips: [(1, 1, 10, 'NYC', 'completed', '2013-10-01'), (2, 2, 11, 'NYC', 'cancelled_by_driver', '2013-10-01')],Users: [(10, 'No', 'client'), (11, 'No', 'driver')] 0.50 +Trips: [(1, 1, 10, 'NYC', 'completed', '2013-10-03'), (2, 2, 11, 'NYC', 'cancelled_by_client', '2013-10-03')],Users: [(10, 'No', 'client'), (11, 'Yes', 'driver')] +0.00 `, HiddenTestCases: ` 2 diff --git a/apps/execution-service/utils/validateTestCaseFormat.go b/apps/execution-service/utils/validateTestCaseFormat.go index ef03752a1b..e38e0881b4 100644 --- a/apps/execution-service/utils/validateTestCaseFormat.go +++ b/apps/execution-service/utils/validateTestCaseFormat.go @@ -30,10 +30,7 @@ func ValidateTestCaseFormat(testCase string, validateInputCode string, validateO len(lines)) } - println("test1") - for i := 1; i < len(lines); i += 2 { - println("test2") ok, err := validateInputOrOutputFormat(validateInputCode, lines[i]) if err != nil { return false, fmt.Errorf("error validating input: %v", err) @@ -41,7 +38,6 @@ func ValidateTestCaseFormat(testCase string, validateInputCode string, validateO if !ok { return false, fmt.Errorf("test case format is incorrect, input format is invalid") } - println("test3") ok, err = validateInputOrOutputFormat(validateOutputCode, lines[i+1]) if err != nil { return false, fmt.Errorf("error validating output: %v", err) @@ -49,9 +45,7 @@ func ValidateTestCaseFormat(testCase string, validateInputCode string, validateO if !ok { return false, fmt.Errorf("test case format is incorrect, output format is invalid") } - println("test4") } - println("test5") return true, nil } @@ -67,8 +61,6 @@ package main %s `, validateInputOrOutputCode) - println(fullCode) - // Evaluate the function code _, err := i.Eval(fullCode) if err != nil { From ca7864d05d534f8120c1d46a90150f00ce1e9323 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Sun, 3 Nov 2024 23:38:20 +0800 Subject: [PATCH 08/16] Update README APIs --- apps/execution-service/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index fc84f1647a..d0263d82f2 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -25,7 +25,7 @@ The server will be available at http://localhost:8083. To run the application via Docker, run the following command: ```bash -docker build -t question-service . +docker build -t execution-service . ``` ```bash @@ -38,7 +38,8 @@ The server will be available at http://localhost:8083. - `POST /tests/populate` - `GET /tests/{questionDocRefId}/` -- `GET /tests/{questionDocRefId}/execute` +- `POST /tests/{questionDocRefId}/execute` +- `POST /tests/{questionDocRefId}/submit` ## Managing Firebase @@ -80,7 +81,7 @@ The following json format will be returned: ] ``` -`GET /tests/{questionDocRefId}/execute` +`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: @@ -154,6 +155,8 @@ The following json format will be returned: } ``` +`POST /tests/{questionDocRefId}/submit` + To submit a solution and execute visible and hidden test cases via a question ID, run the following command, with custom code and language: ```bash From f79896f9f3c0c3e24271289b3043ced147e27d48 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Mon, 4 Nov 2024 14:06:03 +0800 Subject: [PATCH 09/16] Add sandbox for python execution (to be debugged) --- apps/docker-compose.yml | 9 +++++++++ apps/execution-service/Dockerfile | 6 ++++++ apps/execution-service/execution/python/Dockerfile | 3 +++ apps/execution-service/execution/python/python.go | 12 ++++++++++-- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 apps/execution-service/execution/python/Dockerfile diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index 30d5443d32..a0565fe1dd 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -94,6 +94,7 @@ services: - apps_network volumes: - ./execution-service:/execution-service + - /var/run/docker.sock:/var/run/docker.sock redis: image: redis:latest @@ -103,5 +104,13 @@ services: - 6379:6379 container_name: redis-container + python-sandbox: + build: + context: ./execution-service/execution/python + dockerfile: Dockerfile + networks: + - apps_network + container_name: python-sandbox + networks: apps_network: diff --git a/apps/execution-service/Dockerfile b/apps/execution-service/Dockerfile index 1a0bb66e44..eda8bea8d0 100644 --- a/apps/execution-service/Dockerfile +++ b/apps/execution-service/Dockerfile @@ -2,6 +2,12 @@ FROM golang:1.23 WORKDIR /usr/src/app +# Install Docker CLI +RUN apt-get update && apt-get install -y \ + docker.io \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ diff --git a/apps/execution-service/execution/python/Dockerfile b/apps/execution-service/execution/python/Dockerfile new file mode 100644 index 0000000000..54ff4e8d0e --- /dev/null +++ b/apps/execution-service/execution/python/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.10-slim + +WORKDIR /app diff --git a/apps/execution-service/execution/python/python.go b/apps/execution-service/execution/python/python.go index a8f89316fc..b59f195901 100644 --- a/apps/execution-service/execution/python/python.go +++ b/apps/execution-service/execution/python/python.go @@ -25,8 +25,16 @@ func RunPythonCode(code string, input string) (string, string, error) { return "", "", fmt.Errorf("failed to close temporary file: %w", err) } + // Read the contents of the script file for debugging + content, err := os.ReadFile(tmpFile.Name()) + if err != nil { + return "", "", fmt.Errorf("failed to read temporary file: %w", err) + } + fmt.Printf("Contents of script.py:\n%s\n", content) + // Prepare the command to execute the Python script - cmd := exec.Command("python3", tmpFile.Name()) + cmd := exec.Command("docker", "run", "--rm", "-v", fmt.Sprintf("%s:/app/script.py", tmpFile.Name()), "python-sandbox", "python", "/app/script.py") + //cmd := exec.Command("python3", tmpFile.Name()) cmd.Stdin = bytes.NewBufferString(input) // Capture the output and error @@ -37,7 +45,7 @@ func RunPythonCode(code string, input string) (string, string, error) { // Run the command if err := cmd.Run(); err != nil { - return "", fmt.Sprintf("Command execution failed: %s: %w", errorOutput.String(), 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 From 1716efa6ffc86697e2b8166166f5e1b2809f0902 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Mon, 4 Nov 2024 21:42:30 +0800 Subject: [PATCH 10/16] Dockerise execution of python code --- .../execution/python/python.go | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/apps/execution-service/execution/python/python.go b/apps/execution-service/execution/python/python.go index b59f195901..a8383d737f 100644 --- a/apps/execution-service/execution/python/python.go +++ b/apps/execution-service/execution/python/python.go @@ -3,41 +3,22 @@ package python import ( "bytes" "fmt" - "os" "os/exec" "strings" ) -// RunPythonCode executes the provided Python code with the given input func RunPythonCode(code string, input string) (string, string, error) { - // Create a temporary Python file to execute - tmpFile, err := os.CreateTemp("", "*.py") - if err != nil { - return "", "", fmt.Errorf("failed to create temporary file: %w", err) - } - defer os.Remove(tmpFile.Name()) // Clean up the temporary file afterwards - - // Write the provided code to the temporary file - if _, err := tmpFile.WriteString(code); err != nil { - return "", "", fmt.Errorf("failed to write code to temporary file: %w", err) - } - if err := tmpFile.Close(); err != nil { - return "", "", fmt.Errorf("failed to close temporary file: %w", err) - } - - // Read the contents of the script file for debugging - content, err := os.ReadFile(tmpFile.Name()) - if err != nil { - return "", "", fmt.Errorf("failed to read temporary file: %w", err) - } - fmt.Printf("Contents of script.py:\n%s\n", content) - - // Prepare the command to execute the Python script - cmd := exec.Command("docker", "run", "--rm", "-v", fmt.Sprintf("%s:/app/script.py", tmpFile.Name()), "python-sandbox", "python", "/app/script.py") - //cmd := exec.Command("python3", tmpFile.Name()) + cmd := exec.Command( + "docker", "run", "--rm", + "-i", // allows for standard input to be passed in + "python-sandbox", + "python", "-c", code, + ) + + // Pass in any input data to the Python script cmd.Stdin = bytes.NewBufferString(input) - // Capture the output and error + // Capture output and error var output bytes.Buffer var errorOutput bytes.Buffer cmd.Stdout = &output From fdd7a13baf6d14eee807d0b31ab94bc14bb5818a Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 5 Nov 2024 10:03:41 +0800 Subject: [PATCH 11/16] Update test.yml --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d61a728c7..d0bb079fa6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,13 @@ jobs: env: QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} JWT_SECRET: ${{ secrets.JWT_SECRET }} + EXECUTION_SERVICE_URL: ${{ vars.EXECUTION_SERVICE_URL }} run: | cd ./apps/question-service echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env + echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env - name: Set up credentials env: @@ -129,6 +131,7 @@ jobs: cd ../question-service echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env + echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env cd ../user-service echo "DB_CLOUD_URI=$DB_CLOUD_URI" >> .env From b5324e2bf5837847c607db45c8713472c6c48edd Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 5 Nov 2024 10:18:54 +0800 Subject: [PATCH 12/16] update test.yml --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0bb079fa6..b4ce9d9854 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,10 +27,9 @@ jobs: EXECUTION_SERVICE_URL: ${{ vars.EXECUTION_SERVICE_URL }} run: | cd ./apps/question-service - echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env - echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env + echo "EXECUTION_SERVICE_URL=http://localhost:8083/" >> .env - name: Set up credentials env: From 70c52bdfbaca7880abc43661b3c8741f5e8a194b Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 5 Nov 2024 10:30:10 +0800 Subject: [PATCH 13/16] Update optional population of tests --- .github/workflows/test.yml | 2 +- apps/question-service/tests/read_test.go | 2 +- apps/question-service/utils/populate.go | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4ce9d9854..e7e189fb34 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: cd ./apps/question-service echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env - echo "EXECUTION_SERVICE_URL=http://localhost:8083/" >> .env + echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env - name: Set up credentials env: diff --git a/apps/question-service/tests/read_test.go b/apps/question-service/tests/read_test.go index 2186e49d1a..e6e1fcdb7d 100644 --- a/apps/question-service/tests/read_test.go +++ b/apps/question-service/tests/read_test.go @@ -42,7 +42,7 @@ func TestMain(m *testing.M) { // Returns the docref of one of the questions if a test need it func setupDb(t *testing.T) string { // Repopulate document - utils.Populate(service.Client) + utils.Populate(service.Client, false) coll := service.Client.Collection("questions") if coll == nil { diff --git a/apps/question-service/utils/populate.go b/apps/question-service/utils/populate.go index 87ab2dc5b6..70cdee3db0 100644 --- a/apps/question-service/utils/populate.go +++ b/apps/question-service/utils/populate.go @@ -352,7 +352,7 @@ func repopulateTests(dbClient *firestore.Client) error { return nil } -func Populate(client *firestore.Client) { +func Populate(client *firestore.Client, populateTests bool) { ctx := context.Background() // Run the transaction to delete all questions and add new ones @@ -361,6 +361,11 @@ func Populate(client *firestore.Client) { log.Fatalf("Failed to populate sample questions in transaction: %v", err) } + if !populateTests { + log.Println("Counter reset, all questions deleted and sample questions added successfully in a transaction.") + return + } + // Populate testcases in the execution-service err = repopulateTests(client) if err != nil { From 66be207db45f1c95ab9755ef493db054f4f85e79 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 5 Nov 2024 10:32:55 +0800 Subject: [PATCH 14/16] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7e189fb34..b6839b2f07 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: run: curl -sL firebase.tools | bash - name: Run Go tests with Firebase emulator - run: firebase emulators:exec --only firestore 'cd ./apps/question-service; go test ./...' + run: firebase emulators:exec --only firestore 'cd ./apps/question-service/tests; go test ./...' frontend-unit-tests: runs-on: ubuntu-latest From eab5a68db0c934f9eb615da8194ac27f0d0e8c50 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 5 Nov 2024 10:36:35 +0800 Subject: [PATCH 15/16] Fix populate bug --- apps/question-service/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/question-service/main.go b/apps/question-service/main.go index 349a66402e..cf8bacfec0 100644 --- a/apps/question-service/main.go +++ b/apps/question-service/main.go @@ -45,7 +45,7 @@ func main() { shouldPopulate := flag.Bool("populate", false, "Populate database") flag.Parse() if *shouldPopulate { - utils.Populate(client) + utils.Populate(client, true) return } From 911ff0f2758a4c4cd62f12a706ec4fc68f85e943 Mon Sep 17 00:00:00 2001 From: Ryan Chia Date: Tue, 5 Nov 2024 14:46:20 +0800 Subject: [PATCH 16/16] move decode_test to tests folder --- .github/workflows/test.yml | 2 +- apps/question-service/{utils => tests}/decode_test.go | 9 +++++---- apps/question-service/tests/read_test.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) rename apps/question-service/{utils => tests}/decode_test.go (85%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6839b2f07..90408360b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: run: curl -sL firebase.tools | bash - name: Run Go tests with Firebase emulator - run: firebase emulators:exec --only firestore 'cd ./apps/question-service/tests; go test ./...' + run: firebase emulators:exec --only firestore 'cd ./apps/question-service; go test -v ./tests' frontend-unit-tests: runs-on: ubuntu-latest diff --git a/apps/question-service/utils/decode_test.go b/apps/question-service/tests/decode_test.go similarity index 85% rename from apps/question-service/utils/decode_test.go rename to apps/question-service/tests/decode_test.go index 7ee3744359..60ac0a760c 100644 --- a/apps/question-service/utils/decode_test.go +++ b/apps/question-service/tests/decode_test.go @@ -1,8 +1,9 @@ -package utils +package tests import ( "net/http" "net/http/httptest" + "question-service/utils" "strings" "testing" ) @@ -12,11 +13,11 @@ type CustomObj struct { NumType int64 `json:"num"` } -func TestXd(t *testing.T) { +func TestDecode(t *testing.T) { t.Run("parses string correctly", func(t *testing.T) { var obj CustomObj req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{"str": "asd", "num":64}`)) - err := DecodeJSONBody(nil, req, &obj) + err := utils.DecodeJSONBody(nil, req, &obj) if err != nil { t.Errorf("err should be nil but got %q instead", err) @@ -29,7 +30,7 @@ func TestXd(t *testing.T) { t.Run("fails with incorrect object", func(t *testing.T) { var obj CustomObj req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{"str": "asd", "num":64 `)) - err := DecodeJSONBody(nil, req, &obj) + err := utils.DecodeJSONBody(nil, req, &obj) const expected = "Invalid request payload: unexpected EOF" if err == nil { diff --git a/apps/question-service/tests/read_test.go b/apps/question-service/tests/read_test.go index e6e1fcdb7d..769cb1ded9 100644 --- a/apps/question-service/tests/read_test.go +++ b/apps/question-service/tests/read_test.go @@ -20,7 +20,7 @@ var ctx = context.Background() func TestMain(m *testing.M) { // Set FIRESTORE_EMULATOR_HOST environment variable. - err := os.Setenv("FIRESTORE_EMULATOR_HOST", "localhost:8080") + err := os.Setenv("FIRESTORE_EMULATOR_HOST", "127.0.0.1:8080") if err != nil { log.Fatalf("could not set env %v", err) }