diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3c2d479 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,147 @@ +# CODEOWNERS file for CodeQuest Platform +# This file defines who needs to review changes to different parts of the codebase +# +# For more info about CODEOWNERS file format, see: +# https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-on-github/about-code-owners + +# ============================================================================== +# Global Ownership +# ============================================================================== + +# Default owners for everything in the repo +# These users will be requested for review when someone opens a pull request +* @crisecheverria + +# ============================================================================== +# Root Configuration Files +# ============================================================================== + +# Package management and workspace configuration +package.json @crisecheverria +package-lock.json @crisecheverria +/.gitignore @crisecheverria +/README.md @crisecheverria +/LICENSE @crisecheverria + +# ============================================================================== +# CI/CD and GitHub Configuration +# ============================================================================== + +# GitHub workflows and actions +/.github/ @crisecheverria + +# CODEOWNERS file itself (requires admin approval) +/.github/CODEOWNERS @crisecheverria + +# ============================================================================== +# Backend Package +# ============================================================================== + +# Backend package configuration +/packages/backend/package.json @crisecheverria +/packages/backend/tsconfig.json @crisecheverria +/packages/backend/jest.config.js @crisecheverria +/packages/backend/.eslintrc.js @crisecheverria + +# Backend source code - main areas +/packages/backend/src/ @crisecheverria + +# Backend models (database schema changes require careful review) +/packages/backend/src/models/ @crisecheverria + +# Backend API routes (public interface changes) +/packages/backend/src/routes/ @crisecheverria + +# Backend services (business logic) +/packages/backend/src/services/ @crisecheverria + +# Backend configuration (security sensitive) +/packages/backend/src/config/ @crisecheverria + +# Database migrations and scripts +/packages/backend/src/scripts/ @crisecheverria +/packages/backend/src/db/ @crisecheverria + +# Backend middleware (auth, error handling) +/packages/backend/src/middleware/ @crisecheverria + +# Backend tests +/packages/backend/src/__tests__/ @crisecheverria + +# Dockerfiles and containers (deployment critical) +/packages/backend/Dockerfile* @crisecheverria +/packages/backend/*.sh @crisecheverria + +# Go executor (performance critical) +/packages/backend/go-executor/ @crisecheverria + +# Data files (challenge and concept definitions) +/packages/backend/data/ @crisecheverria + +# ============================================================================== +# Frontend Package +# ============================================================================== + +# Frontend package configuration +/packages/frontend/package.json @crisecheverria +/packages/frontend/tsconfig.json @crisecheverria +/packages/frontend/vite.config.ts @crisecheverria +/packages/frontend/vitest.config.ts @crisecheverria +/packages/frontend/tailwind.config.js @crisecheverria +/packages/frontend/postcss.config.js @crisecheverria +/packages/frontend/eslint.config.js @crisecheverria + +# Frontend source code +/packages/frontend/src/ @crisecheverria + +# Frontend components +/packages/frontend/src/components/ @crisecheverria + +# Frontend API layer +/packages/frontend/src/api/ @crisecheverria + +# Frontend types (interface definitions) +/packages/frontend/src/types/ @crisecheverria + +# Frontend tests +/packages/frontend/src/__tests__/ @crisecheverria + +# Frontend build configuration +/packages/frontend/index.html @crisecheverria +/packages/frontend/public/ @crisecheverria + +# ============================================================================== +# Documentation +# ============================================================================== + +# Documentation files +*.md @crisecheverria +/docs/ @crisecheverria + +# ============================================================================== +# Security and Configuration +# ============================================================================== + +# Environment files and security configs +*.env* @crisecheverria +.audit-ci.json @crisecheverria + +# ============================================================================== +# Special Cases - Multiple Reviewers (Examples for future use) +# ============================================================================== + +# Uncomment and modify these when you have multiple team members: +# +# Critical security files (require multiple approvals) +# /packages/backend/src/middleware/auth.ts @crisecheverria @security-team +# /packages/backend/src/config/index.ts @crisecheverria @security-team +# +# Database models (require backend team review) +# /packages/backend/src/models/ @crisecheverria @backend-team +# +# Frontend components (require frontend team review) +# /packages/frontend/src/components/ @crisecheverria @frontend-team +# +# API contracts (require both backend and frontend teams) +# /packages/backend/src/routes/ @crisecheverria @backend-team @frontend-team +# /packages/frontend/src/api/ @crisecheverria @backend-team @frontend-team \ No newline at end of file diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 9615d7f..aea79d6 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -3,24 +3,28 @@ name: Backend Tests on: push: branches: [main, develop] - paths: - - 'packages/backend/**' - - '.github/workflows/test-backend.yml' + paths: + - "packages/backend/**" + - ".github/workflows/test-backend.yml" pull_request: branches: [main] - paths: - - 'packages/backend/**' - - '.github/workflows/test-backend.yml' + paths: + - "packages/backend/**" + - ".github/workflows/test-backend.yml" + +permissions: + contents: read + pull-requests: write jobs: test: name: Backend Tests runs-on: ubuntu-latest - + strategy: matrix: node-version: [18.x, 20.x] - + services: mongodb: image: mongo:7.0 @@ -34,43 +38,43 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - + cache: "npm" + - name: Setup Go uses: actions/setup-go@v4 with: - go-version: '1.21' - + go-version: "1.21" + - name: Verify Go Installation run: go version - + - name: Install dependencies run: | npm ci npm ci -w packages/backend - + - name: Run ESLint run: npm run lint -w packages/backend - + - name: Build backend run: npm run build -w packages/backend - + - name: Install MongoDB tools run: | wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list sudo apt-get update sudo apt-get install -y mongodb-mongosh - + - name: Wait for MongoDB run: | until mongosh --host localhost:27017 --username admin --password password --authenticationDatabase admin --eval "db.adminCommand('ping')" --quiet; do @@ -78,7 +82,7 @@ jobs: sleep 5 done echo "MongoDB is ready" - + - name: Run unit tests run: npm run test:unit -w packages/backend env: @@ -88,8 +92,8 @@ jobs: USE_NATIVE_GO_EXECUTOR: false DOCKER_TIMEOUT: 10000 TEST_VERBOSE: false - - - name: Run integration tests + + - name: Run integration tests run: npm run test:integration -w packages/backend env: NODE_ENV: test @@ -98,7 +102,7 @@ jobs: USE_NATIVE_GO_EXECUTOR: false DOCKER_TIMEOUT: 15000 TEST_VERBOSE: false - + - name: Run all tests with coverage run: npm run test:coverage -w packages/backend env: @@ -108,7 +112,7 @@ jobs: USE_NATIVE_GO_EXECUTOR: false DOCKER_TIMEOUT: 15000 TEST_VERBOSE: false - + - name: Comment coverage on PR if: github.event_name == 'pull_request' uses: romeovs/lcov-reporter-action@v0.3.1 @@ -116,29 +120,29 @@ jobs: lcov-file: ./packages/backend/coverage/lcov.info github-token: ${{ secrets.GITHUB_TOKEN }} title: Backend Test Coverage Report - + security: name: Security Audit runs-on: ubuntu-latest needs: test - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18.x' - cache: 'npm' - + node-version: "18.x" + cache: "npm" + - name: Install dependencies run: | npm ci npm ci -w packages/backend - + - name: Run security audit run: npm audit --audit-level moderate -w packages/backend - + - name: Run dependency check - run: npx audit-ci --config .audit-ci.json -w packages/backend || true \ No newline at end of file + run: npx audit-ci --config .audit-ci.json -w packages/backend || true diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 54aac33..f013135 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -3,60 +3,64 @@ name: Frontend Tests on: push: branches: [main, develop] - paths: - - 'packages/frontend/**' - - '.github/workflows/test-frontend.yml' + paths: + - "packages/frontend/**" + - ".github/workflows/test-frontend.yml" pull_request: branches: [main] - paths: - - 'packages/frontend/**' - - '.github/workflows/test-frontend.yml' + paths: + - "packages/frontend/**" + - ".github/workflows/test-frontend.yml" + +permissions: + contents: read + pull-requests: write jobs: test: name: Frontend Tests runs-on: ubuntu-latest - + strategy: matrix: node-version: [18.x, 20.x] - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - + cache: "npm" + - name: Install dependencies run: | npm ci npm ci -w packages/frontend - + - name: Run ESLint run: npm run lint -w packages/frontend - + - name: Run TypeScript check run: npx tsc --noEmit -p packages/frontend - + - name: Run unit tests run: npm run test:unit -w packages/frontend env: NODE_ENV: test - - - name: Run integration tests + + - name: Run integration tests run: npm run test:integration -w packages/frontend env: NODE_ENV: test - + - name: Run all tests with coverage run: npm run test:coverage -w packages/frontend env: NODE_ENV: test - + - name: Comment coverage on PR if: github.event_name == 'pull_request' uses: romeovs/lcov-reporter-action@v0.3.1 @@ -64,34 +68,34 @@ jobs: lcov-file: ./packages/frontend/coverage/lcov.info github-token: ${{ secrets.GITHUB_TOKEN }} title: Frontend Test Coverage Report - + - name: Build frontend run: npm run build -w packages/frontend env: NODE_ENV: production - + security: name: Security Audit runs-on: ubuntu-latest needs: test - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18.x' - cache: 'npm' - + node-version: "18.x" + cache: "npm" + - name: Install dependencies run: | npm ci npm ci -w packages/frontend - + - name: Run security audit run: npm audit --audit-level moderate -w packages/frontend - + - name: Run dependency check - run: npx audit-ci --config .audit-ci.json -w packages/frontend || true \ No newline at end of file + run: npx audit-ci --config .audit-ci.json -w packages/frontend || true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e1aa6f8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Codequest Platform - Agent Guidelines + +## Build/Test Commands + +- **All**: `npm run build` | `npm run test` | `npm run lint` | `npm run dev` +- **Backend**: `npm run build:backend` | `npm run test -w packages/backend` | `npm run dev:backend` +- **Frontend**: `npm run build:frontend` | `npm run test -w packages/frontend` | `npm run dev:frontend` +- **Single tests**: `npm run test:unit -w packages/backend` | `npm run test:integration -w packages/frontend` +- **Test with coverage**: `npm run test:coverage -w packages/backend` | `npm run test:coverage -w packages/frontend` + +## Code Style & Standards + +- **TypeScript**: Strict mode enabled, use proper types (avoid `any` except in tests) +- **Imports**: Use ES6 imports, group by: external libs โ†’ internal modules โ†’ types +- **Naming**: camelCase for variables/functions, PascalCase for classes/components/interfaces +- **Error Handling**: Use try-catch blocks, return structured errors with status codes +- **Validation**: Use Zod schemas for API validation, define interfaces before implementation +- **Models**: Use Mongoose with TypeScript interfaces, include proper indexes and pre-hooks +- **Components**: Functional components with TypeScript props, use const assertions for mappings +- **File Structure**: Group related files, use index.ts for exports, separate types from implementation +- **Testing**: Separate unit/integration tests, use descriptive test names, mock external dependencies +- **Comments**: Document complex business logic, avoid obvious comments, use JSDoc for public APIs +- **Async**: Use async/await over promises, handle errors at appropriate levels diff --git a/packages/backend/.env.example b/packages/backend/.env.example index 3866de2..375c591 100644 --- a/packages/backend/.env.example +++ b/packages/backend/.env.example @@ -5,13 +5,22 @@ MONGODB_PASSWORD=password MONGODB_AUTH_SOURCE=admin JWT_SECRET=your-secret-key NODE_ENV=development -DOCKER_MEMORY_LIMIT=129m -DOCKER_CPU_QUOTA=100001 -DOCKER_TIMEOUT=30001 +# Docker Configuration - Production optimized values +DOCKER_MEMORY_LIMIT=512m +DOCKER_CPU_QUOTA=200000 +DOCKER_TIMEOUT=60000 +DOCKER_GO_TIMEOUT=120000 +# Go Executor Configuration +USE_NATIVE_GO_EXECUTOR=true +NATIVE_GO_TIMEOUT=90000 +# Runner Configuration +RUNNER_TEMP_DIR=/tmp/codequest +# GitHub OAuth GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret GITHUB_CALLBACK_URL=http://localhost:3001/api/auth/github/callback JWT_EXPIRES_IN=8d +# AI Configuration AI_API_KEY=your_api_key AI_API_ENDPOINT=https://api.openai.com/v1/chat/completions USE_EXTERNAL_AI_API=true diff --git a/packages/backend/Dockerfile.go-runner b/packages/backend/Dockerfile.go-runner index abefabf..c605ffa 100644 --- a/packages/backend/Dockerfile.go-runner +++ b/packages/backend/Dockerfile.go-runner @@ -3,31 +3,34 @@ FROM golang:1.21-alpine # Install necessary packages for faster builds RUN apk add --no-cache git ca-certificates -# Pre-warm the module cache with common dependencies -RUN go env -w GOPROXY=https://proxy.golang.org,direct && \ - go env -w GOSUMDB=sum.golang.org && \ - go env -w GOCACHE=/tmp/gocache && \ - go env -w GOMODCACHE=/tmp/gomodcache +# Create persistent cache directories with proper permissions +RUN mkdir -p /go/pkg/mod /go/cache /code/temp && \ + chmod -R 777 /go/pkg/mod /go/cache /code/temp -# Create working directory and cache directories -WORKDIR /code -RUN mkdir -p /tmp/gocache /tmp/gomodcache /code/temp && \ - chmod 777 /tmp/gocache /tmp/gomodcache /code/temp +# Set Go environment variables for optimal performance +ENV GOMODCACHE=/go/pkg/mod +ENV GOCACHE=/go/cache +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 +ENV GOPROXY=https://proxy.golang.org,direct +ENV GOSUMDB=sum.golang.org +ENV GO111MODULE=on + +# Pre-warm cache with common packages used in coding challenges +WORKDIR /prebuild +RUN printf 'module prebuild\ngo 1.21\n\nrequire (\n\tgolang.org/x/text v0.14.0\n)' > go.mod && \ + printf 'package main\n\nimport (\n\t"encoding/json"\n\t"fmt"\n\t"reflect"\n\t"sort"\n\t"strings"\n\t"strconv"\n\t"math"\n\t"regexp"\n\t"time"\n\t"unicode"\n\t"golang.org/x/text/unicode/norm"\n)\n\nfunc main() {\n\t_ = json.Marshal\n\t_ = fmt.Sprintf\n\t_ = reflect.DeepEqual\n\t_ = sort.Ints\n\t_ = strings.Join\n\t_ = strconv.Itoa\n\t_ = math.Max\n\t_ = regexp.Compile\n\t_ = time.Now\n\t_ = unicode.IsLetter\n\t_ = norm.NFC\n}' > main.go && \ + go mod tidy && \ + go build -ldflags "-s -w" -o /dev/null main.go && \ + rm -rf /prebuild -# Copy and build the precompiled Go executor +# Copy and setup the precompiled Go executor COPY go-executor/go-executor /usr/local/bin/go-executor RUN chmod +x /usr/local/bin/go-executor -# Pre-compile common standard library packages to speed up builds -RUN echo 'package main; import ("encoding/json"; "fmt"; "reflect"); func main() { _ = json.Marshal; _ = fmt.Sprintf; _ = reflect.DeepEqual }' > /tmp/warmup.go && \ - cd /tmp && go build -o /dev/null warmup.go && rm warmup.go - -# Set Go environment variables for faster compilation -ENV CGO_ENABLED=0 -ENV GOOS=linux -ENV GOARCH=amd64 -ENV GOCACHE=/tmp/gocache -ENV GOMODCACHE=/tmp/gomodcache +# Create working directory +WORKDIR /code # Default command CMD ["go-executor"] \ No newline at end of file diff --git a/packages/backend/build-optimized-go-runner.sh b/packages/backend/build-optimized-go-runner.sh new file mode 100755 index 0000000..8c70d82 --- /dev/null +++ b/packages/backend/build-optimized-go-runner.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Build optimized Go runner Docker image with performance enhancements + +set -e + +echo "๐Ÿ—๏ธ Building optimized Go challenge execution environment..." + +# Build the Go executor binary +echo "๐Ÿ“ฆ Building Go executor binary..." +cd go-executor +go build -ldflags "-s -w" -o go-executor executor.go +echo "โœ… Go executor binary built successfully" + +# Build the Docker image +echo "๐Ÿณ Building Docker image: go-runner..." +cd .. +docker build -t go-runner -f Dockerfile.go-runner . + +echo "๐Ÿ“Š Docker image built. Checking size..." +docker images go-runner + +echo "๐Ÿงน Creating Docker volumes for persistent cache..." +docker volume create go-mod-cache 2>/dev/null || echo "Volume go-mod-cache already exists" +docker volume create go-build-cache 2>/dev/null || echo "Volume go-build-cache already exists" + +echo "โœ… Optimized Go runner environment ready!" +echo "" +echo "๐Ÿš€ Performance optimizations applied:" +echo " - Persistent Go module and build cache" +echo " - Pre-warmed dependencies" +echo " - Optimized build flags (-ldflags -s -w)" +echo " - Increased memory and CPU limits for production" +echo " - Faster compilation timeouts" +echo "" +echo "๐Ÿ’ก To use in production, set these environment variables:" +echo " NODE_ENV=production" +echo " DOCKER_MEMORY_LIMIT=512m" +echo " DOCKER_CPU_QUOTA=200000" +echo " DOCKER_GO_TIMEOUT=120000" +echo " USE_NATIVE_GO_EXECUTOR=true" \ No newline at end of file diff --git a/packages/backend/go-executor/executor.go b/packages/backend/go-executor/executor.go index 2d70805..a29178d 100644 --- a/packages/backend/go-executor/executor.go +++ b/packages/backend/go-executor/executor.go @@ -27,9 +27,9 @@ type TestResult struct { } type ExecutionMetadata struct { - FunctionName string `json:"functionName"` - ParameterTypes []string `json:"parameterTypes"` - ReturnType string `json:"returnType"` + FunctionName string `json:"functionName"` + ParameterTypes []string `json:"parameterTypes"` + ReturnType string `json:"returnType"` } func main() { @@ -81,9 +81,9 @@ func main() { } func executeCode(userCode string, testCases []TestCase, metadata ExecutionMetadata) []TestResult { - // Create temporary directory - tmpDir, err := ioutil.TempDir("/tmp", "go-exec-") - if err != nil { + // Use a more predictable temp directory location to leverage volume mounts + tmpDir := "/tmp/go-exec" + if err := os.MkdirAll(tmpDir, 0755); err != nil { return []TestResult{{ Passed: false, Error: fmt.Sprintf("Failed to create temp dir: %v", err), @@ -96,7 +96,7 @@ func executeCode(userCode string, testCases []TestCase, metadata ExecutionMetada // Generate optimized test executable executableCode := generateExecutableCode(cleanedCode, testCases, metadata) - + codeFile := filepath.Join(tmpDir, "main.go") if err := ioutil.WriteFile(codeFile, []byte(executableCode), 0644); err != nil { return []TestResult{{ @@ -105,15 +105,30 @@ func executeCode(userCode string, testCases []TestCase, metadata ExecutionMetada }} } - // Build executable (only user code + minimal test framework) + // Build executable with optimizations for faster compilation execPath := filepath.Join(tmpDir, "test-exec") - buildCmd := exec.Command("go", "build", "-o", execPath, codeFile) - buildCmd.Env = append(os.Environ(), + + // Set shorter timeout for builds since cache should be warm + buildTimeout := 15 * time.Second + buildCtx, buildCancel := context.WithTimeout(context.Background(), buildTimeout) + defer buildCancel() + + buildCmd := exec.CommandContext(buildCtx, "go", "build", "-ldflags", "-s -w", "-o", execPath, codeFile) + buildCmd.Env = append(os.Environ(), "CGO_ENABLED=0", - "GOCACHE=/tmp/gocache", + "GOCACHE=/go/cache", // Use persistent cache from volume mount + "GOMODCACHE=/go/pkg/mod", // Use persistent module cache + "GOPROXY=direct", // Skip proxy for faster local builds + "GOSUMDB=off", // Skip checksum verification for speed ) if output, err := buildCmd.CombinedOutput(); err != nil { + if buildCtx.Err() == context.DeadlineExceeded { + return []TestResult{{ + Passed: false, + Error: fmt.Sprintf("Compilation timeout after %v\nPartial output: %s", buildTimeout, string(output)), + }} + } return []TestResult{{ Passed: false, Error: fmt.Sprintf("Compilation failed: %v\nOutput: %s", err, string(output)), @@ -121,29 +136,25 @@ func executeCode(userCode string, testCases []TestCase, metadata ExecutionMetada } // Execute the binary with timeout - execCmd := exec.Command(execPath) - execCmd.Dir = tmpDir - - // Set timeout for execution - timeout := 5 * time.Second - + execTimeout := 10 * time.Second // Increased from 5s for complex test cases + // Create context with timeout for cleaner cancellation - ctx, cancel := context.WithTimeout(context.Background(), timeout) + ctx, cancel := context.WithTimeout(context.Background(), execTimeout) defer cancel() - - execCmd = exec.CommandContext(ctx, execPath) + + execCmd := exec.CommandContext(ctx, execPath) execCmd.Dir = tmpDir - + // Execute with timeout output, execErr := execCmd.CombinedOutput() - + if ctx.Err() == context.DeadlineExceeded { return []TestResult{{ Passed: false, - Error: "Execution timeout", + Error: fmt.Sprintf("Execution timeout after %v", execTimeout), }} } - + if execErr != nil { return []TestResult{{ Passed: false, @@ -166,36 +177,36 @@ func executeCode(userCode string, testCases []TestCase, metadata ExecutionMetada func cleanUserCode(code string) string { lines := strings.Split(code, "\n") var cleanedLines []string - + inImportBlock := false inMainFunction := false braceCount := 0 - + for _, line := range lines { trimmed := strings.TrimSpace(line) - + // Skip package declarations if strings.HasPrefix(trimmed, "package ") { continue } - + // Skip imports (executor provides its own imports) if strings.HasPrefix(trimmed, "import ") && !strings.Contains(trimmed, "(") { continue } - + if strings.HasPrefix(trimmed, "import (") { inImportBlock = true continue } - + if inImportBlock { if trimmed == ")" { inImportBlock = false } continue } - + // Skip user's main function if strings.HasPrefix(trimmed, "func main(") { inMainFunction = true @@ -211,7 +222,7 @@ func cleanUserCode(code string) string { continue } } - + if inMainFunction { // Count braces to know when main function ends for _, r := range line { @@ -221,16 +232,16 @@ func cleanUserCode(code string) string { braceCount-- } } - + if braceCount <= 0 { inMainFunction = false } continue } - + cleanedLines = append(cleanedLines, line) } - + // Remove empty lines at the beginning and end for len(cleanedLines) > 0 && strings.TrimSpace(cleanedLines[0]) == "" { cleanedLines = cleanedLines[1:] @@ -238,13 +249,13 @@ func cleanUserCode(code string) string { for len(cleanedLines) > 0 && strings.TrimSpace(cleanedLines[len(cleanedLines)-1]) == "" { cleanedLines = cleanedLines[:len(cleanedLines)-1] } - + return strings.Join(cleanedLines, "\n") } func generateExecutableCode(userCode string, testCases []TestCase, metadata ExecutionMetadata) string { testCasesCode := generateTestCasesCode(testCases, metadata.FunctionName) - + return fmt.Sprintf(`package main import ( @@ -254,11 +265,11 @@ import ( ) type TestResult struct { - Passed bool ` + "`json:\"passed\"`" + ` - Error string ` + "`json:\"error,omitempty\"`" + ` - Expected interface{} ` + "`json:\"expected\"`" + ` - Actual interface{} ` + "`json:\"actual,omitempty\"`" + ` - Description string ` + "`json:\"description\"`" + ` + Passed bool `+"`json:\"passed\"`"+` + Error string `+"`json:\"error,omitempty\"`"+` + Expected interface{} `+"`json:\"expected\"`"+` + Actual interface{} `+"`json:\"actual,omitempty\"`"+` + Description string `+"`json:\"description\"`"+` } %s @@ -275,7 +286,7 @@ func main() { func generateTestCasesCode(testCases []TestCase, functionName string) string { var testCode strings.Builder - + for testCaseIndex, testCase := range testCases { // Convert inputs to Go code var inputs []string @@ -313,7 +324,7 @@ func generateTestCasesCode(testCases []TestCase, functionName string) string { inputs = append(inputs, fmt.Sprintf("%v", v)) } } - + // Convert expected output var expected string switch v := testCase.Expected.(type) { @@ -347,7 +358,7 @@ func generateTestCasesCode(testCases []TestCase, functionName string) string { default: expected = fmt.Sprintf("%v", v) } - + testCode.WriteString(fmt.Sprintf(` // Test case %d: %s { @@ -370,7 +381,7 @@ func generateTestCasesCode(testCases []TestCase, functionName string) string { } `, testCaseIndex+1, testCase.Description, expected, functionName, strings.Join(inputs, ", "), testCase.Description)) } - + return testCode.String() } @@ -386,4 +397,4 @@ func outputError(message string) { }} output, _ := json.Marshal(result) fmt.Println(string(output)) -} \ No newline at end of file +} diff --git a/packages/backend/go-executor/go-executor b/packages/backend/go-executor/go-executor index 24b9f19..098922b 100755 Binary files a/packages/backend/go-executor/go-executor and b/packages/backend/go-executor/go-executor differ diff --git a/packages/backend/go-executor/main.go b/packages/backend/go-executor/main.go.unused similarity index 100% rename from packages/backend/go-executor/main.go rename to packages/backend/go-executor/main.go.unused diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts index 32d2753..d60fe3c 100644 --- a/packages/backend/src/config/index.ts +++ b/packages/backend/src/config/index.ts @@ -82,15 +82,27 @@ export const config: AppConfig = { mongoUri: getMongoUri(), environment: process.env.NODE_ENV || "development", docker: { - memory: process.env.DOCKER_MEMORY_LIMIT || "128m", - cpuQuota: process.env.DOCKER_CPU_QUOTA || "100000", - timeout: process.env.DOCKER_TIMEOUT || "10000", - goTimeout: process.env.DOCKER_GO_TIMEOUT || "35000", + memory: + process.env.DOCKER_MEMORY_LIMIT || + (process.env.NODE_ENV === "production" ? "512m" : "256m"), + cpuQuota: + process.env.DOCKER_CPU_QUOTA || + (process.env.NODE_ENV === "production" ? "200000" : "100000"), + timeout: + process.env.DOCKER_TIMEOUT || + (process.env.NODE_ENV === "production" ? "60000" : "30000"), + goTimeout: + process.env.DOCKER_GO_TIMEOUT || + (process.env.NODE_ENV === "production" ? "120000" : "60000"), tempDir: process.env.RUNNER_TEMP_DIR || path.join(process.cwd(), "temp"), }, executor: { useNativeGo: process.env.USE_NATIVE_GO_EXECUTOR === "true", - nativeGoTimeout: parseInt(process.env.NATIVE_GO_TIMEOUT || "45000", 10), // 45 seconds default for production + nativeGoTimeout: parseInt( + process.env.NATIVE_GO_TIMEOUT || + (process.env.NODE_ENV === "production" ? "90000" : "45000"), + 10, + ), }, github: { clientId: process.env.GITHUB_CLIENT_ID || "", diff --git a/packages/backend/src/services/goExecutor.ts b/packages/backend/src/services/goExecutor.ts index b50f9f1..971afbe 100644 --- a/packages/backend/src/services/goExecutor.ts +++ b/packages/backend/src/services/goExecutor.ts @@ -34,7 +34,7 @@ export class GoExecutor { // Log important configurations console.log(`Go Executor initialized with: - - Native Go Executor: ${config.executor.useNativeGo ? 'ENABLED' : 'DISABLED'} + - Native Go Executor: ${config.executor.useNativeGo ? "ENABLED" : "DISABLED"} - Temp directory: ${this.tempDir} - Timeout: ${this.TIMEOUT_MS}ms - Memory limit: ${config.docker.memory} @@ -60,18 +60,58 @@ export class GoExecutor { } } + private async ensureGoVolumes(): Promise { + try { + // Create persistent volumes for Go cache to speed up compilation + const volumeNames = ["go-mod-cache", "go-build-cache"]; + + for (const volumeName of volumeNames) { + try { + // Check if volume exists + await this.docker.getVolume(volumeName).inspect(); + console.log(`Go volume ${volumeName} already exists`); + } catch (error) { + // Volume doesn't exist, create it + console.log(`Creating Go cache volume: ${volumeName}`); + await this.docker.createVolume({ + Name: volumeName, + Labels: { + "codequest.cache": "go", + "codequest.purpose": volumeName.includes("mod") + ? "modules" + : "build", + }, + }); + console.log(`Successfully created Go cache volume: ${volumeName}`); + } + } + } catch (error) { + console.warn( + `Warning: Failed to create Go cache volumes: ${error}. Continuing without persistent cache.`, + ); + // Don't throw error - we can continue without persistent cache, just slower + } + } + private async prepareGoFiles( code: string, containerId: string, testCases: TestCase[], - metadata: ChallengeMetadata - ): Promise<{userCodeFile: string, testCasesFile: string, metadataFile: string}> { + metadata: ChallengeMetadata, + ): Promise<{ + userCodeFile: string; + testCasesFile: string; + metadataFile: string; + }> { // Ensure temp directory exists with proper permissions await this.ensureTempDir(); const userCodeFile = path.join(this.tempDir, `${containerId}-code.go`); const testCasesFile = path.join(this.tempDir, `${containerId}-tests.json`); - const metadataFile = path.join(this.tempDir, `${containerId}-metadata.json`); + const metadataFile = path.join( + this.tempDir, + `${containerId}-metadata.json`, + ); console.log(`Preparing Go files for precompiled executor:`); console.log(`- User code: ${userCodeFile}`); @@ -82,20 +122,23 @@ export class GoExecutor { await fs.writeFile(userCodeFile, code, { mode: 0o666 }); // Save test cases as JSON - await fs.writeFile(testCasesFile, JSON.stringify(testCases, null, 2), { mode: 0o666 }); + await fs.writeFile(testCasesFile, JSON.stringify(testCases, null, 2), { + mode: 0o666, + }); // Save metadata as JSON const execMetadata = { functionName: metadata.functionName, parameterTypes: metadata.parameterTypes, - returnType: metadata.returnType + returnType: metadata.returnType, }; - await fs.writeFile(metadataFile, JSON.stringify(execMetadata, null, 2), { mode: 0o666 }); + await fs.writeFile(metadataFile, JSON.stringify(execMetadata, null, 2), { + mode: 0o666, + }); return { userCodeFile, testCasesFile, metadataFile }; } - private async getContainerLogs(container: Docker.Container): Promise { return new Promise((resolve, reject) => { container.logs( @@ -115,7 +158,7 @@ export class GoExecutor { (stream as Readable).on("data", (chunk) => chunks.push(chunk)); (stream as Readable).on("end", () => resolve(Buffer.concat(chunks))); (stream as Readable).on("error", reject); - } + }, ); }); } @@ -123,16 +166,16 @@ export class GoExecutor { private processResults( logs: Buffer, testCases: TestCase[], - executionTime: [number, number] + executionTime: [number, number], ): SubmissionResult { try { console.log("Processing Go execution results..."); const timeInMs = executionTime[0] * 1000 + executionTime[1] / 1000000; - + // Docker logs have an 8-byte header that needs to be stripped // Format: [STREAM_TYPE][0][0][0][SIZE][SIZE][SIZE][SIZE][DATA...] let outputStr = logs.toString("utf8"); - + // Remove Docker log headers (8 bytes at the start of each log line) if (outputStr.length > 8 && outputStr.charCodeAt(0) === 1) { // Skip the 8-byte header and get the actual content @@ -141,21 +184,29 @@ export class GoExecutor { console.log("Raw Go logs length:", outputStr.length); if (outputStr.length > 200) { - console.log("Raw Go logs preview:", outputStr.substring(0, 200) + "..."); + console.log( + "Raw Go logs preview:", + outputStr.substring(0, 200) + "...", + ); } else { console.log("Raw Go logs:", outputStr); } // Check for compilation and execution errors - if (outputStr.includes('EXECUTION_ERROR:') || outputStr.includes('missing return') || outputStr.includes('syntax error') || outputStr.includes('build failed')) { - const errorMsg = outputStr.includes('missing return') + if ( + outputStr.includes("EXECUTION_ERROR:") || + outputStr.includes("missing return") || + outputStr.includes("syntax error") || + outputStr.includes("build failed") + ) { + const errorMsg = outputStr.includes("missing return") ? "Function is missing return statement. Make sure your function returns a value." - : outputStr.includes('syntax error') - ? "Syntax error in your Go code. Please check your code for errors." - : outputStr.includes('build failed') - ? "Go compilation failed. Please check your code for errors." - : "Go execution error occurred."; - + : outputStr.includes("syntax error") + ? "Syntax error in your Go code. Please check your code for errors." + : outputStr.includes("build failed") + ? "Go compilation failed. Please check your code for errors." + : "Go execution error occurred."; + return { success: false, results: testCases.map((testCase) => ({ @@ -207,7 +258,7 @@ export class GoExecutor { memoryUsed: 0, expected: result.expected, actual: result.actual, - }) + }), ); const allPassed = results.every((result) => result.passed); @@ -247,23 +298,32 @@ export class GoExecutor { async executeGo( code: string, testCases: TestCase[], - metadata: ChallengeMetadata + metadata: ChallengeMetadata, ): Promise { // Use native executor if configured and available if (config.executor.useNativeGo) { try { - console.log('๐Ÿš€ Attempting native Go execution...'); + console.log("๐Ÿš€ Attempting native Go execution..."); const isAvailable = await this.nativeExecutor.isAvailable(); - + if (isAvailable) { - const result = await this.nativeExecutor.executeGo(code, testCases, metadata); - console.log('โœ… Native Go execution completed successfully'); + const result = await this.nativeExecutor.executeGo( + code, + testCases, + metadata, + ); + console.log("โœ… Native Go execution completed successfully"); return result; } else { - console.log('โš ๏ธ Native Go executor not available, falling back to Docker'); + console.log( + "โš ๏ธ Native Go executor not available, falling back to Docker", + ); } } catch (error) { - console.error('โŒ Native Go execution failed, falling back to Docker:', error); + console.error( + "โŒ Native Go execution failed, falling back to Docker:", + error, + ); // Continue to Docker execution below } } @@ -274,27 +334,39 @@ export class GoExecutor { private async executeGoWithDocker( code: string, testCases: TestCase[], - metadata: ChallengeMetadata + metadata: ChallengeMetadata, ): Promise { - console.log('๐Ÿณ Using Docker Go execution (fallback)'); + console.log("๐Ÿณ Using Docker Go execution (fallback)"); const containerId = uuidv4(); - let files: {userCodeFile: string, testCasesFile: string, metadataFile: string} | null = null; + let files: { + userCodeFile: string; + testCasesFile: string; + metadataFile: string; + } | null = null; let container: Docker.Container | null = null; try { files = await this.prepareGoFiles(code, containerId, testCases, metadata); - + const resolvedUserCodeFile = path.resolve(files.userCodeFile); const resolvedTestCasesFile = path.resolve(files.testCasesFile); const resolvedMetadataFile = path.resolve(files.metadataFile); console.log(`Starting Go container with precompiled executor`); + // Create persistent volumes for Go cache if they don't exist + await this.ensureGoVolumes(); + // Create container using precompiled executor (no AutoRemove to prevent race condition) container = await this.docker.createContainer({ Image: "go-runner", name: `go-execution-${containerId}`, - Cmd: ["go-executor", "/code/user-code.go", "/code/test-cases.json", "/code/metadata.json"], + Cmd: [ + "go-executor", + "/code/user-code.go", + "/code/test-cases.json", + "/code/metadata.json", + ], HostConfig: { AutoRemove: false, // Manual cleanup to prevent race condition Memory: @@ -305,7 +377,10 @@ export class GoExecutor { Binds: [ `${resolvedUserCodeFile}:/code/user-code.go:ro`, `${resolvedTestCasesFile}:/code/test-cases.json:ro`, - `${resolvedMetadataFile}:/code/metadata.json:ro` + `${resolvedMetadataFile}:/code/metadata.json:ro`, + // Add persistent cache volumes for faster compilation + "go-mod-cache:/go/pkg/mod", + "go-build-cache:/go/cache", ], }, WorkingDir: "/code", @@ -314,28 +389,42 @@ export class GoExecutor { console.log(`Go container created: ${container.id}`); const startTime = process.hrtime(); - console.log(`Starting Go container execution at: ${new Date().toISOString()}`); - + console.log( + `Starting Go container execution at: ${new Date().toISOString()}`, + ); + await container.start(); - console.log(`Go container started: ${container.id} at: ${new Date().toISOString()}`); + console.log( + `Go container started: ${container.id} at: ${new Date().toISOString()}`, + ); // Wait for container to complete with timeout const containerPromise = container.wait(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { - console.log(`Go execution TIMEOUT after ${this.TIMEOUT_MS}ms at: ${new Date().toISOString()}`); - reject(new Error(`Go execution timeout - took longer than ${this.TIMEOUT_MS}ms`)); + console.log( + `Go execution TIMEOUT after ${this.TIMEOUT_MS}ms at: ${new Date().toISOString()}`, + ); + reject( + new Error( + `Go execution timeout - took longer than ${this.TIMEOUT_MS}ms`, + ), + ); }, this.TIMEOUT_MS); }); - console.log(`Waiting for container to complete with ${this.TIMEOUT_MS}ms timeout...`); - + console.log( + `Waiting for container to complete with ${this.TIMEOUT_MS}ms timeout...`, + ); + // Add periodic status checking const statusCheckInterval = setInterval(async () => { try { if (container) { const containerInfo = await container.inspect(); - console.log(`Container ${container.id} status: ${containerInfo.State.Status}, Running: ${containerInfo.State.Running}`); + console.log( + `Container ${container.id} status: ${containerInfo.State.Status}, Running: ${containerInfo.State.Running}`, + ); } } catch (error) { console.log(`Failed to inspect container: ${error}`); @@ -344,16 +433,17 @@ export class GoExecutor { let logs: Buffer; let executionTime: [number, number]; - + try { await Promise.race([containerPromise, timeoutPromise]); clearInterval(statusCheckInterval); - console.log(`Go container finished: ${container.id} at: ${new Date().toISOString()}`); - + console.log( + `Go container finished: ${container.id} at: ${new Date().toISOString()}`, + ); + // Get logs immediately after container finishes (before cleanup) logs = await this.getContainerLogs(container); executionTime = process.hrtime(startTime); - } catch (error) { clearInterval(statusCheckInterval); // Try to get logs even if there was an error @@ -394,7 +484,7 @@ export class GoExecutor { if (container) { try { console.log(`Cleaning up Go container: ${container.id}`); - + // Stop container if still running try { const containerInfo = await container.inspect(); @@ -402,17 +492,20 @@ export class GoExecutor { await container.stop({ t: 1 }); // 1 second grace period } } catch (stopError) { - console.log(`Container stop error (expected if already stopped): ${stopError instanceof Error ? stopError.message : stopError}`); + console.log( + `Container stop error (expected if already stopped): ${stopError instanceof Error ? stopError.message : stopError}`, + ); } - + // Remove container try { await container.remove({ force: true }); console.log(`Successfully removed Go container: ${container.id}`); } catch (removeError) { - console.log(`Container remove error: ${removeError instanceof Error ? removeError.message : removeError}`); + console.log( + `Container remove error: ${removeError instanceof Error ? removeError.message : removeError}`, + ); } - } catch (error) { console.error("Error cleaning up Go container:", error); } @@ -437,4 +530,4 @@ export class GoExecutor { } } } -} \ No newline at end of file +} diff --git a/packages/backend/src/services/nativeGoExecutor.ts b/packages/backend/src/services/nativeGoExecutor.ts index 2346082..d15e9c8 100644 --- a/packages/backend/src/services/nativeGoExecutor.ts +++ b/packages/backend/src/services/nativeGoExecutor.ts @@ -1,9 +1,9 @@ -import { spawn } from 'child_process'; -import { TestCase, ExecutionResult, SubmissionResult } from '../types'; -import path from 'path'; -import fs from 'fs/promises'; -import os from 'os'; -import { config } from '../config'; +import { spawn } from "child_process"; +import { TestCase, ExecutionResult, SubmissionResult } from "../types"; +import path from "path"; +import fs from "fs/promises"; +import os from "os"; +import { config } from "../config"; interface ChallengeMetadata { functionName: string; @@ -24,8 +24,36 @@ export class NativeGoExecutor { constructor() { this.TIMEOUT_MS = config.executor.nativeGoTimeout; - console.log(`Native Go Executor initialized with ${this.TIMEOUT_MS}ms timeout for compilation + execution`); + console.log( + `Native Go Executor initialized with ${this.TIMEOUT_MS}ms timeout for compilation + execution`, + ); console.log(`Environment: ${config.environment}`); + + // Ensure cache directories exist + this.ensureCacheDirectories(); + } + + /** + * Ensure persistent cache directories exist for faster compilation + */ + private async ensureCacheDirectories(): Promise { + try { + const cacheDir = + process.env.GOCACHE || path.join(os.tmpdir(), "codequest-gocache"); + const modCacheDir = + process.env.GOMODCACHE || + path.join(os.tmpdir(), "codequest-gomodcache"); + + await fs.mkdir(cacheDir, { recursive: true }); + await fs.mkdir(modCacheDir, { recursive: true }); + + console.log( + `Native Go cache directories ready: ${cacheDir}, ${modCacheDir}`, + ); + } catch (error) { + console.warn(`Warning: Failed to create Go cache directories: ${error}`); + // Continue without persistent cache + } } /** @@ -33,10 +61,10 @@ export class NativeGoExecutor { */ private async checkGoAvailability(): Promise { return new Promise((resolve) => { - const childProcess = spawn('go', ['version']); + const childProcess = spawn("go", ["version"]); let isResolved = false; let timeoutId: ReturnType; - + const resolveOnce = (result: boolean) => { if (!isResolved) { isResolved = true; @@ -46,15 +74,15 @@ export class NativeGoExecutor { resolve(result); } }; - - childProcess.on('close', (code) => { + + childProcess.on("close", (code) => { resolveOnce(code === 0); }); - - childProcess.on('error', () => { + + childProcess.on("error", () => { resolveOnce(false); }); - + // Timeout after 5 seconds timeoutId = setTimeout(() => { childProcess.kill(); @@ -68,7 +96,7 @@ export class NativeGoExecutor { * Based on the CLI's code cleaning approach */ private cleanGoUserCode(code: string): string { - const lines = code.split('\n'); + const lines = code.split("\n"); const cleanedLines: string[] = []; let inImportBlock = false; // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars @@ -78,37 +106,40 @@ export class NativeGoExecutor { for (const line of lines) { const trimmedLine = line.trim(); - + // Skip package declarations - if (trimmedLine.startsWith('package ')) { + if (trimmedLine.startsWith("package ")) { continue; } // Handle import blocks - if (trimmedLine.startsWith('import (')) { + if (trimmedLine.startsWith("import (")) { inImportBlock = true; continue; } if (inImportBlock) { - if (trimmedLine === ')') { + if (trimmedLine === ")") { inImportBlock = false; } continue; } - + // Skip single line imports - if (trimmedLine.startsWith('import ')) { + if (trimmedLine.startsWith("import ")) { continue; } // Detect main function start - if (trimmedLine.includes('func main()') || trimmedLine.includes('func main() ')) { + if ( + trimmedLine.includes("func main()") || + trimmedLine.includes("func main() ") + ) { inMainFunction = true; mainFunctionBraceCount = 0; // Count braces on the same line for (const char of line) { - if (char === '{') mainFunctionBraceCount++; - if (char === '}') mainFunctionBraceCount--; + if (char === "{") mainFunctionBraceCount++; + if (char === "}") mainFunctionBraceCount--; } continue; } @@ -116,8 +147,8 @@ export class NativeGoExecutor { // Skip main function content if (inMainFunction) { for (const char of line) { - if (char === '{') mainFunctionBraceCount++; - if (char === '}') mainFunctionBraceCount--; + if (char === "{") mainFunctionBraceCount++; + if (char === "}") mainFunctionBraceCount--; } if (mainFunctionBraceCount <= 0) { inMainFunction = false; @@ -129,7 +160,7 @@ export class NativeGoExecutor { cleanedLines.push(line); } - return cleanedLines.join('\n'); + return cleanedLines.join("\n"); } /** @@ -138,19 +169,22 @@ export class NativeGoExecutor { private generateGoTestCode( userCode: string, testCases: TestCase[], - metadata: ChallengeMetadata + metadata: ChallengeMetadata, ): string { const cleanedUserCode = this.cleanGoUserCode(userCode); - + // Generate individual test case execution - const testCaseExecutions = testCases.map((testCase, index) => { - const inputs = Array.isArray(testCase.input) ? testCase.input : [testCase.input]; - const expected = testCase.expected; - - // Generate function call with proper type casting - const functionCall = this.generateFunctionCall(metadata, inputs, index); - - return ` + const testCaseExecutions = testCases + .map((testCase, index) => { + const inputs = Array.isArray(testCase.input) + ? testCase.input + : [testCase.input]; + const expected = testCase.expected; + + // Generate function call with proper type casting + const functionCall = this.generateFunctionCall(metadata, inputs, index); + + return ` // Test case ${index + 1} { var result interface{} @@ -176,7 +210,8 @@ export class NativeGoExecutor { results = append(results, testResult) }`; - }).join('\n'); + }) + .join("\n"); return ` package main @@ -241,34 +276,42 @@ func main() { metadata: ChallengeMetadata, inputs: any[], // eslint-disable-next-line no-unused-vars - _testIndex: number + _testIndex: number, ): string { const { functionName, parameterTypes } = metadata; - + if (inputs.length === 0) { return `result = ${functionName}()`; } - + // Generate type-specific argument casting - const args = inputs.map((input, i) => { - const paramType = parameterTypes[i] || 'interface{}'; - const value = JSON.stringify(input); - - switch (paramType.toLowerCase()) { - case 'int': - return typeof input === 'number' ? input.toString() : `int(${value})`; - case 'string': - return value; - case 'float64': - case 'float32': - return typeof input === 'number' ? input.toString() : `float64(${value})`; - case 'bool': - return typeof input === 'boolean' ? input.toString() : `bool(${value})`; - default: - return value; - } - }).join(', '); - + const args = inputs + .map((input, i) => { + const paramType = parameterTypes[i] || "interface{}"; + const value = JSON.stringify(input); + + switch (paramType.toLowerCase()) { + case "int": + return typeof input === "number" + ? input.toString() + : `int(${value})`; + case "string": + return value; + case "float64": + case "float32": + return typeof input === "number" + ? input.toString() + : `float64(${value})`; + case "bool": + return typeof input === "boolean" + ? input.toString() + : `bool(${value})`; + default: + return value; + } + }) + .join(", "); + return `result = ${functionName}(${args})`; } @@ -277,59 +320,74 @@ func main() { */ private async executeGoCode( goCode: string, - timeoutMs: number + timeoutMs: number, ): Promise { const startTime = Date.now(); - + console.log(`๐Ÿ—๏ธ Starting Go execution with ${timeoutMs}ms timeout`); - + // Create unique execution directory - const execDir = await fs.mkdtemp(path.join(os.tmpdir(), 'go-native-')); + const execDir = await fs.mkdtemp(path.join(os.tmpdir(), "go-native-")); console.log(`๐Ÿ“ Created execution directory: ${execDir}`); - + try { // Write Go code to main.go - const mainFile = path.join(execDir, 'main.go'); + const mainFile = path.join(execDir, "main.go"); await fs.writeFile(mainFile, goCode); console.log(`๐Ÿ“ Written Go code to ${mainFile} (${goCode.length} chars)`); // Initialize Go module console.log(`๐Ÿ”ง Initializing Go module...`); const initStartTime = Date.now(); - const initResult = await this.runCommand('go', ['mod', 'init', 'solution'], execDir, 10000); // 10s for init + const initResult = await this.runCommand( + "go", + ["mod", "init", "solution"], + execDir, + 10000, + ); // 10s for init const initDuration = Date.now() - initStartTime; - console.log(`๐Ÿ”ง Go module init completed in ${initDuration}ms (success: ${initResult.success})`); - + console.log( + `๐Ÿ”ง Go module init completed in ${initDuration}ms (success: ${initResult.success})`, + ); + if (!initResult.success) { return { success: false, - output: '', + output: "", error: `Go module initialization failed: ${initResult.error}`, duration: Date.now() - startTime, - exitCode: initResult.exitCode + exitCode: initResult.exitCode, }; } // Execute go run main.go console.log(`๐Ÿš€ Starting Go compilation and execution...`); const runStartTime = Date.now(); - const runResult = await this.runCommand('go', ['run', 'main.go'], execDir, timeoutMs - initDuration); + const runResult = await this.runCommand( + "go", + ["run", "main.go"], + execDir, + timeoutMs - initDuration, + ); const runDuration = Date.now() - runStartTime; - console.log(`๐Ÿš€ Go run completed in ${runDuration}ms (success: ${runResult.success})`); - - if (!runResult.success && runResult.error?.includes('timed out')) { + console.log( + `๐Ÿš€ Go run completed in ${runDuration}ms (success: ${runResult.success})`, + ); + + if (!runResult.success && runResult.error?.includes("timed out")) { console.error(`โฐ Go execution timed out after ${runDuration}ms`); - console.error(`๐Ÿ“Š Timing breakdown: Init=${initDuration}ms, Run=${runDuration}ms, Total=${Date.now() - startTime}ms`); + console.error( + `๐Ÿ“Š Timing breakdown: Init=${initDuration}ms, Run=${runDuration}ms, Total=${Date.now() - startTime}ms`, + ); } - + return { success: runResult.success, output: runResult.output, error: runResult.error, duration: Date.now() - startTime, - exitCode: runResult.exitCode + exitCode: runResult.exitCode, }; - } finally { // Clean up execution directory try { @@ -347,28 +405,40 @@ func main() { command: string, args: string[], cwd: string, - timeoutMs: number - ): Promise<{ success: boolean; output: string; error?: string; exitCode: number }> { + timeoutMs: number, + ): Promise<{ + success: boolean; + output: string; + error?: string; + exitCode: number; + }> { return new Promise((resolve) => { - // Set Go environment variables to use temporary directories + // Set Go environment variables for optimal performance const env: Record = { ...process.env, - GOCACHE: path.join(cwd, '.gocache'), // Use execution directory for Go build cache - GOPATH: path.join(cwd, '.gopath'), // Set temporary GOPATH - GO111MODULE: 'on', // Ensure module mode is enabled - CGO_ENABLED: '0', // Disable CGO for faster compilation and better portability - GOPROXY: 'direct', // Use direct mode to avoid proxy delays - GOSUMDB: 'off' // Disable checksum database for faster builds + GOCACHE: + process.env.GOCACHE || path.join(os.tmpdir(), "codequest-gocache"), + GOMODCACHE: + process.env.GOMODCACHE || + path.join(os.tmpdir(), "codequest-gomodcache"), + GO111MODULE: "on", // Ensure module mode is enabled + CGO_ENABLED: "0", // Disable CGO for faster compilation and better portability + GOPROXY: "direct", // Use direct mode to avoid proxy delays + GOSUMDB: "off", // Disable checksum database for faster builds }; - - console.log(`๐Ÿ”จ Running: ${command} ${args.join(' ')} (timeout: ${timeoutMs}ms)`); + + console.log( + `๐Ÿ”จ Running: ${command} ${args.join(" ")} (timeout: ${timeoutMs}ms)`, + ); console.log(`๐Ÿ“‚ Working directory: ${cwd}`); - console.log(`๐ŸŒ Environment: GOCACHE=${env.GOCACHE}, GO111MODULE=${env.GO111MODULE}, CGO_ENABLED=${env.CGO_ENABLED}`); - + console.log( + `๐ŸŒ Environment: GOCACHE=${env.GOCACHE}, GO111MODULE=${env.GO111MODULE}, CGO_ENABLED=${env.CGO_ENABLED}`, + ); + const childProcess = spawn(command, args, { cwd, env }); - - let stdout = ''; - let stderr = ''; + + let stdout = ""; + let stderr = ""; let timeoutId: ReturnType; let isResolved = false; @@ -382,39 +452,41 @@ func main() { // Set timeout timeoutId = setTimeout(() => { - childProcess.kill('SIGKILL'); + childProcess.kill("SIGKILL"); resolveOnce({ success: false, output: stdout, error: `Command timed out after ${timeoutMs}ms`, - exitCode: 124 + exitCode: 124, }); }, timeoutMs); - childProcess.stdout?.on('data', (data: any) => { + childProcess.stdout?.on("data", (data: any) => { stdout += data.toString(); }); - childProcess.stderr?.on('data', (data: any) => { + childProcess.stderr?.on("data", (data: any) => { stderr += data.toString(); }); - childProcess.on('close', (code: number | null) => { + childProcess.on("close", (code: number | null) => { const success = code === 0; resolveOnce({ success, output: stdout, - error: success ? undefined : stderr || `Process exited with code ${code}`, - exitCode: code || 0 + error: success + ? undefined + : stderr || `Process exited with code ${code}`, + exitCode: code || 0, }); }); - childProcess.on('error', (error: Error) => { + childProcess.on("error", (error: Error) => { resolveOnce({ success: false, output: stdout, error: error.message, - exitCode: 1 + exitCode: 1, }); }); }); @@ -425,17 +497,18 @@ func main() { */ private processResults( goResult: GoExecutionResult, - testCases: TestCase[] + testCases: TestCase[], ): SubmissionResult { try { if (!goResult.success) { - const errorMessage = goResult.error?.includes('missing return') + const errorMessage = goResult.error?.includes("missing return") ? "Function is missing return statement. Make sure your function returns a value." - : goResult.error?.includes('syntax error') - ? "Syntax error in your Go code. Please check your code for errors." - : goResult.error?.includes('build failed') || goResult.error?.includes('compilation') - ? "Go compilation failed. Please check your code for errors." - : `Go execution error: ${goResult.error || 'Unknown error'}`; + : goResult.error?.includes("syntax error") + ? "Syntax error in your Go code. Please check your code for errors." + : goResult.error?.includes("build failed") || + goResult.error?.includes("compilation") + ? "Go compilation failed. Please check your code for errors." + : `Go execution error: ${goResult.error || "Unknown error"}`; return { success: false, @@ -487,7 +560,7 @@ func main() { memoryUsed: 0, expected: result.expected, actual: result.actual, - }) + }), ); const allPassed = results.every((result) => result.passed); @@ -503,9 +576,8 @@ func main() { totalTests: testCases.length, }, }; - } catch (error) { - console.error('Error processing native Go results:', error); + console.error("Error processing native Go results:", error); return { success: false, results: testCases.map((testCase) => ({ @@ -531,16 +603,18 @@ func main() { async executeGo( code: string, testCases: TestCase[], - metadata: ChallengeMetadata + metadata: ChallengeMetadata, ): Promise { try { - console.log('๐Ÿš€ Native Go Executor: Starting execution'); + console.log("๐Ÿš€ Native Go Executor: Starting execution"); const startTime = Date.now(); // Check Go availability const goAvailable = await this.checkGoAvailability(); if (!goAvailable) { - throw new Error('Go runtime not available on system. Please install Go or use Docker executor.'); + throw new Error( + "Go runtime not available on system. Please install Go or use Docker executor.", + ); } // Generate test harness code @@ -549,20 +623,24 @@ func main() { // Execute the code const goResult = await this.executeGoCode(testCode, this.TIMEOUT_MS); - + const totalTime = Date.now() - startTime; - console.log(`๐ŸŽฏ Native Go Executor: Completed in ${totalTime}ms (${goResult.success ? 'SUCCESS' : 'FAILED'})`); + console.log( + `๐ŸŽฏ Native Go Executor: Completed in ${totalTime}ms (${goResult.success ? "SUCCESS" : "FAILED"})`, + ); return this.processResults(goResult, testCases); - } catch (error) { - console.error('Native Go execution error:', error); + console.error("Native Go execution error:", error); return { success: false, results: testCases.map((testCase) => ({ passed: false, - error: error instanceof Error ? error.message : 'Native Go execution failed', + error: + error instanceof Error + ? error.message + : "Native Go execution failed", testCase, executionTime: this.TIMEOUT_MS, memoryUsed: 0, @@ -583,4 +661,4 @@ func main() { async isAvailable(): Promise { return this.checkGoAvailability(); } -} \ No newline at end of file +}